@ditojs/admin 2.26.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.26.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,31 +33,31 @@
33
33
  "not ie_mob > 0"
34
34
  ],
35
35
  "dependencies": {
36
- "@ditojs/ui": "^2.26.0",
37
- "@ditojs/utils": "^2.26.0",
36
+ "@ditojs/ui": "^2.26.1",
37
+ "@ditojs/utils": "^2.26.1",
38
38
  "@kyvg/vue3-notification": "^3.2.1",
39
39
  "@lk77/vue3-color": "^3.0.6",
40
- "@tiptap/core": "^2.2.6",
41
- "@tiptap/extension-blockquote": "^2.2.6",
42
- "@tiptap/extension-bold": "^2.2.6",
43
- "@tiptap/extension-bullet-list": "^2.2.6",
44
- "@tiptap/extension-code": "^2.2.6",
45
- "@tiptap/extension-code-block": "^2.2.6",
46
- "@tiptap/extension-document": "^2.2.6",
47
- "@tiptap/extension-hard-break": "^2.2.6",
48
- "@tiptap/extension-heading": "^2.2.6",
49
- "@tiptap/extension-history": "^2.2.6",
50
- "@tiptap/extension-horizontal-rule": "^2.2.6",
51
- "@tiptap/extension-italic": "^2.2.6",
52
- "@tiptap/extension-link": "^2.2.6",
53
- "@tiptap/extension-list-item": "^2.2.6",
54
- "@tiptap/extension-ordered-list": "^2.2.6",
55
- "@tiptap/extension-paragraph": "^2.2.6",
56
- "@tiptap/extension-strike": "^2.2.6",
57
- "@tiptap/extension-text": "^2.2.6",
58
- "@tiptap/extension-underline": "^2.2.6",
59
- "@tiptap/pm": "^2.2.6",
60
- "@tiptap/vue-3": "^2.2.6",
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
61
  "@vueuse/integrations": "^10.9.0",
62
62
  "codeflask": "^1.4.1",
63
63
  "filesize": "^10.1.1",
@@ -69,21 +69,21 @@
69
69
  "tippy.js": "^6.3.7",
70
70
  "type-fest": "^4.15.0",
71
71
  "vue": "3.4.10",
72
- "vue-multiselect": "^3.0.0-beta.3",
72
+ "vue-multiselect": "^3.0.0",
73
73
  "vue-router": "^4.3.0",
74
74
  "vue-upload-component": "^3.1.15"
75
75
  },
76
76
  "devDependencies": {
77
- "@ditojs/build": "^2.26.0",
77
+ "@ditojs/build": "^2.26.1",
78
78
  "@vitejs/plugin-vue": "^5.0.4",
79
79
  "@vue/compiler-sfc": "3.4.21",
80
80
  "pug": "^3.0.2",
81
- "sass": "1.74.1",
82
- "typescript": "^5.4.4",
81
+ "sass": "1.75.0",
82
+ "typescript": "^5.4.5",
83
83
  "vite": "^5.2.8"
84
84
  },
85
85
  "types": "types",
86
- "gitHead": "da47b5b8b9fca80193a7e93344bdc14cdaada329",
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 {
@@ -127,8 +143,8 @@ export default DitoComponent.component('DitoForm', {
127
143
  )
128
144
  },
129
145
 
130
- isRootForm() {
131
- return this.dataPath === '' && !this.isNestedRoute
146
+ isInlinedSource() {
147
+ return isInlined(this.sourceSchema)
132
148
  },
133
149
 
134
150
  isActive() {
@@ -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
  }
@@ -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:
@@ -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
  }
@@ -122,7 +122,12 @@ export function getSchemaIdentifier(schema) {
122
122
  return JSON.stringify(schema)
123
123
  }
124
124
 
125
- export async function resolveSchema(schema, unwrapModule = false) {
125
+ const resolvedSchemas = new WeakMap()
126
+ export async function resolveSchema(value, unwrapModule = false) {
127
+ if (resolvedSchemas.has(value)) {
128
+ return resolvedSchemas.get(value)
129
+ }
130
+ let schema = value
126
131
  if (isFunction(schema)) {
127
132
  schema = schema()
128
133
  }
@@ -147,6 +152,7 @@ export async function resolveSchema(schema, unwrapModule = false) {
147
152
  }
148
153
  }
149
154
  }
155
+ resolvedSchemas.set(value, schema)
150
156
  return schema
151
157
  }
152
158
 
@@ -236,29 +242,34 @@ export async function resolveSchemaComponents(schemas) {
236
242
  await mapConcurrently(Object.values(schemas || {}), resolveSchemaComponent)
237
243
  }
238
244
 
245
+ const processedSchemas = new WeakSet()
239
246
  export async function processSchemaComponents(
240
247
  api,
241
248
  schema,
242
249
  routes = null,
243
250
  level = 0
244
251
  ) {
245
- const promises = []
246
- const process = (component, name, relativeLevel) => {
247
- promises.push(
248
- processSchemaComponent(
249
- api,
250
- component,
251
- name,
252
- routes,
253
- level + relativeLevel
252
+ if (schema && !processedSchemas.has(schema)) {
253
+ processedSchemas.add(schema)
254
+
255
+ const promises = []
256
+ const process = (component, name, relativeLevel) => {
257
+ promises.push(
258
+ processSchemaComponent(
259
+ api,
260
+ component,
261
+ name,
262
+ routes,
263
+ level + relativeLevel
264
+ )
254
265
  )
255
- )
256
- }
266
+ }
257
267
 
258
- iterateNestedSchemaComponents(schema, process)
259
- iterateSchemaComponents(getPanelSchemas(schema), process)
268
+ iterateNestedSchemaComponents(schema, process)
269
+ iterateSchemaComponents(getPanelSchemas(schema), process)
260
270
 
261
- await Promise.all(promises)
271
+ await Promise.all(promises)
272
+ }
262
273
  }
263
274
 
264
275
  export async function processSchemaComponent(