@ditojs/admin 2.1.1 → 2.1.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.1.1",
3
+ "version": "2.1.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.1.1",
36
+ "@ditojs/ui": "^2.1.2",
37
37
  "@ditojs/utils": "^2.1.1",
38
38
  "@kyvg/vue3-notification": "^2.9.0",
39
39
  "@lk77/vue3-color": "^3.0.6",
@@ -82,7 +82,7 @@
82
82
  "vite": "^4.2.1"
83
83
  },
84
84
  "types": "types",
85
- "gitHead": "b4ff87b7ffa812092a27b6c28a8ab3c5d7d7094d",
85
+ "gitHead": "39cbb715f40b8c3c657ca8d6790def22f170b31b",
86
86
  "scripts": {
87
87
  "build": "vite build",
88
88
  "watch": "yarn build --mode 'development' --watch",
@@ -67,6 +67,10 @@ export default class DitoContext {
67
67
  return get(this, 'nested', true)
68
68
  }
69
69
 
70
+ get schema() {
71
+ return get(this, 'schema', null)
72
+ }
73
+
70
74
  get value() {
71
75
  return get(this, 'value', undefined)
72
76
  }
@@ -154,9 +158,9 @@ export default class DitoContext {
154
158
  return get(this, 'formLabel', null)
155
159
  }
156
160
 
157
- // TODO: Remove exposure since the associated component doesn't always exist,
158
- // e.g. nested forms in `processData()`. Instead, bind component to `this`
159
- // only where available.
161
+ // NOTE: Like 'options', the associated component doesn't always exist, e.g.
162
+ // in nested forms during `processData()`. But `schema` is guaranteed to
163
+ // always be available.
160
164
  get component() {
161
165
  return get(this, 'component', null)
162
166
  }
@@ -37,7 +37,7 @@
37
37
  <script>
38
38
  import DitoComponent from '../DitoComponent.js'
39
39
  import { appendDataPath } from '../utils/data.js'
40
- import { getAllPanelSchemas, isNested } from '../utils/schema.js'
40
+ import { getAllPanelEntries, isNested } from '../utils/schema.js'
41
41
 
42
42
  // @vue/component
43
43
  export default DitoComponent.component('DitoPane', {
@@ -95,12 +95,12 @@ export default DitoComponent.component('DitoPane', {
95
95
  )
96
96
  },
97
97
 
98
- panelSchemas() {
98
+ panelEntries() {
99
99
  // Gather all panel schemas from all component schemas, by finding those
100
- // that want to provide a panel. See `getAllPanelSchemas()` for details.
100
+ // that want to provide a panel. See `getAllPanelEntries()` for details.
101
101
  return this.componentSchemas.flatMap(
102
102
  ({ schema, nestedDataPath: dataPath }) =>
103
- getAllPanelSchemas(
103
+ getAllPanelEntries(
104
104
  schema,
105
105
  dataPath,
106
106
  this.schemaComponent,
@@ -75,7 +75,7 @@
75
75
  v-else-if="isPopulated"
76
76
  )
77
77
  DitoPanels(
78
- :panels="panelSchemas"
78
+ :panels="panelEntries"
79
79
  :data="data"
80
80
  :meta="meta"
81
81
  :store="store"
@@ -99,7 +99,7 @@ import ItemMixin from '../mixins/ItemMixin.js'
99
99
  import { appendDataPath, getParentItem } from '../utils/data.js'
100
100
  import {
101
101
  getNamedSchemas,
102
- getPanelSchemas,
102
+ getPanelEntries,
103
103
  setDefaultValues,
104
104
  processData
105
105
  } from '../utils/schema.js'
@@ -164,12 +164,12 @@ export default DitoComponent.component('DitoSchema', {
164
164
  return this.hasOwnData ? null : this.parentComponent.schemaComponent
165
165
  },
166
166
 
167
- panelSchemas() {
168
- const panels = getPanelSchemas(this.schema.panels, '')
167
+ panelEntries() {
168
+ const panelEntries = getPanelEntries(this.schema.panels, '')
169
169
  for (const pane of this.panes) {
170
- panels.push(...pane.panelSchemas)
170
+ panelEntries.push(...pane.panelEntries)
171
171
  }
172
- return panels
172
+ return panelEntries
173
173
  },
174
174
 
175
175
  tabs() {
@@ -93,10 +93,7 @@ export default DitoComponent.component('DitoView', {
93
93
  },
94
94
 
95
95
  providesData() {
96
- return someSchemaComponent(
97
- this.viewSchema,
98
- component => hasResource(component)
99
- )
96
+ return someSchemaComponent(this.viewSchema, hasResource)
100
97
  }
101
98
  },
102
99
 
@@ -100,6 +100,7 @@ export default {
100
100
  default: null
101
101
  }),
102
102
 
103
+ // TODO: Rename to `options.labelKey` / `optionLabelKey`?
103
104
  optionLabel: getSchemaAccessor('options.label', {
104
105
  type: [String, Function],
105
106
  default: null,
@@ -114,6 +115,7 @@ export default {
114
115
  }
115
116
  }),
116
117
 
118
+ // TODO: Rename to `options.valueKey` / `optionValueKey`?
117
119
  optionValue: getSchemaAccessor('options.value', {
118
120
  type: [String, Function],
119
121
  default: null,
@@ -16,7 +16,8 @@ export default {
16
16
  getSortableOptions(draggable, fallback = false) {
17
17
  return {
18
18
  animation: 150,
19
- disabled: !draggable,
19
+ // TODO: This is broken in VueSortable, always enable it for now.
20
+ // disabled: !draggable,
20
21
  handle: '.dito-button-drag',
21
22
  dragClass: 'dito-sortable-active',
22
23
  chosenClass: 'dito-sortable-chosen',
@@ -8,6 +8,7 @@
8
8
  VueMultiselect(
9
9
  ref="element"
10
10
  v-model="selectedOptions"
11
+ :class="{ 'multiselect--show-highlight': showHighlight }"
11
12
  :showLabels="false"
12
13
  :placeholder="placeholder"
13
14
  tagPlaceholder="Press enter to add new tag"
@@ -45,6 +46,7 @@ import TypeMixin from '../mixins/TypeMixin.js'
45
46
  import OptionsMixin from '../mixins/OptionsMixin.js'
46
47
  import VueMultiselect from 'vue-multiselect'
47
48
  import { getSchemaAccessor } from '../utils/accessor.js'
49
+ import { isBoolean } from '@ditojs/utils'
48
50
 
49
51
  // @vue/component
50
52
  export default DitoTypeComponent.register('multiselect', {
@@ -53,6 +55,7 @@ export default DitoTypeComponent.register('multiselect', {
53
55
 
54
56
  data() {
55
57
  return {
58
+ isMounted: false,
56
59
  searchedOptions: null,
57
60
  populate: false
58
61
  }
@@ -71,8 +74,8 @@ export default DitoTypeComponent.register('multiselect', {
71
74
  )
72
75
  )
73
76
  // Filter out options that we couldn't match.
74
- // TODO: We really should display an error instead
75
- .filter(value => value)
77
+ // TODO: Should we display an error instead?
78
+ .filter(Boolean)
76
79
  : this.selectedOption
77
80
  },
78
81
 
@@ -111,20 +114,26 @@ export default DitoTypeComponent.register('multiselect', {
111
114
  }),
112
115
 
113
116
  placeholder() {
114
- const { placeholder, searchable, taggable } = this.schema
115
- return (
116
- placeholder || (
117
- searchable && taggable
118
- ? `Search or add a ${this.label}`
119
- : searchable
120
- ? `Select or search ${this.label}`
121
- : undefined
122
- )
123
- )
117
+ let { placeholder, searchable, taggable } = this.schema
118
+ if (isBoolean(placeholder)) {
119
+ placeholder = placeholder ? undefined : null
120
+ }
121
+ return placeholder === undefined
122
+ ? searchable && taggable
123
+ ? `Search or add a ${this.label}`
124
+ : searchable
125
+ ? `Select or search ${this.label}`
126
+ : undefined
127
+ : placeholder
128
+ },
129
+
130
+ showHighlight() {
131
+ return this.isMounted && this.$refs.element.pointerDirty
124
132
  }
125
133
  },
126
134
 
127
135
  mounted() {
136
+ this.isMounted = true
128
137
  if (this.autofocus) {
129
138
  // vue-multiselect doesn't support the autofocus attribute. We need to
130
139
  // handle it here.
@@ -211,224 +220,248 @@ $tag-line-height: 1em;
211
220
  .dito-multiselect {
212
221
  position: relative;
213
222
 
214
- .multiselect {
215
- font-size: inherit;
216
- min-height: inherit;
217
- color: $color-black;
223
+ &.dito-multiselect-single {
224
+ --input-width: 100%;
218
225
  }
219
226
 
220
- .multiselect__tags {
221
- font-size: inherit;
222
- overflow: auto;
223
- min-height: inherit;
224
- padding: 0 $spinner-width 0 0;
225
- // So tags can float on multiple lines and have proper margins:
226
- padding-bottom: $tag-margin;
227
+ &.dito-multiselect-multiple {
228
+ --input-width: auto;
227
229
  }
228
230
 
229
- .multiselect__tag {
230
- float: left;
231
- margin: $tag-margin 0 0 $tag-margin;
232
- border-radius: 1em;
233
- padding: $tag-padding $tag-icon-width $tag-padding 0.8em;
234
- line-height: $tag-line-height;
235
- height: calc($input-height - 2 * $tag-padding);
231
+ &.dito-has-errors {
232
+ &__tags {
233
+ border-color: $color-error;
234
+ }
236
235
  }
237
236
 
238
- .multiselect__tags-wrap {
239
- overflow: auto;
240
- line-height: 0;
237
+ .dito-button-clear {
238
+ width: $spinner-width;
241
239
  }
242
240
 
243
- .multiselect__single,
244
- .multiselect__placeholder,
245
- .multiselect__input {
241
+ .multiselect {
242
+ $self: last-selector(&);
243
+
246
244
  font-size: inherit;
247
- line-height: inherit;
248
- min-height: 0;
249
- margin: 0 0 1px 0;
250
- // Sadly, vue-select sets style="padding: ...;" in addition to using
251
- // classes, so `!important` is necessary:
252
- padding: $input-padding !important;
253
- // So input can float next to tags and have proper margins with
254
- // .multiselect__tags:
255
- padding-bottom: 0 !important;
256
- background: none;
257
- }
245
+ min-height: inherit;
246
+ color: $color-black;
258
247
 
259
- .multiselect__placeholder,
260
- .multiselect__input::placeholder {
261
- color: $color-placeholder;
262
- }
248
+ &--active {
249
+ #{$self}__placeholder {
250
+ // Don't use `display: none` to hide place-holder, as the layout would
251
+ // collapse.
252
+ display: inline-block;
253
+ visibility: hidden;
254
+ }
263
255
 
264
- .multiselect--active {
265
- .multiselect__placeholder {
266
- // Don't use `display: none` to hide place-holder, as the layout would
267
- // collapse.
268
- display: inline-block;
269
- visibility: hidden;
270
- }
271
- }
256
+ #{$self}__single,
257
+ #{$self}__input {
258
+ // Sadly, vue-select sets `style="width"` in addition to using classes
259
+ // so `!important` is necessary:
260
+ width: var(--input-width) !important;
261
+ }
272
262
 
273
- .multiselect__select,
274
- .multiselect__spinner {
275
- padding: 0;
276
- // $border-width to prevent masking border with .multiselect__spinner
277
- top: $border-width;
278
- right: $border-width;
279
- bottom: $border-width;
280
- height: inherit;
281
- border-radius: $border-radius;
282
- }
263
+ #{$self}__tags {
264
+ border-color: $color-active;
265
+ border-bottom-left-radius: 0;
266
+ border-bottom-right-radius: 0;
267
+ }
283
268
 
284
- .multiselect__select {
285
- width: 0;
286
- margin-right: calc($select-arrow-width / 2);
269
+ #{$self}__content-wrapper {
270
+ border: $border-width solid $color-active;
271
+ border-top-color: $border-color;
272
+ margin: -1px 0 0;
273
+ border-top-left-radius: 0;
274
+ border-top-right-radius: 0;
275
+ }
287
276
 
288
- &::before {
289
- @include arrow($select-arrow-size);
277
+ &#{$self}--above {
278
+ #{$self}__tags {
279
+ border-radius: $border-radius;
280
+ border-top-left-radius: 0;
281
+ border-top-right-radius: 0;
282
+ }
290
283
 
291
- bottom: $select-arrow-bottom;
292
- right: calc(-1 * $select-arrow-size / 2);
284
+ #{$self}__content-wrapper {
285
+ border: $border-width solid $color-active;
286
+ border-bottom-color: $border-color;
287
+ margin: 0 0 -1px;
288
+ border-radius: $border-radius;
289
+ border-bottom-left-radius: 0;
290
+ border-bottom-right-radius: 0;
291
+ }
292
+ }
293
293
  }
294
- }
295
294
 
296
- .multiselect__spinner {
297
- width: $spinner-width;
298
-
299
- &::before,
300
- &::after {
301
- // Change the width of the loading spinner
302
- border-width: 3px;
303
- border-top-color: $color-active;
304
- inset: 0;
305
- margin: auto;
295
+ &__tags {
296
+ font-size: inherit;
297
+ overflow: auto;
298
+ min-height: inherit;
299
+ padding: 0 $spinner-width 0 0;
300
+ // So tags can float on multiple lines and have proper margins:
301
+ padding-bottom: $tag-margin;
306
302
  }
307
- }
308
303
 
309
- .multiselect__option {
310
- min-height: unset;
311
- height: unset;
312
- line-height: $tag-line-height;
313
- padding: $input-padding;
314
-
315
- &::after {
316
- // Instruction text for options
317
- padding: $input-padding;
304
+ &__tag {
305
+ float: left;
306
+ margin: $tag-margin 0 0 $tag-margin;
307
+ border-radius: 1em;
308
+ padding: $tag-padding $tag-icon-width $tag-padding 0.8em;
318
309
  line-height: $tag-line-height;
310
+ height: calc($input-height - 2 * $tag-padding);
319
311
  }
320
- }
321
312
 
322
- .multiselect__option--highlight {
323
- &::after {
324
- display: block;
325
- position: absolute;
326
- background: transparent;
327
- color: $color-white;
313
+ &__tags-wrap {
314
+ overflow: auto;
315
+ line-height: 0;
328
316
  }
329
- }
330
317
 
331
- .multiselect__option--disabled {
332
- background: none;
333
- color: $color-disabled;
334
- }
318
+ &__single,
319
+ &__placeholder,
320
+ &__input {
321
+ font-size: inherit;
322
+ line-height: inherit;
323
+ min-height: 0;
324
+ margin: 0 0 1px 0;
325
+ // Sadly, vue-select sets style="padding: ...;" in addition to using
326
+ // classes, so `!important` is necessary:
327
+ padding: $input-padding !important;
328
+ // So input can float next to tags and have proper margins with
329
+ // &__tags:
330
+ padding-bottom: 0 !important;
331
+ background: none;
332
+ }
335
333
 
336
- .multiselect__tag-icon {
337
- background: none;
338
- border-radius: 1em;
339
- width: $tag-icon-width;
340
- margin: 0;
334
+ &__placeholder,
335
+ &__input::placeholder {
336
+ color: $color-placeholder;
337
+ }
341
338
 
342
- &::after {
343
- @extend %icon-clear;
339
+ &__placeholder {
340
+ &::after {
341
+ // Enforce actual line-height for positioning.
342
+ content: '\200b';
343
+ }
344
+ }
344
345
 
345
- font-size: 0.9em;
346
- color: $color-text-inverted;
346
+ &__select,
347
+ &__spinner {
348
+ padding: 0;
349
+ // $border-width to prevent masking border with &__spinner
350
+ top: $border-width;
351
+ right: $border-width;
352
+ bottom: $border-width;
353
+ height: inherit;
354
+ border-radius: $border-radius;
347
355
  }
348
356
 
349
- &:hover::after {
350
- color: $color-text;
357
+ &__select {
358
+ width: 0;
359
+ margin-right: calc($select-arrow-width / 2);
360
+
361
+ &::before {
362
+ @include arrow($select-arrow-size);
363
+
364
+ bottom: $select-arrow-bottom;
365
+ right: calc(-1 * $select-arrow-size / 2);
366
+ }
351
367
  }
352
- }
353
368
 
354
- .multiselect__option--selected {
355
- background: $color-highlight;
356
- color: $color-text;
357
- font-weight: normal;
369
+ &__spinner {
370
+ width: $spinner-width;
358
371
 
359
- &.multiselect__option--highlight {
360
- color: $color-text-inverted;
372
+ &::before,
373
+ &::after {
374
+ // Change the width of the loading spinner
375
+ border-width: 3px;
376
+ border-top-color: $color-active;
377
+ inset: 0;
378
+ margin: auto;
379
+ }
361
380
  }
362
- }
363
381
 
364
- .multiselect__tag,
365
- .multiselect__option--highlight {
366
- background: $color-active;
367
- color: $color-text-inverted;
368
- }
382
+ &__option {
383
+ $option: last-selector(&);
369
384
 
370
- .multiselect__tags,
371
- .multiselect__content-wrapper {
372
- border: $border-style;
373
- border-radius: $border-radius;
374
- }
385
+ min-height: unset;
386
+ height: unset;
387
+ line-height: $tag-line-height;
388
+ padding: $input-padding;
375
389
 
376
- &.dito-multiselect-single {
377
- --input-width: 100%;
378
- }
390
+ &::after {
391
+ // Instruction text for options
392
+ padding: $input-padding;
393
+ line-height: $tag-line-height;
394
+ }
379
395
 
380
- &.dito-multiselect-multiple {
381
- --input-width: auto;
382
- }
396
+ // Only show the highlight once the pulldown has received mouse or
397
+ // keyboard interaction, in which case `&--show-highlight` will be set,
398
+ // which is controlled by `pointerDirty` in vue-multiselect.
399
+ // Until then, clear the highlight style, but only if it isn't also
400
+ // disabled or selected, in which case we want to keep the style.
401
+ @at-root #{$self}:not(#{$self}--show-highlight)
402
+ #{$option}:not(#{$option}--disabled):not(#{$option}--selected) {
403
+ color: $color-text;
404
+ background: transparent;
405
+ }
383
406
 
384
- .multiselect--active {
385
- .multiselect__single,
386
- .multiselect__input {
387
- // Sadly, vue-select sets `style="width"` in addition to using classes
388
- // so `!important` is necessary:
389
- width: var(--input-width) !important;
390
- }
407
+ &--highlight {
408
+ &::after {
409
+ display: block;
410
+ position: absolute;
411
+ background: transparent;
412
+ color: $color-white;
413
+ }
414
+
415
+ @at-root #{$self}#{$self}--show-highlight #{last-selector(&)} {
416
+ color: $color-text-inverted;
417
+ background: $color-active;
418
+ }
419
+ }
420
+
421
+ &--selected {
422
+ font-weight: normal;
423
+ color: $color-text;
424
+ background: $color-highlight;
425
+
426
+ &#{$option}--highlight {
427
+ color: $color-text-inverted;
428
+ }
429
+ }
391
430
 
392
- .multiselect__tags {
393
- border-color: $color-active;
394
- border-bottom-left-radius: 0;
395
- border-bottom-right-radius: 0;
431
+ &--disabled {
432
+ background: none;
433
+ color: $color-disabled;
434
+ }
396
435
  }
397
436
 
398
- .multiselect__content-wrapper {
399
- border: $border-width solid $color-active;
400
- border-top-color: $border-color;
401
- margin: -1px 0 0;
402
- border-top-left-radius: 0;
403
- border-top-right-radius: 0;
437
+ &__tag {
438
+ color: $color-text-inverted;
439
+ background: $color-active;
404
440
  }
405
441
 
406
- &.multiselect--above {
407
- .multiselect__tags {
408
- border-radius: $border-radius;
409
- border-top-left-radius: 0;
410
- border-top-right-radius: 0;
442
+ &__tag-icon {
443
+ background: none;
444
+ border-radius: 1em;
445
+ width: $tag-icon-width;
446
+ margin: 0;
447
+
448
+ &::after {
449
+ @extend %icon-clear;
450
+
451
+ font-size: 0.9em;
452
+ color: $color-text-inverted;
411
453
  }
412
454
 
413
- .multiselect__content-wrapper {
414
- border: $border-width solid $color-active;
415
- border-bottom-color: $border-color;
416
- margin: 0 0 -1px;
417
- border-radius: $border-radius;
418
- border-bottom-left-radius: 0;
419
- border-bottom-right-radius: 0;
455
+ &:hover::after {
456
+ color: $color-text;
420
457
  }
421
458
  }
422
- }
423
459
 
424
- &.dito-has-errors {
425
- .multiselect__tags {
426
- border-color: $color-error;
460
+ &__tags,
461
+ &__content-wrapper {
462
+ border: $border-style;
463
+ border-radius: $border-radius;
427
464
  }
428
465
  }
429
-
430
- .dito-button-clear {
431
- width: $spinner-width;
432
- }
433
466
  }
434
467
  </style>
@@ -46,9 +46,11 @@ export default DitoTypeComponent.register(
46
46
 
47
47
  inputValue: {
48
48
  get() {
49
- return this.type === 'password' &&
49
+ return (
50
+ this.type === 'password' &&
50
51
  this.value === undefined &&
51
52
  !this.focused
53
+ )
52
54
  ? maskedPassword
53
55
  : this.value
54
56
  },
@@ -63,6 +63,7 @@ export const filterComponents = {
63
63
 
64
64
  export function getFiltersPanel(filters, dataPath, proxy) {
65
65
  const panel = {
66
+ type: 'panel',
66
67
  label: 'Filters',
67
68
  name: '$filters',
68
69
  target: dataPath,
@@ -125,6 +126,7 @@ function getFiltersComponents(filters) {
125
126
  ? filterComponents[type]?.(filter)
126
127
  : filter.components
127
128
  if (components) {
129
+ form.type = 'form'
128
130
  form.components = {}
129
131
  // Convert labels to placeholders:
130
132
  for (const [key, component] of Object.entries(components)) {