@ditojs/admin 2.2.11 → 2.2.13

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ditojs/admin",
3
- "version": "2.2.11",
3
+ "version": "2.2.13",
4
4
  "type": "module",
5
5
  "description": "Dito.js Admin is a schema based admin interface for Dito.js Server, featuring auto-generated views and forms and built with Vue.js",
6
6
  "repository": "https://github.com/ditojs/dito/tree/master/packages/admin",
@@ -82,7 +82,7 @@
82
82
  "vite": "^4.3.1"
83
83
  },
84
84
  "types": "types",
85
- "gitHead": "52aada1f67b13f9b30b745777c64649c6c7b171f",
85
+ "gitHead": "cb2613f69dcb1837db22d95c736ea2d414326f5c",
86
86
  "scripts": {
87
87
  "build": "vite build",
88
88
  "watch": "yarn build --mode 'development' --watch",
package/src/DitoAdmin.js CHANGED
@@ -13,6 +13,7 @@ import * as components from './components/index.js'
13
13
  import * as types from './types/index.js'
14
14
  import DitoRoot from './components/DitoRoot.vue'
15
15
  import DitoTypeComponent from './DitoTypeComponent.js'
16
+ import ResizeDirective from './directives/resize.js'
16
17
  import { getResource } from './utils/resource.js'
17
18
  import { deprecate } from './utils/deprecate.js'
18
19
  import { formatQuery } from './utils/route.js'
@@ -198,7 +199,7 @@ export default class DitoAdmin {
198
199
  componentName: 'VueNotifications'
199
200
  })
200
201
 
201
- // root.component('vue-modal', VueModal)
202
+ app.directive('resize', ResizeDirective)
202
203
 
203
204
  app.use(
204
205
  createRouter({
@@ -6,7 +6,7 @@
6
6
  v-for="(buttonSchema, buttonDataPath) in buttonSchemas"
7
7
  )
8
8
  DitoContainer(
9
- v-if="shouldRender(buttonSchema)"
9
+ v-if="shouldRenderSchema(buttonSchema)"
10
10
  :key="buttonDataPath"
11
11
  :schema="buttonSchema"
12
12
  :dataPath="buttonDataPath"
@@ -45,7 +45,9 @@ export default DitoComponent.component('DitoContainer', {
45
45
  single: { type: Boolean, default: false },
46
46
  nested: { type: Boolean, default: true },
47
47
  disabled: { type: Boolean, required: true },
48
- generateLabels: { type: Boolean, default: false }
48
+ generateLabels: { type: Boolean, default: false },
49
+ firstInRow: { type: Boolean, default: false },
50
+ lastInRow: { type: Boolean, default: false }
49
51
  },
50
52
 
51
53
  data() {
@@ -129,6 +131,9 @@ export default DitoComponent.component('DitoContainer', {
129
131
  [`${prefix}--has-label`]: this.hasLabel,
130
132
  [`${prefix}--aligned`]: keepAligned(this.schema),
131
133
  [`${prefix}--omit-padding`]: omitPadding(this.schema),
134
+ [`${prefix}--first-in-row`]: this.firstInRow,
135
+ [`${prefix}--last-in-row`]: this.lastInRow,
136
+ [`${prefix}--alone-in-row`]: this.firstInRow && this.lastInRow,
132
137
  ...(
133
138
  isString(containerClass)
134
139
  ? { [containerClass]: true }
@@ -212,19 +217,24 @@ export default DitoComponent.component('DitoContainer', {
212
217
  }
213
218
 
214
219
  &--aligned {
215
- // To align components with and without labels.
216
- justify-content: space-between;
220
+ // For components with labels, align the label at the top and the component
221
+ // at the bottom.
222
+ --justify: space-between;
217
223
 
218
224
  &:has(> :only-child) {
219
- justify-content: flex-end;
225
+ // But if there is no label, still align the component to the bottom.
226
+ --justify: flex-end;
220
227
  }
221
228
 
222
- // Don't align if neighbouring components aren't aligned either.
223
- // Look ahead:
224
- #{$self}:not(#{&}) + &,
225
- // Look behind:
226
- &:has(+ #{$self}:not(#{&})) {
227
- justify-content: flex-start;
229
+ &:not(#{$self}--only-in-row) {
230
+ // Now only apply alignment if there are neighbouring components no the
231
+ // same row that also align.
232
+ // Look ahead:
233
+ &:not(#{$self}--last-in-row) + #{&}:not(#{$self}--first-in-row),
234
+ // Look behind:
235
+ &:not(#{$self}--last-in-row):has(+ #{&}:not(#{$self}--first-in-row)) {
236
+ justify-content: var(--justify);
237
+ }
228
238
  }
229
239
  }
230
240
 
@@ -14,7 +14,7 @@
14
14
  v-for="(form, type) in forms"
15
15
  )
16
16
  a(
17
- v-if="shouldRender(form)"
17
+ v-if="shouldRenderSchema(form)"
18
18
  v-show="shouldShow(form)"
19
19
  :class="getFormClass(form, type)"
20
20
  @mousedown.stop="onPulldownMouseDown(type)"
@@ -61,7 +61,7 @@ export default DitoComponent.component('DitoCreateButton', {
61
61
 
62
62
  methods: {
63
63
  createItem(form, type = null) {
64
- if (this.shouldRender(form) && !this.shouldDisable(form)) {
64
+ if (this.shouldRenderSchema(form) && !this.shouldDisable(form)) {
65
65
  if (this.isInlined) {
66
66
  this.sourceComponent.createItem(form, type)
67
67
  } else {
@@ -79,6 +79,7 @@ export default DitoComponent.component('DitoDialog', {
79
79
 
80
80
  schema() {
81
81
  return {
82
+ type: 'dialog',
82
83
  components: this.components
83
84
  }
84
85
  },
@@ -283,6 +283,16 @@ export default DitoComponent.component('DitoForm', {
283
283
  },
284
284
 
285
285
  watch: {
286
+ $route(to, from) {
287
+ // Reload form data when navigating to a different entity in same form.
288
+ if (this.providesData) {
289
+ const { param } = this.meta
290
+ if (param && to.params[param] !== from.params[param]) {
291
+ this.loadData(true)
292
+ }
293
+ }
294
+ },
295
+
286
296
  sourceData: 'clearClonedData',
287
297
  // Needed for the 'create' redirect in `inheritedData()` to work:
288
298
  create: 'setupData'
@@ -14,14 +14,15 @@
14
14
  nestedDataPath,
15
15
  nested,
16
16
  store
17
- } in componentSchemas`
17
+ }, index in componentSchemas`
18
18
  )
19
19
  .dito-break(
20
20
  v-if="schema.break === 'before'"
21
21
  )
22
22
  DitoContainer(
23
- v-if="shouldRender(schema)"
23
+ v-if="shouldRenderSchema(schema)"
24
24
  :key="nestedDataPath"
25
+ v-resize="event => onResize(index, event)"
25
26
  :schema="schema"
26
27
  :dataPath="dataPath"
27
28
  :data="data"
@@ -31,6 +32,8 @@
31
32
  :nested="nested"
32
33
  :disabled="disabled"
33
34
  :generateLabels="generateLabels"
35
+ :firstInRow="schema.break === 'before' || isFirstInRow(index)"
36
+ :lastInRow="schema.break === 'after' || isLastInRow(index)"
34
37
  )
35
38
  .dito-break(
36
39
  v-if="schema.break === 'after'"
@@ -63,6 +66,12 @@ export default DitoComponent.component('DitoPane', {
63
66
  generateLabels: { type: Boolean, default: false }
64
67
  },
65
68
 
69
+ data() {
70
+ return {
71
+ positions: []
72
+ }
73
+ },
74
+
66
75
  computed: {
67
76
  tabComponent() {
68
77
  return this.tab ? this : this.$tabComponent()
@@ -135,9 +144,42 @@ export default DitoComponent.component('DitoPane', {
135
144
  if (this.tab) {
136
145
  this.$router.push({ hash: `#${this.tab}` })
137
146
  }
147
+ },
148
+
149
+ onResize(index, { target }) {
150
+ const { y, width, height } = target.getBoundingClientRect()
151
+ this.positions[index] = width > 0 && height > 0 ? y : null
152
+ },
153
+
154
+ isFirstInRow(index) {
155
+ const { positions } = this
156
+ return (
157
+ positions[index] !== null && (
158
+ index === 0 ||
159
+ (findNextPosition(positions, index, -1, Infinity) < positions[index])
160
+ )
161
+ )
162
+ },
163
+
164
+ isLastInRow(index) {
165
+ const { positions } = this
166
+ return (
167
+ positions[index] !== null && (
168
+ index === positions.length - 1 ||
169
+ findNextPosition(positions, index, +1, 0) > positions[index]
170
+ )
171
+ )
138
172
  }
139
173
  }
140
174
  })
175
+
176
+ function findNextPosition(positions, index, step, fallback) {
177
+ for (let i = index + step; i >= 0 && i < positions.length; i += step) {
178
+ const position = positions[i]
179
+ if (position) return position
180
+ }
181
+ return fallback
182
+ }
141
183
  </script>
142
184
 
143
185
  <style lang="scss">
@@ -1,6 +1,7 @@
1
1
  <template lang="pug">
2
2
  //- Only show panels in tabs when the tabs are also visible.
3
3
  component.dito-panel(
4
+ v-if="shouldRenderSchema(panelSchema)"
4
5
  v-show="visible && (!panelTabComponent || panelTabComponent.visible)"
5
6
  :is="panelTag"
6
7
  @submit.prevent
@@ -164,8 +165,8 @@ export default DitoComponent.component('DitoPanel', {
164
165
  @import '../styles/_imports';
165
166
 
166
167
  .dito-panel {
167
- & + & {
168
- margin-top: $content-padding;
168
+ &:not(:last-child) {
169
+ margin-bottom: $content-padding;
169
170
  }
170
171
 
171
172
  &__header {
@@ -6,7 +6,7 @@
6
6
  v-for="{ schema, dataPath, tabComponent } in panels"
7
7
  )
8
8
  DitoPanel(
9
- v-if="shouldRender(schema)"
9
+ v-if="shouldRenderSchema(schema)"
10
10
  :key="getPanelKey(dataPath, tabComponent)"
11
11
  :schema="schema"
12
12
  :dataPath="dataPath"
@@ -172,7 +172,12 @@ export default DitoComponent.component('DitoRoot', {
172
172
  async (resolve, reject) => {
173
173
  // Process components to resolve async schemas.
174
174
  const routes = []
175
- await processSchemaComponents(this.api, { components }, routes, 0)
175
+ await processSchemaComponents(
176
+ this.api,
177
+ { type: 'dialog', components },
178
+ routes,
179
+ 0
180
+ )
176
181
  if (routes.length > 0) {
177
182
  throw new Error(
178
183
  'Dialogs do not support components that produce routes'
@@ -155,6 +155,7 @@ export default DitoComponent.component('DitoSchema', {
155
155
  ? data(this.context)
156
156
  : data
157
157
  ),
158
+ currentTab: null,
158
159
  componentsRegistry: {},
159
160
  panesRegistry: {},
160
161
  panelsRegistry: {}
@@ -187,17 +188,7 @@ export default DitoComponent.component('DitoSchema', {
187
188
  },
188
189
 
189
190
  selectedTab() {
190
- const currentTab = this.$route.hash?.slice(1) || null
191
- const tab =
192
- currentTab && this.shouldRender(this.tabs[currentTab])
193
- ? currentTab
194
- : this.defaultTab?.name || null
195
- if (tab !== currentTab) {
196
- // TODO: Move this watcher!
197
- // Any tab change needs to be reflected in the router also.
198
- this.$router.replace({ hash: `#${tab}` })
199
- }
200
- return tab
191
+ return this.currentTab || this.defaultTab?.name || null
201
192
  },
202
193
 
203
194
  defaultTab() {
@@ -336,6 +327,32 @@ export default DitoComponent.component('DitoSchema', {
336
327
  }
337
328
  },
338
329
 
330
+ watch: {
331
+ '$route.hash': {
332
+ immediate: true,
333
+ handler(hash) {
334
+ if (this.hasTabs) {
335
+ this.currentTab = hash?.slice(1) || null
336
+ }
337
+ }
338
+ },
339
+
340
+ 'selectedTab'(selectedTab) {
341
+ if (this.hasTabs) {
342
+ let tab = null
343
+ if (selectedTab !== this.currentTab) {
344
+ // Any tab change needs to be reflected in the router also.
345
+ tab = selectedTab
346
+ } else if (!this.shouldRenderSchema(this.tabs[selectedTab])) {
347
+ tab = this.defaultTab?.name
348
+ }
349
+ if (tab) {
350
+ this.$router.replace({ hash: `#${tab}` })
351
+ }
352
+ }
353
+ }
354
+ },
355
+
339
356
  created() {
340
357
  this._register(true)
341
358
  this.setupSchemaFields()
@@ -6,7 +6,7 @@ nav.dito-sidebar.dito-scroll-parent
6
6
  v-for="view in views"
7
7
  )
8
8
  RouterLink(
9
- v-if="shouldRender(view)"
9
+ v-if="shouldRenderSchema(view)"
10
10
  v-slot="{ isActive, href, route }"
11
11
  custom
12
12
  :to="`/${view.path}`"
@@ -5,7 +5,7 @@ thead.dito-table-head
5
5
  v-for="column in columns"
6
6
  )
7
7
  th(
8
- v-if="shouldRender(column)"
8
+ v-if="shouldRenderSchema(column)"
9
9
  :class="getColumnClass(column)"
10
10
  )
11
11
  RouterLink(
@@ -4,7 +4,7 @@
4
4
  v-for="(tabSchema, key) in tabs"
5
5
  )
6
6
  RouterLink.dito-link(
7
- v-if="shouldRender(tabSchema)"
7
+ v-if="shouldRenderSchema(tabSchema)"
8
8
  :key="key"
9
9
  :to="{ hash: `#${key}` }"
10
10
  :class="{ 'dito-active': selectedTab === key }"
@@ -11,7 +11,7 @@ template(
11
11
  :key="name"
12
12
  )
13
13
  .dito-view.dito-scroll-parent(
14
- v-else-if="shouldRender(viewSchema)"
14
+ v-else-if="shouldRenderSchema(viewSchema)"
15
15
  :data-resource="sourceSchema.path"
16
16
  )
17
17
  DitoSchema(
@@ -28,7 +28,10 @@ template(
28
28
  <script>
29
29
  import DitoComponent from '../DitoComponent.js'
30
30
  import RouteMixin from '../mixins/RouteMixin.js'
31
- import { someSchemaComponent, isSingleComponentView } from '../utils/schema.js'
31
+ import {
32
+ isSingleComponentView,
33
+ someNestedSchemaComponent
34
+ } from '../utils/schema.js'
32
35
  import { hasResource } from '../utils/resource.js'
33
36
 
34
37
  // @vue/component
@@ -93,7 +96,7 @@ export default DitoComponent.component('DitoView', {
93
96
  },
94
97
 
95
98
  providesData() {
96
- return someSchemaComponent(this.viewSchema, hasResource)
99
+ return someNestedSchemaComponent(this.viewSchema, hasResource)
97
100
  }
98
101
  },
99
102
 
@@ -0,0 +1,83 @@
1
+ import { asArray } from '@ditojs/utils'
2
+
3
+ export default {
4
+ mounted(node, binding) {
5
+ observeResize(node, binding.value, binding.arg)
6
+ },
7
+
8
+ unmounted(node, binding) {
9
+ unobserveResize(node, binding.value, binding.arg)
10
+ }
11
+ }
12
+
13
+ export function observeResize(node, handler, options) {
14
+ Observer.getObserver(options).observe(node, handler)
15
+ }
16
+
17
+ export function unobserveResize(node, handler, options) {
18
+ Observer.getObserver(options).unobserve(node, handler)
19
+ }
20
+
21
+ export const isResizeSupported = typeof ResizeObserver !== 'undefined'
22
+
23
+ const observers = {}
24
+
25
+ class Observer {
26
+ constructor(key, options) {
27
+ this.key = key
28
+ this.options = options
29
+ this.observer = isResizeSupported
30
+ ? new ResizeObserver(entries => this.handle(entries))
31
+ : null
32
+ this.handlersByNode = new WeakMap()
33
+ this.nodeCount = 0
34
+ }
35
+
36
+ observe(node, handler) {
37
+ let handlers = this.handlersByNode.get(node)
38
+ if (!handlers) {
39
+ handlers = new Set()
40
+ this.handlersByNode.set(node, handlers)
41
+ this.observer?.observe(node, this.options)
42
+ this.nodeCount++
43
+ }
44
+ handlers.add(handler)
45
+ }
46
+
47
+ unobserve(node, handler) {
48
+ const handlers = this.handlersByNode.get(node)
49
+ if (handlers?.delete(handler) && handlers.size === 0) {
50
+ this.handlersByNode.delete(node)
51
+ this.observer?.unobserve(node)
52
+ if (--this.nodeCount === 0) {
53
+ delete observers[this.key]
54
+ }
55
+ }
56
+ }
57
+
58
+ handle(entries) {
59
+ for (const entry of entries) {
60
+ const handlers = this.handlersByNode.get(entry.target)
61
+ if (handlers) {
62
+ const event = {
63
+ target: entry.target,
64
+ contentRect: entry.contentRect,
65
+ // Use `asArray` since Firefox before v92 returns these as objects:
66
+ borderBoxSize: asArray(entry.borderBoxSize),
67
+ contentBoxSize: asArray(entry.contentBoxSize),
68
+ devicePixelContentBoxSize: asArray(entry.devicePixelContentBoxSize)
69
+ }
70
+ for (const handler of handlers) {
71
+ handler(event)
72
+ }
73
+ }
74
+ }
75
+ }
76
+
77
+ static getObserver({ box = 'content-box' } = {}) {
78
+ const options = { box }
79
+ const key = JSON.stringify(options)
80
+ observers[key] ||= new Observer(key, options)
81
+ return observers[key]
82
+ }
83
+ }
@@ -1,11 +1,8 @@
1
1
  import {
2
2
  isObject,
3
- isArray,
4
3
  isString,
5
4
  isFunction,
6
- asArray,
7
5
  equals,
8
- getValueAtDataPath,
9
6
  labelize,
10
7
  hyphenate,
11
8
  format
@@ -13,7 +10,7 @@ import {
13
10
  import appState from '../appState.js'
14
11
  import DitoContext from '../DitoContext.js'
15
12
  import EmitterMixin from './EmitterMixin.js'
16
- import { isMatchingType, convertType } from '../utils/type.js'
13
+ import { getSchemaValue, shouldRenderSchema } from '../utils/schema.js'
17
14
  import { getResource, getMemberResource } from '../utils/resource.js'
18
15
  import { computed, reactive } from 'vue'
19
16
 
@@ -201,43 +198,15 @@ export default {
201
198
 
202
199
  getSchemaValue(
203
200
  keyOrDataPath,
204
- { type, default: def, schema = this.schema, callback = true } = {}
201
+ { type, schema = this.schema, callback = true, default: def } = {}
205
202
  ) {
206
- const types = type && asArray(type)
207
- // For performance reasons, data-paths in `keyOrDataPath` can only be
208
- // provided in in array format here:
209
- let value = schema
210
- ? isArray(keyOrDataPath)
211
- ? getValueAtDataPath(schema, keyOrDataPath, () => undefined)
212
- : schema[keyOrDataPath]
213
- : undefined
214
-
215
- if (value === undefined && def !== undefined) {
216
- if (callback && isFunction(def) && !isMatchingType(types, def)) {
217
- // Support `default()` functions for any type except `Function`:
218
- def = def.call(this)
219
- }
220
- return def
221
- }
222
-
223
- if (isMatchingType(types, value)) {
224
- return value
225
- }
226
- // Any schema value handled through `getSchemaValue()` can provide
227
- // a function that's resolved when the value is evaluated:
228
- if (callback && isFunction(value)) {
229
- value = value(this.context)
230
- }
231
- // Now finally see if we can convert to the expect types.
232
- if (types && value != null && !isMatchingType(types, value)) {
233
- for (const type of types) {
234
- const converted = convertType(type, value)
235
- if (converted !== value) {
236
- return converted
237
- }
238
- }
239
- }
240
- return value
203
+ return getSchemaValue(keyOrDataPath, {
204
+ type,
205
+ schema,
206
+ callback,
207
+ context: this.context,
208
+ default: isFunction(def) ? () => def.call(this) : def
209
+ })
241
210
  },
242
211
 
243
212
  getLabel(schema, name) {
@@ -263,12 +232,8 @@ export default {
263
232
  }
264
233
  },
265
234
 
266
- shouldRender(schema = null) {
267
- return this.getSchemaValue('if', {
268
- type: Boolean,
269
- default: true,
270
- schema
271
- })
235
+ shouldRenderSchema(schema = null) {
236
+ return shouldRenderSchema(schema, this.context)
272
237
  },
273
238
 
274
239
  shouldShow(schema = null) {
@@ -314,7 +314,7 @@ export default {
314
314
 
315
315
  watch: {
316
316
  $route(to, from) {
317
- if (from.path === to.path && from.hash === to.hash) {
317
+ if (this.providesData && from.path === to.path && from.hash === to.hash) {
318
318
  // Paths and hashes remain the same, so only queries have changed.
319
319
  // Update filter and reload data without clearing.
320
320
  this.query = to.query
@@ -57,7 +57,7 @@
57
57
  v-for="column in columns"
58
58
  )
59
59
  DitoTableCell(
60
- v-if="shouldRender(column)"
60
+ v-if="shouldRenderSchema(column)"
61
61
  :key="column.name"
62
62
  :class="getCellClass(column)"
63
63
  :cell="column"