@ditojs/admin 2.25.0 → 2.26.1

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.25.0",
3
+ "version": "2.26.1",
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",
@@ -33,57 +33,57 @@
33
33
  "not ie_mob > 0"
34
34
  ],
35
35
  "dependencies": {
36
- "@ditojs/ui": "^2.25.0",
37
- "@ditojs/utils": "^2.25.0",
38
- "@kyvg/vue3-notification": "^3.1.4",
36
+ "@ditojs/ui": "^2.26.1",
37
+ "@ditojs/utils": "^2.26.1",
38
+ "@kyvg/vue3-notification": "^3.2.1",
39
39
  "@lk77/vue3-color": "^3.0.6",
40
- "@tiptap/core": "^2.2.2",
41
- "@tiptap/extension-blockquote": "^2.2.2",
42
- "@tiptap/extension-bold": "^2.2.2",
43
- "@tiptap/extension-bullet-list": "^2.2.2",
44
- "@tiptap/extension-code": "^2.2.2",
45
- "@tiptap/extension-code-block": "^2.2.2",
46
- "@tiptap/extension-document": "^2.2.2",
47
- "@tiptap/extension-hard-break": "^2.2.2",
48
- "@tiptap/extension-heading": "^2.2.2",
49
- "@tiptap/extension-history": "^2.2.2",
50
- "@tiptap/extension-horizontal-rule": "^2.2.2",
51
- "@tiptap/extension-italic": "^2.2.2",
52
- "@tiptap/extension-link": "^2.2.2",
53
- "@tiptap/extension-list-item": "^2.2.2",
54
- "@tiptap/extension-ordered-list": "^2.2.2",
55
- "@tiptap/extension-paragraph": "^2.2.2",
56
- "@tiptap/extension-strike": "^2.2.2",
57
- "@tiptap/extension-text": "^2.2.2",
58
- "@tiptap/extension-underline": "^2.2.2",
59
- "@tiptap/pm": "^2.2.2",
60
- "@tiptap/vue-3": "^2.2.2",
61
- "@vueuse/integrations": "^10.7.2",
40
+ "@tiptap/core": "^2.3.0",
41
+ "@tiptap/extension-blockquote": "^2.3.0",
42
+ "@tiptap/extension-bold": "^2.3.0",
43
+ "@tiptap/extension-bullet-list": "^2.3.0",
44
+ "@tiptap/extension-code": "^2.3.0",
45
+ "@tiptap/extension-code-block": "^2.3.0",
46
+ "@tiptap/extension-document": "^2.3.0",
47
+ "@tiptap/extension-hard-break": "^2.3.0",
48
+ "@tiptap/extension-heading": "^2.3.0",
49
+ "@tiptap/extension-history": "^2.3.0",
50
+ "@tiptap/extension-horizontal-rule": "^2.3.0",
51
+ "@tiptap/extension-italic": "^2.3.0",
52
+ "@tiptap/extension-link": "^2.3.0",
53
+ "@tiptap/extension-list-item": "^2.3.0",
54
+ "@tiptap/extension-ordered-list": "^2.3.0",
55
+ "@tiptap/extension-paragraph": "^2.3.0",
56
+ "@tiptap/extension-strike": "^2.3.0",
57
+ "@tiptap/extension-text": "^2.3.0",
58
+ "@tiptap/extension-underline": "^2.3.0",
59
+ "@tiptap/pm": "^2.3.0",
60
+ "@tiptap/vue-3": "^2.3.0",
61
+ "@vueuse/integrations": "^10.9.0",
62
62
  "codeflask": "^1.4.1",
63
- "filesize": "^10.1.0",
63
+ "filesize": "^10.1.1",
64
64
  "filesize-parser": "^1.5.0",
65
65
  "focus-trap": "^7.5.4",
66
- "nanoid": "^5.0.5",
66
+ "nanoid": "^5.0.7",
67
67
  "sortablejs": "^1.15.2",
68
68
  "tinycolor2": "^1.6.0",
69
69
  "tippy.js": "^6.3.7",
70
- "type-fest": "^4.10.2",
70
+ "type-fest": "^4.15.0",
71
71
  "vue": "3.4.10",
72
- "vue-multiselect": "^3.0.0-beta.3",
73
- "vue-router": "^4.2.5",
74
- "vue-upload-component": "^3.1.8"
72
+ "vue-multiselect": "^3.0.0",
73
+ "vue-router": "^4.3.0",
74
+ "vue-upload-component": "^3.1.15"
75
75
  },
76
76
  "devDependencies": {
77
- "@ditojs/build": "^2.25.0",
77
+ "@ditojs/build": "^2.26.1",
78
78
  "@vitejs/plugin-vue": "^5.0.4",
79
- "@vue/compiler-sfc": "3.4.10",
79
+ "@vue/compiler-sfc": "3.4.21",
80
80
  "pug": "^3.0.2",
81
- "sass": "1.70.0",
82
- "typescript": "^5.3.3",
83
- "vite": "^5.1.1"
81
+ "sass": "1.75.0",
82
+ "typescript": "^5.4.5",
83
+ "vite": "^5.2.8"
84
84
  },
85
85
  "types": "types",
86
- "gitHead": "888c84b30ad53c2a44ce50174f7ffb851454cec2",
86
+ "gitHead": "8f2e07e3774d0e03b24ee78cffe60c5edacb93ae",
87
87
  "scripts": {
88
88
  "build": "vite build",
89
89
  "watch": "yarn build --mode 'development' --watch",
@@ -61,6 +61,12 @@ export default class DitoContext {
61
61
  : new DitoContext(component, context)
62
62
  }
63
63
 
64
+ extend(object) {
65
+ // Create a copy of this context that inherits from the real one, but
66
+ // overrides some properties with the ones from the passed `object`.
67
+ return Object.setPrototypeOf(object, this)
68
+ }
69
+
64
70
  // `nested` is `true` when the data-path points a value inside an item, and
65
71
  // `false` when it points to the item itself.
66
72
  get nested() {
@@ -1,66 +1,82 @@
1
1
  <template lang="pug">
2
+ DefineTemplate
3
+ //- Prevent implicit submission of the form, for example when typing enter
4
+ //- in an input field.
5
+ //- https://stackoverflow.com/a/51507806
6
+ button(
7
+ v-show="false"
8
+ type="submit"
9
+ disabled
10
+ )
11
+ DitoSchema(
12
+ :schema="schema"
13
+ :dataPath="dataPath"
14
+ :data="data"
15
+ :meta="meta"
16
+ :store="store"
17
+ :padding="isInlinedSource ? 'nested' : 'root'"
18
+ :disabled="isLoading"
19
+ :scrollable="!isInlinedSource"
20
+ generateLabels
21
+ )
22
+ template(#buttons)
23
+ DitoButtons.dito-buttons-round.dito-buttons-large.dito-buttons-main(
24
+ :class="{ 'dito-buttons-sticky': !isInlinedSource }"
25
+ :buttons="buttonSchemas"
26
+ :dataPath="dataPath"
27
+ :data="data"
28
+ :meta="meta"
29
+ :store="store"
30
+ :disabled="isLoading"
31
+ )
32
+
2
33
  .dito-form.dito-scroll-parent(
3
- :class="{ 'dito-form-nested': isNestedRoute }"
34
+ :class="{ 'dito-form-inlined': isInlinedSource }"
4
35
  :data-resource="sourceSchema.path"
5
36
  )
6
- //- NOTE: Nested form components are kept alive by using `v-show` instead of
7
- //- `v-if` here, so event handling and other things still work with nested
8
- //- editing. Only render a router-view here if this isn't the last data route
9
- //- and not a nested form route, which will appear elsewhere in its own view.
10
- RouterView(
11
- v-if="!(isLastUnnestedRoute || isNestedRoute)"
12
- v-show="!isActive"
37
+ template(
38
+ v-if="isInlinedSource"
13
39
  )
14
- //- Use a <div> for inlined forms, as we shouldn't nest actual <form> tags.
15
- component(
16
- v-show="isActive"
17
- :is="isNestedRoute ? 'div' : 'form'"
18
- :class="{ 'dito-scroll-parent': isRootForm }"
19
- @submit.prevent
40
+ //- Use a <div> for inlined forms, as we shouldn't nest actual <form> tags.
41
+ //- NOTE: inlined form components are kept alive by using `v-show` instead
42
+ //- of `v-if` here, so event handling and other things still work with
43
+ //- inlined editing.
44
+ div(
45
+ v-show="isActive"
46
+ )
47
+ ReuseTemplate
48
+ template(
49
+ v-else
20
50
  )
21
- //- Prevent implicit submission of the form, for example when typing enter
22
- //- in an input field.
23
- //- https://stackoverflow.com/a/51507806
24
- button(
25
- v-show="false"
26
- type="submit"
27
- disabled
51
+ //- Only render a router-view here if this isn't the last data route and not
52
+ //- an inlined form route, which will appear elsewhere in its own view.
53
+ RouterView(
54
+ v-if="!isLastUnnestedRoute"
55
+ v-show="!isActive"
28
56
  )
29
- DitoSchema(
30
- :schema="schema"
31
- :dataPath="dataPath"
32
- :data="data"
33
- :meta="meta"
34
- :store="store"
35
- :padding="isNestedRoute ? 'nested' : 'root'"
36
- :disabled="isLoading"
37
- :scrollable="isRootForm"
38
- generateLabels
57
+ form.dito-scroll-parent(
58
+ v-show="isActive"
59
+ @submit.prevent
39
60
  )
40
- template(#buttons)
41
- DitoButtons.dito-buttons-round.dito-buttons-large.dito-buttons-main(
42
- :class="{ 'dito-buttons-sticky': isRootForm }"
43
- :buttons="buttonSchemas"
44
- :dataPath="dataPath"
45
- :data="data"
46
- :meta="meta"
47
- :store="store"
48
- :disabled="isLoading"
49
- )
61
+ ReuseTemplate
50
62
  </template>
51
63
 
52
64
  <script>
65
+ import { createReusableTemplate } from '@vueuse/core'
53
66
  import { clone, capitalize, parseDataPath, assignDeeply } from '@ditojs/utils'
54
67
  import DitoComponent from '../DitoComponent.js'
55
68
  import RouteMixin from '../mixins/RouteMixin.js'
56
69
  import ResourceMixin from '../mixins/ResourceMixin.js'
57
70
  import { getResource, getMemberResource } from '../utils/resource.js'
58
- import { getButtonSchemas, isObjectSource } from '../utils/schema.js'
71
+ import { getButtonSchemas, isInlined, isObjectSource } from '../utils/schema.js'
59
72
  import { resolvePath } from '../utils/path.js'
60
73
 
74
+ const [DefineTemplate, ReuseTemplate] = createReusableTemplate()
75
+
61
76
  // @vue/component
62
77
  export default DitoComponent.component('DitoForm', {
63
78
  mixins: [RouteMixin, ResourceMixin],
79
+ components: { DefineTemplate, ReuseTemplate },
64
80
 
65
81
  data() {
66
82
  return {
@@ -88,15 +104,18 @@ export default DitoComponent.component('DitoForm', {
88
104
  },
89
105
 
90
106
  schema() {
91
- return (
92
- this.getItemFormSchema(
93
- this.sourceSchema,
94
- // If there is no data yet, provide an empty object with just the
95
- // right type set, so the form can always be determined.
96
- this.data || { type: this.type },
97
- this.context
98
- ) || {}
99
- ) // Always return a schema object so we don't need to check for it.
107
+ return this.getItemFormSchema(
108
+ this.sourceSchema,
109
+ this.data || (
110
+ this.creationType
111
+ ? // If there is no data yet but the type to create a new item is
112
+ // is specified, provide a temporary empty object with just the
113
+ // type set, so `getItemFormSchema()` can determine the form.
114
+ { type: this.creationType }
115
+ : null
116
+ ),
117
+ this.context
118
+ )
100
119
  },
101
120
 
102
121
  buttonSchemas() {
@@ -124,8 +143,8 @@ export default DitoComponent.component('DitoForm', {
124
143
  )
125
144
  },
126
145
 
127
- isRootForm() {
128
- return this.dataPath === '' && !this.isNestedRoute
146
+ isInlinedSource() {
147
+ return isInlined(this.sourceSchema)
129
148
  },
130
149
 
131
150
  isActive() {
@@ -156,7 +175,7 @@ export default DitoComponent.component('DitoForm', {
156
175
  return this.mainSchemaComponent?.selectedTab || null
157
176
  },
158
177
 
159
- type() {
178
+ creationType() {
160
179
  // The type of form to create, if there are multiple forms to choose from.
161
180
  return this.$route.query.type
162
181
  },
@@ -235,7 +254,8 @@ export default DitoComponent.component('DitoForm', {
235
254
  // parent data so we can replace the entry at `sourceKey` on it.
236
255
  if (i === l - 1) {
237
256
  // TODO: Fix side-effects
238
- // eslint-disable-next-line
257
+ // eslint-disable-next-line max-len
258
+ // eslint-disable-next-line vue/no-side-effects-in-computed-properties
239
259
  this.sourceKey = key
240
260
  } else {
241
261
  data = data[key]
@@ -330,7 +350,7 @@ export default DitoComponent.component('DitoForm', {
330
350
  // @override ResourceMixin.setupData()
331
351
  setupData() {
332
352
  if (this.isCreating) {
333
- this.createdData ||= this.createData(this.schema, this.type)
353
+ this.createdData ||= this.createData(this.schema, this.creationType)
334
354
  } else {
335
355
  this.ensureData()
336
356
  }
@@ -3,14 +3,14 @@ import DitoComponent from '../DitoComponent.js'
3
3
  import DitoForm from './DitoForm.vue'
4
4
 
5
5
  // @vue/component
6
- export default DitoComponent.component('DitoFormNested', {
6
+ export default DitoComponent.component('DitoFormInlined', {
7
7
  extends: DitoForm
8
8
  })
9
9
  </script>
10
10
 
11
11
  <style lang="scss">
12
- .dito-form-nested {
13
- // No scrolling inside nested forms, and prevent open .multiselect from
12
+ .dito-form-inlined {
13
+ // No scrolling in inlined forms, and prevent open .multiselect from
14
14
  // being cropped.
15
15
  overflow: visible;
16
16
  }
@@ -17,7 +17,7 @@
17
17
  // -Use <span> for .dito-break so we can use `.dito-container:first-of-type`
18
18
  // selector.
19
19
  span.dito-break(
20
- v-if="schema.break === 'before'"
20
+ v-if="['before', 'both'].includes(schema.break)"
21
21
  )
22
22
  DitoContainer(
23
23
  v-if="shouldRenderSchema(schema)"
@@ -37,7 +37,7 @@
37
37
  :accumulatedBasis="accumulatedBasis"
38
38
  )
39
39
  span.dito-break(
40
- v-if="schema.break === 'after'"
40
+ v-if="['after', 'both'].includes(schema.break)"
41
41
  )
42
42
  </template>
43
43
 
@@ -118,7 +118,8 @@ import {
118
118
  getNamedSchemas,
119
119
  getPanelEntries,
120
120
  setDefaultValues,
121
- processData
121
+ processData,
122
+ isEmptySchema
122
123
  } from '../utils/schema.js'
123
124
  import { getSchemaAccessor, getStoreAccessor } from '../utils/accessor.js'
124
125
 
@@ -233,11 +234,15 @@ export default DitoComponent.component('DitoSchema', {
233
234
  },
234
235
 
235
236
  processedData() {
237
+ // TODO: Fix side-effects
238
+ // eslint-disable-next-line vue/no-side-effects-in-computed-properties
236
239
  return this.processData({ target: 'server', schemaOnly: true })
237
240
  },
238
241
 
239
242
  clipboardData: {
240
243
  get() {
244
+ // TODO: Fix side-effects
245
+ // eslint-disable-next-line vue/no-side-effects-in-computed-properties
241
246
  return this.processData({ target: 'clipboard', schemaOnly: true })
242
247
  },
243
248
 
@@ -361,6 +366,17 @@ export default DitoComponent.component('DitoSchema', {
361
366
  },
362
367
 
363
368
  watch: {
369
+ schema: {
370
+ immediate: true,
371
+ handler(schema) {
372
+ // For forms with type depending on loaded data, we need to wait for the
373
+ // actual schema to become ready before setting up schema related things
374
+ if (!isEmptySchema(schema)) {
375
+ this.setupSchema()
376
+ }
377
+ }
378
+ },
379
+
364
380
  routeTab: {
365
381
  immediate: true,
366
382
  // https://github.com/vuejs/vue-router/issues/3393#issuecomment-1158470149
@@ -397,10 +413,6 @@ export default DitoComponent.component('DitoSchema', {
397
413
 
398
414
  created() {
399
415
  this._register(true)
400
- this.setupSchemaFields()
401
- // Delegate change events through to parent schema:
402
- this.delegate('change', this.parentSchemaComponent)
403
- this.emitEvent('initialize') // Not `'create'`, since that's for data.
404
416
  if (this.scrollable && this.wide) {
405
417
  this.appState.pageClass = 'dito-page--wide'
406
418
  }
@@ -419,6 +431,13 @@ export default DitoComponent.component('DitoSchema', {
419
431
  },
420
432
 
421
433
  methods: {
434
+ setupSchema() {
435
+ this.setupSchemaFields()
436
+ // Delegate change events through to parent schema:
437
+ this.delegate('change', this.parentSchemaComponent)
438
+ this.emitEvent('initialize') // Not `'create'`, since that's for data.
439
+ },
440
+
422
441
  getComponentsByDataPath(dataPath) {
423
442
  return this._getEntriesByDataPath(this.componentsByDataPath, dataPath)
424
443
  },
@@ -27,7 +27,7 @@ export { default as DitoCreateButton } from './DitoCreateButton.vue'
27
27
  export { default as DitoClipboard } from './DitoClipboard.vue'
28
28
  export { default as DitoView } from './DitoView.vue'
29
29
  export { default as DitoForm } from './DitoForm.vue'
30
- export { default as DitoFormNested } from './DitoFormNested.vue'
30
+ export { default as DitoFormInlined } from './DitoFormInlined.vue'
31
31
  export { default as DitoErrors } from './DitoErrors.vue'
32
32
  export { default as DitoScopes } from './DitoScopes.vue'
33
33
  export { default as DitoPagination } from './DitoPagination.vue'
@@ -534,30 +534,36 @@ export default {
534
534
  }
535
535
  },
536
536
 
537
- async emitEvent(event, {
537
+ emitEvent(event, {
538
538
  context = null,
539
539
  parent = null
540
540
  } = {}) {
541
541
  const hasListeners = this.hasListeners(event)
542
542
  const parentHasListeners = parent?.hasListeners(event)
543
543
  if (hasListeners || parentHasListeners) {
544
- // The effects of some events need some time to propagate through Vue.
545
- // Use $nextTick() to make sure our handlers see these changes.
546
- // For example, `processedItem` is only correct after components that
547
- // are newly rendered due to data changes have registered themselves.
548
- if (['load', 'change'].includes(event)) {
549
- await this.$nextTick()
550
- }
551
-
552
- const getContext = () => (context = DitoContext.get(this, context))
553
- const res = hasListeners
554
- ? await this.emit(event, getContext())
555
- : undefined
556
- // Don't bubble to parent if handled event returned `false`
557
- if (parentHasListeners && res !== false) {
558
- parent.emit(event, getContext())
559
- }
560
- return res
544
+ const emitEvent = target =>
545
+ target.emit(event, (context = DitoContext.get(this, context)))
546
+
547
+ const handleParentListeners = result =>
548
+ // Don't bubble to parent if handled event returned `false`
549
+ parentHasListeners && result !== false
550
+ ? emitEvent(parent).then(() => result)
551
+ : result
552
+
553
+ const handleListeners = () =>
554
+ hasListeners
555
+ ? emitEvent(this).then(handleParentListeners)
556
+ : handleParentListeners(undefined)
557
+
558
+ return ['load', 'change'].includes(event)
559
+ ? // The effects of some events need time to propagate through Vue.
560
+ // Use $nextTick() to make sure our handlers see these changes.
561
+ // For example, `processedItem` is only correct after components
562
+ // that are newly rendered due to data changes have registered.
563
+ // NOTE: The result of `handleListeners()` makes it through the
564
+ // `$nextTick()` call and will be returned as expected.
565
+ this.$nextTick(handleListeners)
566
+ : handleListeners()
561
567
  }
562
568
  },
563
569
 
@@ -118,6 +118,8 @@ export default {
118
118
  }
119
119
  })
120
120
  }
121
+ // Make sure it's thenable even if there are no listeners.
122
+ return Promise.resolve()
121
123
  },
122
124
 
123
125
  // Checks if the component has listeners for a given event type:
@@ -186,6 +186,9 @@ export default {
186
186
  // we're currently editing.
187
187
  for (const option of options) {
188
188
  if (!('id' in option)) {
189
+ // TODO: Fix side-effects
190
+ // eslint-disable-next-line max-len
191
+ // eslint-disable-next-line vue/no-side-effects-in-computed-properties
189
192
  setTemporaryId(option, 'id')
190
193
  }
191
194
  }
@@ -203,7 +206,7 @@ export default {
203
206
  }
204
207
  results.push(entry)
205
208
  }
206
- entry.options.push(option)
209
+ entry[this.groupByOptions].push(option)
207
210
  return results
208
211
  },
209
212
  []
@@ -60,10 +60,6 @@ export default {
60
60
  return false
61
61
  },
62
62
 
63
- isNestedRoute() {
64
- return this.meta.nested
65
- },
66
-
67
63
  isView() {
68
64
  return false
69
65
  },
@@ -617,7 +617,7 @@ export default {
617
617
  isListSource(schema) ? param : null
618
618
  ),
619
619
  component: DitoComponent.component(
620
- nested ? 'DitoFormNested' : 'DitoForm'
620
+ nested ? 'DitoFormInlined' : 'DitoForm'
621
621
  ),
622
622
  meta: formMeta
623
623
  }
@@ -219,6 +219,6 @@ table.dito-table {
219
219
  }
220
220
 
221
221
  table.dito-table table.dito-table,
222
- .dito-form-nested table.dito-table {
222
+ .dito-form-inlined table.dito-table {
223
223
  margin: 0;
224
224
  }
@@ -87,7 +87,8 @@ export default DitoTypeComponent.register('color', {
87
87
  const color = tinycolor(this.value)
88
88
  if (color.isValid()) {
89
89
  // TODO: Fix side-effects
90
- // eslint-disable-next-line
90
+ // eslint-disable-next-line max-len
91
+ // eslint-disable-next-line vue/no-side-effects-in-computed-properties
91
92
  this.convertedHexValue = color
92
93
  .toString(color.getAlpha() < 1 ? 'hex8' : 'hex6')
93
94
  .slice(1)
@@ -33,7 +33,8 @@ export default DitoTypeComponent.register(
33
33
  if (schema.data || schema.dataPath) {
34
34
  const value = this.handleDataSchema(schema, 'schema')
35
35
  // TODO: Fix side-effects
36
- // eslint-disable-next-line
36
+ // eslint-disable-next-line max-len
37
+ // eslint-disable-next-line vue/no-side-effects-in-computed-properties
37
38
  this.data[this.name] = value
38
39
  }
39
40
  return TypeMixin.computed.value.get.call(this)