@ditojs/admin 2.2.1 → 2.2.3

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.1",
3
+ "version": "2.2.3",
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,7 +33,7 @@
33
33
  "not ie_mob > 0"
34
34
  ],
35
35
  "dependencies": {
36
- "@ditojs/ui": "^2.2.0",
36
+ "@ditojs/ui": "^2.2.3",
37
37
  "@ditojs/utils": "^2.2.0",
38
38
  "@kyvg/vue3-notification": "^2.9.0",
39
39
  "@lk77/vue3-color": "^3.0.6",
@@ -73,16 +73,16 @@
73
73
  "vue-upload-component": "^3.1.8"
74
74
  },
75
75
  "devDependencies": {
76
- "@ditojs/build": "^2.2.0",
76
+ "@ditojs/build": "^2.2.3",
77
77
  "@vitejs/plugin-vue": "^4.1.0",
78
78
  "@vue/compiler-sfc": "^3.2.47",
79
79
  "pug": "^3.0.2",
80
80
  "sass": "1.62.0",
81
81
  "typescript": "^5.0.4",
82
- "vite": "^4.2.1"
82
+ "vite": "^4.3.1"
83
83
  },
84
84
  "types": "types",
85
- "gitHead": "306d4d9ef2a0c989b026f1d1cd77f6d4fd60430b",
85
+ "gitHead": "7102747103c7d60e9fffca44ca6ae459bea67cc1",
86
86
  "scripts": {
87
87
  "build": "vite build",
88
88
  "watch": "yarn build --mode 'development' --watch",
package/src/DitoAdmin.js CHANGED
@@ -3,8 +3,6 @@ import { createRouter, createWebHistory } from 'vue-router'
3
3
  import VueNotifications from '@kyvg/vue3-notification'
4
4
  import {
5
5
  isString,
6
- isArray,
7
- asArray,
8
6
  isAbsoluteUrl,
9
7
  merge,
10
8
  hyphenate,
@@ -17,6 +15,7 @@ import DitoRoot from './components/DitoRoot.vue'
17
15
  import DitoTypeComponent from './DitoTypeComponent.js'
18
16
  import { getResource } from './utils/resource.js'
19
17
  import { deprecate } from './utils/deprecate.js'
18
+ import { formatQuery } from './utils/route.js'
20
19
  import verbs from './verbs.js'
21
20
 
22
21
  export default class DitoAdmin {
@@ -297,24 +296,3 @@ function combineUrls(baseUrl, relativeUrl) {
297
296
  // Use same approach as axios `combineURLs()` to join baseUrl & relativeUrl:
298
297
  return `${baseUrl.replace(/\/+$/, '')}/${relativeUrl.replace(/^\/+/, '')}`
299
298
  }
300
-
301
- function formatQuery(query) {
302
- const entries = query
303
- ? isArray(query)
304
- ? query
305
- : Object.entries(query)
306
- : []
307
- return new URLSearchParams(
308
- // Expand array values into multiple entries under the same key, so
309
- // `formatQuery({ foo: [1, 2], bar: 3 })` => 'foo=1&foo=2&bar=3'.
310
- entries.reduce(
311
- (entries, [key, value]) => {
312
- for (const val of asArray(value)) {
313
- entries.push([key, val])
314
- }
315
- return entries
316
- },
317
- []
318
- )
319
- ).toString()
320
- }
@@ -5,18 +5,30 @@ nav.dito-menu.dito-scroll-parent
5
5
  li(
6
6
  v-for="view in views"
7
7
  )
8
- RouterLink.dito-link(
8
+ RouterLink(
9
9
  v-if="shouldRender(view)"
10
+ v-slot="{ isActive, href, route }"
11
+ custom
10
12
  :to="`/${view.path}`"
11
- activeClass="dito-active"
12
- ) {{ getLabel(view) }}
13
+ )
14
+ a.dito-link(
15
+ :href="href"
16
+ :class="{ 'dito-active': isActive }"
17
+ @click.prevent="onClickLink(route)"
18
+ ) {{ getLabel(view) }}
13
19
  </template>
14
20
 
15
21
  <script>
16
22
  import DitoComponent from '../DitoComponent.js'
17
23
 
18
24
  // @vue/component
19
- export default DitoComponent.component('DitoMenu', {})
25
+ export default DitoComponent.component('DitoMenu', {
26
+ methods: {
27
+ onClickLink(route) {
28
+ this.$router.push({ ...route, force: true })
29
+ }
30
+ }
31
+ })
20
32
  </script>
21
33
 
22
34
  <style lang="scss">
@@ -101,6 +101,7 @@ export default DitoComponent.component('DitoPane', {
101
101
  return this.componentSchemas.flatMap(
102
102
  ({ schema, nestedDataPath: dataPath }) =>
103
103
  getAllPanelEntries(
104
+ this.api,
104
105
  schema,
105
106
  dataPath,
106
107
  this.schemaComponent,
@@ -118,7 +119,7 @@ export default DitoComponent.component('DitoPane', {
118
119
  this._register(true)
119
120
  },
120
121
 
121
- beforeUnmount() {
122
+ unmounted() {
122
123
  this._register(false)
123
124
  },
124
125
 
@@ -5,8 +5,7 @@ component.dito-panel(
5
5
  :is="panelTag"
6
6
  @submit.prevent
7
7
  )
8
- label.dito-panel-title {{ getLabel(schema) }}
9
- DitoSchema.dito-panel-schema(
8
+ DitoSchema.dito-panel__schema(
10
9
  :schema="panelSchema"
11
10
  :dataPath="panelDataPath"
12
11
  :data="panelData"
@@ -15,6 +14,17 @@ component.dito-panel(
15
14
  :disabled="disabled"
16
15
  :hasOwnData="hasOwnData"
17
16
  )
17
+ template(#before)
18
+ h2.dito-panel__header(:class="{ 'dito-panel__header--sticky': sticky }")
19
+ span {{ getLabel(schema) }}
20
+ DitoButtons.dito-buttons-small(
21
+ :buttons="panelButtonSchemas"
22
+ :dataPath="panelDataPath"
23
+ :data="panelData"
24
+ :meta="meta"
25
+ :store="store"
26
+ :disabled="disabled"
27
+ )
18
28
  template(#buttons)
19
29
  DitoButtons(
20
30
  :buttons="buttonSchemas"
@@ -73,6 +83,10 @@ export default DitoComponent.component('DitoPanel', {
73
83
  return getButtonSchemas(this.schema.buttons)
74
84
  },
75
85
 
86
+ panelButtonSchemas() {
87
+ return getButtonSchemas(this.schema.panelButtons)
88
+ },
89
+
76
90
  target() {
77
91
  return this.schema.target || this.dataPath
78
92
  },
@@ -111,6 +125,11 @@ export default DitoComponent.component('DitoPanel', {
111
125
  visible: getSchemaAccessor('visible', {
112
126
  type: Boolean,
113
127
  default: true
128
+ }),
129
+
130
+ sticky: getSchemaAccessor('sticky', {
131
+ type: Boolean,
132
+ default: false
114
133
  })
115
134
  },
116
135
 
@@ -126,7 +145,7 @@ export default DitoComponent.component('DitoPanel', {
126
145
  }
127
146
  },
128
147
 
129
- beforeUnmount() {
148
+ unmounted() {
130
149
  this._register(false)
131
150
  },
132
151
 
@@ -144,19 +163,53 @@ export default DitoComponent.component('DitoPanel', {
144
163
  @import '../styles/_imports';
145
164
 
146
165
  .dito-panel {
147
- margin-bottom: $content-padding;
166
+ padding-bottom: $content-padding;
148
167
 
149
- .dito-panel-title {
168
+ &__header {
150
169
  display: block;
170
+ position: relative;
151
171
  box-sizing: border-box;
152
172
  padding: $input-padding;
153
173
  background: $button-color;
154
174
  border: $border-style;
155
175
  border-top-left-radius: $border-radius;
156
176
  border-top-right-radius: $border-radius;
177
+
178
+ &--sticky {
179
+ $margin: $input-height-factor * $line-height * $font-size-small +
180
+ $form-spacing;
181
+
182
+ position: sticky;
183
+ top: $content-padding;
184
+ margin-bottom: $margin;
185
+ z-index: 1;
186
+
187
+ & + * {
188
+ margin-top: -$margin;
189
+ }
190
+
191
+ &::before {
192
+ content: '';
193
+ display: block;
194
+ position: absolute;
195
+ background: $content-color-background;
196
+ left: 0;
197
+ right: 0;
198
+ height: $content-padding;
199
+ top: -$content-padding;
200
+ margin: -$border-width;
201
+ }
202
+ }
203
+
204
+ .dito-buttons {
205
+ position: absolute;
206
+ right: $input-padding-ver;
207
+ top: 50%;
208
+ transform: translateY(-50%);
209
+ }
157
210
  }
158
211
 
159
- .dito-panel-schema {
212
+ &__schema {
160
213
  font-size: $font-size-small;
161
214
  background: $content-color-background;
162
215
  border: $border-style;
@@ -167,16 +220,24 @@ export default DitoComponent.component('DitoPanel', {
167
220
  > .dito-schema-content {
168
221
  padding: $form-spacing-half $form-spacing;
169
222
 
223
+ .dito-container {
224
+ padding: $form-spacing-half;
225
+ }
226
+
227
+ .dito-object {
228
+ border: 0;
229
+ padding: 0;
230
+ }
231
+
170
232
  > .dito-buttons {
171
233
  --button-margin: #{$form-spacing};
172
234
 
173
235
  padding: $form-spacing-half 0;
174
- }
175
- }
176
236
 
177
- .dito-object {
178
- border: 0;
179
- padding: 0;
237
+ .dito-container {
238
+ padding: 0;
239
+ }
240
+ }
180
241
  }
181
242
 
182
243
  .dito-label {
@@ -190,10 +251,6 @@ export default DitoComponent.component('DitoPanel', {
190
251
  .dito-pane {
191
252
  margin: 0 (-$form-spacing-half);
192
253
  }
193
-
194
- .dito-container {
195
- padding: $form-spacing-half;
196
- }
197
254
  }
198
255
  }
199
256
  </style>
@@ -212,8 +212,16 @@ export default DitoComponent.component('DitoRoot', {
212
212
  ...additionalComponents
213
213
  },
214
214
  buttons: {
215
- cancel: {},
216
- login: { type: 'submit' }
215
+ cancel: {
216
+ type: 'button',
217
+ text: 'Cancel'
218
+ // NOTE: The click event is added in DitoDialog.buttonSchemas()
219
+ },
220
+
221
+ login: {
222
+ type: 'submit',
223
+ text: 'Login'
224
+ }
217
225
  }
218
226
  })
219
227
  if (loginData) {
@@ -1,5 +1,8 @@
1
1
  <template lang="pug">
2
- .dito-schema
2
+ slot(name="before")
3
+ .dito-schema(
4
+ v-bind="$attrs"
5
+ )
3
6
  .dito-schema-content
4
7
  .dito-schema-header(
5
8
  v-if="hasLabel || hasTabs || clipboard"
@@ -81,6 +84,7 @@
81
84
  :store="store"
82
85
  :disabled="disabled"
83
86
  )
87
+ slot(name="after")
84
88
  </template>
85
89
 
86
90
  <script>
@@ -334,7 +338,7 @@ export default DitoComponent.component('DitoSchema', {
334
338
  this.emitEvent('initialize') // Not `'create'`, since that's for data.
335
339
  },
336
340
 
337
- beforeUnmount() {
341
+ unmounted() {
338
342
  this.emitEvent('destroy')
339
343
  this._register(false)
340
344
  },
@@ -98,13 +98,16 @@ export default {
98
98
  },
99
99
 
100
100
  async resolveData(load, loadingOptions = {}) {
101
- this.setLoading(true, loadingOptions)
101
+ // Use a timeout to allow already resolved promises to return data without
102
+ // showing a loading indicator.
103
+ const timer = setTimeout(() => this.setLoading(true, loadingOptions), 0)
102
104
  let data = null
103
105
  try {
104
106
  data = await (isFunction(load) ? load() : load)
105
107
  } catch (error) {
106
108
  this.addError(error.message || error)
107
109
  }
110
+ clearTimeout(timer)
108
111
  this.setLoading(false, loadingOptions)
109
112
  return data
110
113
  }
@@ -176,6 +176,8 @@ export default {
176
176
  },
177
177
 
178
178
  methods: {
179
+ labelize,
180
+
179
181
  // The state of components is only available during the life-cycle of a
180
182
  // component. Some information we need available longer than that, e.g.
181
183
  // `query` & `total` on TypeList, so that when the user navigates back from
@@ -245,13 +247,10 @@ export default {
245
247
  : labelize(name) || ''
246
248
  },
247
249
 
248
- labelize,
249
-
250
- getButtonAttributes(name, button) {
251
- const verb = this.verbs[name] || name
250
+ getButtonAttributes(verb) {
252
251
  return {
253
252
  class: `dito-button-${verb}`,
254
- title: button?.text || labelize(verb)
253
+ title: labelize(verb)
255
254
  }
256
255
  },
257
256
 
@@ -9,7 +9,7 @@ export default {
9
9
  }
10
10
  },
11
11
 
12
- beforeUnmount() {
12
+ unmounted() {
13
13
  for (const { remove } of this.domHandlers) {
14
14
  remove()
15
15
  }
@@ -25,7 +25,13 @@ export default {
25
25
  },
26
26
 
27
27
  stepValue() {
28
- return this.step == null && !this.isInteger ? 'any' : this.step
28
+ // Don't show steps if the input is also clearable, since the step buttons
29
+ // would collide with the clear button.
30
+ return this.clearable
31
+ ? null
32
+ : this.step == null && !this.isInteger
33
+ ? 'any'
34
+ : this.step
29
35
  },
30
36
 
31
37
  decimals: getSchemaAccessor('decimals', {
@@ -3,6 +3,7 @@ import ResourceMixin from './ResourceMixin.js'
3
3
  import SchemaParentMixin from '../mixins/SchemaParentMixin.js'
4
4
  import { getSchemaAccessor, getStoreAccessor } from '../utils/accessor.js'
5
5
  import { getMemberResource } from '../utils/resource.js'
6
+ import { replaceRoute } from '../utils/route.js'
6
7
  import {
7
8
  processRouteSchema,
8
9
  processForms,
@@ -43,8 +44,7 @@ export default {
43
44
  data() {
44
45
  return {
45
46
  wrappedPrimitives: null,
46
- unwrappingPrimitives: false,
47
- ignoreRouteChange: false
47
+ unwrappingPrimitives: false
48
48
  }
49
49
  },
50
50
 
@@ -177,9 +177,9 @@ export default {
177
177
  ...query
178
178
  }
179
179
  if (!equals(query, this.$route.query)) {
180
- // Tell the `$route` watcher to ignore the changed triggered here:
181
- this.ignoreRouteChange = true
182
- this.$router.replace({ query, hash: this.$route.hash })
180
+ // Change the route query parameters, but don't trigger a route
181
+ // change, as that would cause the list to reload.
182
+ replaceRoute({ query })
183
183
  }
184
184
  return query // Let getStoreAccessor() do the actual setting
185
185
  }
@@ -314,10 +314,6 @@ export default {
314
314
 
315
315
  watch: {
316
316
  $route(to, from) {
317
- if (this.ignoreRouteChange) {
318
- this.ignoreRouteChange = false
319
- return
320
- }
321
317
  if (from.path === to.path && from.hash === to.hash) {
322
318
  // Paths and hashes remain the same, so only queries have changed.
323
319
  // Update filter and reload data without clearing.
@@ -229,7 +229,7 @@ export default {
229
229
  this.setupSchemaFields()
230
230
  },
231
231
 
232
- beforeUnmount() {
232
+ unmounted() {
233
233
  this._register(false)
234
234
  },
235
235
 
@@ -63,6 +63,10 @@
63
63
  content: 'Edit';
64
64
  }
65
65
 
66
+ .dito-button-clear:empty::before {
67
+ content: 'Clear';
68
+ }
69
+
66
70
  .dito-button-save:empty::before {
67
71
  content: 'Save';
68
72
  }
@@ -159,6 +163,10 @@
159
163
  @extend %icon-edit;
160
164
  }
161
165
 
166
+ .dito-button-clear:empty::before {
167
+ @extend %icon-clear;
168
+ }
169
+
162
170
  .dito-button-drag:empty::before {
163
171
  @extend %icon-drag;
164
172
  }
@@ -3,7 +3,7 @@ button.dito-button(
3
3
  :id="dataPath"
4
4
  ref="element"
5
5
  :type="type"
6
- :title="text"
6
+ :title="title"
7
7
  :class="buttonClass"
8
8
  v-bind="attributes"
9
9
  ) {{ text }}
@@ -34,12 +34,13 @@ export default DitoTypeComponent.register(
34
34
  },
35
35
 
36
36
  text: getSchemaAccessor('text', {
37
- type: String,
38
- get(text) {
39
- return text || labelize(this.verb)
40
- }
37
+ type: String
41
38
  }),
42
39
 
40
+ title() {
41
+ return this.text || labelize(this.verb)
42
+ },
43
+
43
44
  closeForm: getSchemaAccessor('closeForm', {
44
45
  type: Boolean,
45
46
  default: false
@@ -110,10 +110,6 @@ export default DitoTypeComponent.register('code', {
110
110
  // for proper line-height calculation.
111
111
  padding: $input-padding;
112
112
 
113
- &.dito-width-fill {
114
- width: auto;
115
- }
116
-
117
113
  .codeflask {
118
114
  background: none;
119
115
  // Ignore the parent padding defined above which is only needed to set
@@ -156,12 +156,11 @@ import DitoContext from '../DitoContext.js'
156
156
  import SourceMixin from '../mixins/SourceMixin.js'
157
157
  import SortableMixin from '../mixins/SortableMixin.js'
158
158
  import {
159
- getNamedSchemas,
160
159
  getViewEditPath,
161
160
  resolveSchemaComponent,
162
161
  resolveSchemaComponents
163
162
  } from '../utils/schema.js'
164
- import { getFiltersPanel } from '../utils/filter.js'
163
+ import { createFiltersPanel } from '../utils/filter.js'
165
164
  import { appendDataPath } from '../utils/data.js'
166
165
  import { pickBy, equals, hyphenate } from '@ditojs/utils'
167
166
 
@@ -175,7 +174,7 @@ export default DitoTypeComponent.register('list', {
175
174
  return type
176
175
  },
177
176
 
178
- getPanelSchema(schema, dataPath, schemaComponent) {
177
+ getPanelSchema(api, schema, dataPath, schemaComponent) {
179
178
  const { filters } = schema
180
179
  // See if this list component wants to display a filter panel, and if so,
181
180
  // create the panel schema for it through `getFiltersPanel()`.
@@ -189,27 +188,23 @@ export default DitoTypeComponent.register('list', {
189
188
  component => component.type === 'list'
190
189
  )
191
190
 
192
- return getFiltersPanel(
193
- getNamedSchemas(filters),
194
- dataPath,
195
- {
196
- // Create a simple proxy to get / set the query, see getFiltersPanel()
197
- get query() {
198
- return getListComponent()?.query
199
- },
200
- set query(query) {
201
- const component = getListComponent()
202
- if (component) {
203
- // Filter out undefined values for comparing with equals()
204
- const filter = obj => pickBy(obj, value => value !== undefined)
205
- if (!equals(filter(query), filter(component.query))) {
206
- component.query = query
207
- component.loadData(false)
208
- }
191
+ return createFiltersPanel(api, filters, dataPath, {
192
+ // Create a simple proxy to get / set the query, see getFiltersPanel()
193
+ get query() {
194
+ return getListComponent()?.query
195
+ },
196
+ set query(query) {
197
+ const component = getListComponent()
198
+ if (component) {
199
+ // Filter out undefined values for comparing with equals()
200
+ const filter = obj => pickBy(obj, value => value !== undefined)
201
+ if (!equals(filter(query), filter(component.query))) {
202
+ component.query = query
203
+ component.loadData(false)
209
204
  }
210
205
  }
211
206
  }
212
- )
207
+ })
213
208
  }
214
209
  },
215
210
 
@@ -262,7 +262,7 @@ export default DitoTypeComponent.register('markup', {
262
262
  })
263
263
  },
264
264
 
265
- beforeUnmount() {
265
+ unmounted() {
266
266
  this.editor.destroy()
267
267
  },
268
268
 
@@ -9,6 +9,12 @@ InputField.dito-number(
9
9
  :max="max"
10
10
  :step="stepValue"
11
11
  )
12
+ template(#after)
13
+ button.dito-button-clear.dito-button-overlay(
14
+ v-if="showClearButton"
15
+ :disabled="disabled"
16
+ @click.stop="clear"
17
+ )
12
18
  </template>
13
19
 
14
20
  <script>
@@ -9,7 +9,7 @@ export default DitoTypeComponent.register('panel', {
9
9
  alignBottom: false,
10
10
  omitPadding: true,
11
11
 
12
- getPanelSchema(schema) {
12
+ getPanelSchema(api, schema) {
13
13
  // For a TypePanel, the component schema is also the panel schema, but
14
14
  // remove the added name, so it doesn't get appended twice to data-path.
15
15
  const { name, ...panel } = schema
@@ -6,6 +6,12 @@ InputField.dito-text(
6
6
  :type="inputType"
7
7
  v-bind="attributes"
8
8
  )
9
+ template(#after)
10
+ button.dito-button-clear.dito-button-overlay(
11
+ v-if="showClearButton"
12
+ :disabled="disabled"
13
+ @click.stop="clear"
14
+ )
9
15
  </template>
10
16
 
11
17
  <script>