@ditojs/admin 2.26.1 → 2.27.0

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.1",
3
+ "version": "2.27.0",
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,8 +33,8 @@
33
33
  "not ie_mob > 0"
34
34
  ],
35
35
  "dependencies": {
36
- "@ditojs/ui": "^2.26.1",
37
- "@ditojs/utils": "^2.26.1",
36
+ "@ditojs/ui": "^2.27.0",
37
+ "@ditojs/utils": "^2.27.0",
38
38
  "@kyvg/vue3-notification": "^3.2.1",
39
39
  "@lk77/vue3-color": "^3.0.6",
40
40
  "@tiptap/core": "^2.3.0",
@@ -74,7 +74,7 @@
74
74
  "vue-upload-component": "^3.1.15"
75
75
  },
76
76
  "devDependencies": {
77
- "@ditojs/build": "^2.26.1",
77
+ "@ditojs/build": "^2.27.0",
78
78
  "@vitejs/plugin-vue": "^5.0.4",
79
79
  "@vue/compiler-sfc": "3.4.21",
80
80
  "pug": "^3.0.2",
@@ -83,7 +83,7 @@
83
83
  "vite": "^5.2.8"
84
84
  },
85
85
  "types": "types",
86
- "gitHead": "8f2e07e3774d0e03b24ee78cffe60c5edacb93ae",
86
+ "gitHead": "e80d56a1887faa60f61f4c642502687d50ead333",
87
87
  "scripts": {
88
88
  "build": "vite build",
89
89
  "watch": "yarn build --mode 'development' --watch",
package/src/DitoAdmin.js CHANGED
@@ -124,9 +124,7 @@ export default class DitoAdmin {
124
124
  },
125
125
 
126
126
  member(resource) {
127
- // NOTE: We assume that all members have root-level collection routes,
128
- // to avoid excessive nesting of (sub-)collection routes.
129
- return `${resource.path}/${resource.id}`
127
+ return `${this.default(resource)}/${resource.id}`
130
128
  },
131
129
 
132
130
  upload(resource) {
@@ -1,82 +1,65 @@
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
-
33
2
  .dito-form.dito-scroll-parent(
34
- :class="{ 'dito-form-inlined': isInlinedSource }"
3
+ :class="{ 'dito-form-nested': isNestedRoute }"
35
4
  :data-resource="sourceSchema.path"
36
5
  )
37
- template(
38
- v-if="isInlinedSource"
6
+ //- Only render a router-view here if this isn't the last data route and not a
7
+ //- nested form route, which will appear elsewhere in its own view.
8
+ RouterView(
9
+ v-if="!isLastUnnestedRoute && !isNestedRoute"
10
+ v-show="!isActiveRoute"
39
11
  )
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
12
+ //- NOTE: Nested form components are kept alive by using `v-show` instead of
13
+ //- `v-if` here, so event handling and other things still work with nested
14
+ //- editing.
15
+ DitoFormInner(
16
+ v-show="isActiveRoute"
17
+ :nested="isNestedRoute"
50
18
  )
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"
19
+ //- Prevent implicit submission of the form, for example when typing enter
20
+ //- in an input field.
21
+ //- https://stackoverflow.com/a/51507806
22
+ button(
23
+ v-show="false"
24
+ type="submit"
25
+ disabled
56
26
  )
57
- form.dito-scroll-parent(
58
- v-show="isActive"
59
- @submit.prevent
27
+ DitoSchema(
28
+ :schema="schema"
29
+ :dataPath="dataPath"
30
+ :data="data"
31
+ :meta="meta"
32
+ :store="store"
33
+ :padding="isNestedRoute ? 'nested' : 'root'"
34
+ :active="isActiveRoute"
35
+ :disabled="isLoading"
36
+ :scrollable="!isNestedRoute"
37
+ generateLabels
60
38
  )
61
- ReuseTemplate
39
+ template(#buttons)
40
+ DitoButtons.dito-buttons-round.dito-buttons-large.dito-buttons-main(
41
+ :class="{ 'dito-buttons-sticky': !isNestedRoute }"
42
+ :buttons="buttonSchemas"
43
+ :dataPath="dataPath"
44
+ :data="data"
45
+ :meta="meta"
46
+ :store="store"
47
+ :disabled="isLoading"
48
+ )
62
49
  </template>
63
50
 
64
51
  <script>
65
- import { createReusableTemplate } from '@vueuse/core'
66
52
  import { clone, capitalize, parseDataPath, assignDeeply } from '@ditojs/utils'
67
53
  import DitoComponent from '../DitoComponent.js'
68
54
  import RouteMixin from '../mixins/RouteMixin.js'
69
55
  import ResourceMixin from '../mixins/ResourceMixin.js'
70
56
  import { getResource, getMemberResource } from '../utils/resource.js'
71
- import { getButtonSchemas, isInlined, isObjectSource } from '../utils/schema.js'
57
+ import { getButtonSchemas, isObjectSource } from '../utils/schema.js'
72
58
  import { resolvePath } from '../utils/path.js'
73
59
 
74
- const [DefineTemplate, ReuseTemplate] = createReusableTemplate()
75
-
76
60
  // @vue/component
77
61
  export default DitoComponent.component('DitoForm', {
78
62
  mixins: [RouteMixin, ResourceMixin],
79
- components: { DefineTemplate, ReuseTemplate },
80
63
 
81
64
  data() {
82
65
  return {
@@ -143,11 +126,7 @@ export default DitoComponent.component('DitoForm', {
143
126
  )
144
127
  },
145
128
 
146
- isInlinedSource() {
147
- return isInlined(this.sourceSchema)
148
- },
149
-
150
- isActive() {
129
+ isActiveRoute() {
151
130
  return this.isLastRoute || this.isLastUnnestedRoute
152
131
  },
153
132
 
@@ -190,11 +169,6 @@ export default DitoComponent.component('DitoForm', {
190
169
  return this.isCreating ? 'post' : 'patch'
191
170
  },
192
171
 
193
- resource() {
194
- const resource = this.getResource()
195
- return getMemberResource(this.itemId, resource) || resource
196
- },
197
-
198
172
  breadcrumbPrefix() {
199
173
  return capitalize(this.isCreating ? this.verbs.create : this.verbs.edit)
200
174
  },
@@ -347,6 +321,12 @@ export default DitoComponent.component('DitoForm', {
347
321
  )
348
322
  },
349
323
 
324
+ // @override ResourceMixin.getResource()
325
+ getResource(defaults) {
326
+ const resource = ResourceMixin.methods.getResource.call(this, defaults)
327
+ return getMemberResource(this.itemId, resource) || resource
328
+ },
329
+
350
330
  // @override ResourceMixin.setupData()
351
331
  setupData() {
352
332
  if (this.isCreating) {
@@ -359,7 +339,8 @@ export default DitoComponent.component('DitoForm', {
359
339
  setSourceData(data) {
360
340
  if (this.sourceData && this.sourceKey !== null) {
361
341
  const { mainSchemaComponent } = this
362
- this.sourceData[this.sourceKey] = mainSchemaComponent.filterData(data)
342
+ this.sourceData[this.sourceKey] =
343
+ mainSchemaComponent.filterData(data).localData
363
344
  mainSchemaComponent.onChange()
364
345
  return true
365
346
  }
@@ -422,11 +403,13 @@ export default DitoComponent.component('DitoForm', {
422
403
  const getVerb = present => this.verbs[this.getSubmitVerb(present)]
423
404
 
424
405
  // Allow buttons to override both method and resource path to submit to:
406
+ let { method } = this
407
+ let resource = this.getResource({ method })
425
408
  const buttonResource = getResource(button.schema.resource, {
426
- parent: this.resource
409
+ parent: resource
427
410
  })
428
- const resource = buttonResource || this.resource
429
- const method = resource?.method || this.method
411
+ resource = buttonResource || resource
412
+ method = resource?.method || method
430
413
  const data = this.getPayloadData(button, method)
431
414
  let success
432
415
  if (!buttonResource && this.isTransient) {
@@ -0,0 +1,26 @@
1
+ <template lang="pug">
2
+ //- Use a <div> for nested forms, as we shouldn't nest actual <form> tags.
3
+ div(
4
+ v-if="nested"
5
+ )
6
+ slot
7
+ form.dito-scroll-parent(
8
+ v-else
9
+ @submit.prevent
10
+ )
11
+ slot
12
+ </template>
13
+
14
+ <script>
15
+ import DitoComponent from '../DitoComponent.js'
16
+
17
+ // @vue/component
18
+ export default DitoComponent.component('DitoFormInner', {
19
+ props: {
20
+ nested: {
21
+ type: Boolean,
22
+ default: false
23
+ }
24
+ }
25
+ })
26
+ </script>
@@ -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('DitoFormInlined', {
6
+ export default DitoComponent.component('DitoFormNested', {
7
7
  extends: DitoForm
8
8
  })
9
9
  </script>
10
10
 
11
11
  <style lang="scss">
12
- .dito-form-inlined {
13
- // No scrolling in inlined forms, and prevent open .multiselect from
12
+ .dito-form-nested {
13
+ // No scrolling inside nested forms, and prevent open .multiselect from
14
14
  // being cropped.
15
15
  overflow: visible;
16
16
  }
@@ -9,6 +9,7 @@ slot(name="before")
9
9
  to=".dito-sidebar__teleport"
10
10
  )
11
11
  DitoPanels(
12
+ v-if="active"
12
13
  :panels="panelEntries"
13
14
  :data="data"
14
15
  :meta="meta"
@@ -20,7 +21,9 @@ slot(name="before")
20
21
  :to="headerTeleport"
21
22
  :disabled="!headerTeleport"
22
23
  )
23
- .dito-schema-header
24
+ .dito-schema-header(
25
+ v-if="active"
26
+ )
24
27
  DitoLabel(
25
28
  v-if="hasLabel"
26
29
  :label="label"
@@ -119,7 +122,8 @@ import {
119
122
  getPanelEntries,
120
123
  setDefaultValues,
121
124
  processData,
122
- isEmptySchema
125
+ isEmptySchema,
126
+ isNested
123
127
  } from '../utils/schema.js'
124
128
  import { getSchemaAccessor, getStoreAccessor } from '../utils/accessor.js'
125
129
 
@@ -147,6 +151,7 @@ export default DitoComponent.component('DitoSchema', {
147
151
  store: { type: Object, default: () => ({}) },
148
152
  label: { type: [String, Object], default: null },
149
153
  padding: { type: String, default: null },
154
+ active: { type: Boolean, default: true },
150
155
  inlined: { type: Boolean, default: false },
151
156
  disabled: { type: Boolean, default: false },
152
157
  collapsed: { type: Boolean, default: false },
@@ -223,7 +228,7 @@ export default DitoComponent.component('DitoSchema', {
223
228
  },
224
229
 
225
230
  headerTeleport() {
226
- return this.isRootSchema
231
+ return this.isTopLevelSchema
227
232
  ? '.dito-header__teleport'
228
233
  : this.labelNode
229
234
  },
@@ -283,9 +288,8 @@ export default DitoComponent.component('DitoSchema', {
283
288
  )
284
289
  },
285
290
 
286
- isRootSchema() {
287
- // Section schemas can share the root dataPath but they are inlined.
288
- return this.dataPath === '' && !this.inlined
291
+ isNested() {
292
+ return isNested(this.schema)
289
293
  },
290
294
 
291
295
  isDirty() {
@@ -320,8 +324,12 @@ export default DitoComponent.component('DitoSchema', {
320
324
  return !!this.tabs
321
325
  },
322
326
 
323
- hasRootTabs() {
324
- return this.hasTabs && this.isRootSchema
327
+ isTopLevelSchema() {
328
+ return !this.isNested && !this.inlined
329
+ },
330
+
331
+ hasTopLevelTabs() {
332
+ return this.hasTabs && this.isTopLevelSchema
325
333
  },
326
334
 
327
335
  hasMainPane() {
@@ -385,7 +393,7 @@ export default DitoComponent.component('DitoSchema', {
385
393
  // Remember the current path to know if tab changes should still be
386
394
  // handled, but remove the trailing `/create` or `/:id` from it so that
387
395
  // tabs informs that stay open after creation still work.
388
- if (this.hasRootTabs) {
396
+ if (this.hasTopLevelTabs) {
389
397
  this.selectedTab = routeTab
390
398
  }
391
399
  }
@@ -399,7 +407,7 @@ export default DitoComponent.component('DitoSchema', {
399
407
  content.scrollTop = this.scrollPositions[newTab] ?? 0
400
408
  })
401
409
  }
402
- if (this.hasRootTabs) {
410
+ if (this.hasTopLevelTabs) {
403
411
  const tab = this.shouldRenderSchema(this.tabs[newTab])
404
412
  ? newTab
405
413
  : this.defaultTab
@@ -687,20 +695,22 @@ export default DitoComponent.component('DitoSchema', {
687
695
  },
688
696
 
689
697
  filterData(data) {
690
- // Filters out arrays and objects that are back by data resources
691
- // themselves, as those are already taking care of through their own API
698
+ // Filters out arrays and objects that are backed by data resources
699
+ // themselves, as those are already taken care of through their own API
692
700
  // resource end-points and shouldn't be set.
693
- const copy = {}
701
+ const localData = {}
702
+ const foreignData = {}
694
703
  for (const [name, value] of Object.entries(data)) {
695
704
  if (isArray(value) || isObject(value)) {
696
705
  const components = this.getComponentsByName(name)
697
706
  if (components.some(component => component.providesData)) {
707
+ foreignData[name] = value
698
708
  continue
699
709
  }
700
710
  }
701
- copy[name] = value
711
+ localData[name] = value
702
712
  }
703
- return copy
713
+ return { localData, foreignData }
704
714
  },
705
715
 
706
716
  processData({ target = 'clipboard', schemaOnly = true } = {}) {
@@ -75,7 +75,7 @@ export default DitoComponent.component('DitoView', {
75
75
  },
76
76
 
77
77
  mainComponent() {
78
- return this.mainSchemaComponent.getComponentByDataPath(this.name)
78
+ return this.mainSchemaComponent?.getComponentByDataPath(this.name)
79
79
  },
80
80
 
81
81
  viewSchema() {
@@ -27,7 +27,8 @@ 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 DitoFormInlined } from './DitoFormInlined.vue'
30
+ export { default as DitoFormInner } from './DitoFormInner.vue'
31
+ export { default as DitoFormNested } from './DitoFormNested.vue'
31
32
  export { default as DitoErrors } from './DitoErrors.vue'
32
33
  export { default as DitoScopes } from './DitoScopes.vue'
33
34
  export { default as DitoPagination } from './DitoPagination.vue'
@@ -109,15 +109,22 @@ export default {
109
109
  async resolveData(load, loadingOptions = {}) {
110
110
  // Use a timeout to allow already resolved promises to return data without
111
111
  // showing a loading indicator.
112
- const timer = setTimeout(() => this.setLoading(true, loadingOptions), 0)
112
+ let clearLoading = false
113
+ const timer = setTimeout(() => {
114
+ this.setLoading(true, loadingOptions)
115
+ clearLoading = true
116
+ }, 0)
113
117
  let data = null
114
118
  try {
115
119
  data = await (isFunction(load) ? load() : load)
116
120
  } catch (error) {
117
121
  this.addError(error.message || error)
118
122
  }
119
- clearTimeout(timer)
120
- this.setLoading(false, loadingOptions)
123
+ if (clearLoading) {
124
+ this.setLoading(false, loadingOptions)
125
+ } else {
126
+ clearTimeout(timer)
127
+ }
121
128
  return data
122
129
  }
123
130
  }
@@ -155,15 +155,19 @@ export default {
155
155
  },
156
156
 
157
157
  parentSchemaComponent() {
158
- return this.schemaComponent?.parentComponent.schemaComponent
158
+ return getParentComponent(this, 'schemaComponent')
159
159
  },
160
160
 
161
161
  parentRouteComponent() {
162
- return this.routeComponent?.parentComponent.routeComponent
162
+ return getParentComponent(this, 'routeComponent')
163
163
  },
164
164
 
165
165
  parentFormComponent() {
166
- return this.formComponent?.parentComponent.formComponent
166
+ return getParentComponent(this, 'formComponent')
167
+ },
168
+
169
+ parentResourceComponent() {
170
+ return getParentComponent(this, 'resourceComponent')
167
171
  },
168
172
 
169
173
  // Returns the data of the first route component in the chain of parents
@@ -307,13 +311,12 @@ export default {
307
311
  },
308
312
 
309
313
  getResourcePath(resource) {
310
- resource = getResource(resource)
311
- // Resources without a parent inherit the one from `dataComponent`
312
- // automatically.
313
- if (resource.parent === undefined) {
314
- resource.parent = this.dataComponent?.resource
315
- }
316
- return this.api.resources.any(getResource(resource))
314
+ resource = getResource(resource, {
315
+ // Resources without a parent inherit the one from `dataComponent`
316
+ // automatically.
317
+ parent: this.dataComponent?.resource ?? null
318
+ })
319
+ return this.api.resources.any(resource)
317
320
  },
318
321
 
319
322
  getResourceUrl(resource) {
@@ -574,3 +577,12 @@ export default {
574
577
  }
575
578
 
576
579
  let nextUid = 0
580
+
581
+ function getParentComponent(component, key) {
582
+ const current = component[key]
583
+ let parent = component.parentComponent
584
+ while (parent && parent[key] === current) {
585
+ parent = parent.parentComponent
586
+ }
587
+ return parent?.[key] ?? null
588
+ }
@@ -260,7 +260,7 @@ export default {
260
260
  }
261
261
  },
262
262
 
263
- processValue(schema, value, dataPath, graph) {
263
+ processValue({ schema, value, dataPath }, graph) {
264
264
  if (schema.relate) {
265
265
  // For internally relating data (`schema.options.dataPath`), we need to
266
266
  // process both the options (for '#ref') and the value ('#id').
@@ -1,7 +1,7 @@
1
1
  import ItemMixin from './ItemMixin.js'
2
2
  import LoadingMixin from './LoadingMixin.js'
3
3
  import { setDefaultValues } from '../utils/schema.js'
4
- import { isObject, isString, labelize } from '@ditojs/utils'
4
+ import { assignDeeply, isObject, isString, labelize } from '@ditojs/utils'
5
5
  import { getResource } from '../utils/resource.js'
6
6
  import DitoContext from '../DitoContext.js'
7
7
 
@@ -31,8 +31,6 @@ export default {
31
31
  },
32
32
 
33
33
  resource() {
34
- // Returns the resource object representing the resource for the
35
- // associated source schema.
36
34
  return this.getResource()
37
35
  },
38
36
 
@@ -126,12 +124,13 @@ export default {
126
124
  },
127
125
 
128
126
  methods: {
129
- getResource() {
130
- // This is defined as a method so the computed `resource` getter can
131
- // be overridden and `super` functionality can still be accessed.
127
+ getResource(defaults = { method: 'get' }) {
128
+ // Returns the resource object representing the resource for the
129
+ // associated source schema.
132
130
  return getResource(this.sourceSchema?.resource, {
133
131
  type: 'collection',
134
- parent: this.parentFormComponent?.resource ?? null
132
+ parent: this.parentResourceComponent?.resource ?? null,
133
+ ...defaults
135
134
  })
136
135
  },
137
136
 
@@ -230,7 +229,7 @@ export default {
230
229
  async handleRequest(
231
230
  {
232
231
  method,
233
- resource = this.resource,
232
+ resource = this.getResource({ method }),
234
233
  query,
235
234
  data
236
235
  },
@@ -244,6 +243,7 @@ export default {
244
243
  const controller = new AbortController()
245
244
  this.abortController = controller
246
245
  const { signal } = controller
246
+ method = resource.method || method
247
247
  const request = { method, resource, query, data, signal }
248
248
  this.setLoading(true, loadingOptions)
249
249
  try {
@@ -340,7 +340,17 @@ export default {
340
340
  // Update the underlying data before calling `notify()` or
341
341
  // `this.itemLabel`, so id is set after creating new items.
342
342
  if (setData && data) {
343
- this.setData(data)
343
+ // Preserve the foreign data entries when updating the data.
344
+ const { foreignData } = this.mainSchemaComponent.filterData(
345
+ this.data
346
+ )
347
+ // Tell the parent route to reload its data, so that it can
348
+ // update its foreign data entries.
349
+ const parentMeta = this.parentRouteComponent?.routeRecord?.meta
350
+ if (parentMeta) {
351
+ parentMeta.reload = true
352
+ }
353
+ this.setData(assignDeeply({}, foreignData, data))
344
354
  }
345
355
  onSuccess?.()
346
356
  await this.emitButtonEvent(button, 'success', {
@@ -60,6 +60,10 @@ export default {
60
60
  return false
61
61
  },
62
62
 
63
+ isNestedRoute() {
64
+ return this.meta.nested
65
+ },
66
+
63
67
  isView() {
64
68
  return false
65
69
  },