@ditojs/admin 2.4.3 → 2.5.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.4.3",
3
+ "version": "2.5.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,8 +33,8 @@
33
33
  "not ie_mob > 0"
34
34
  ],
35
35
  "dependencies": {
36
- "@ditojs/ui": "^2.4.1",
37
- "@ditojs/utils": "^2.4.1",
36
+ "@ditojs/ui": "^2.5.1",
37
+ "@ditojs/utils": "^2.5.0",
38
38
  "@kyvg/vue3-notification": "^2.9.0",
39
39
  "@lk77/vue3-color": "^3.0.6",
40
40
  "@tiptap/core": "^2.0.3",
@@ -58,7 +58,7 @@
58
58
  "@tiptap/extension-underline": "^2.0.3",
59
59
  "@tiptap/pm": "^2.0.3",
60
60
  "@tiptap/vue-3": "^2.0.3",
61
- "@vueuse/integrations": "^10.0.2",
61
+ "@vueuse/integrations": "^10.1.2",
62
62
  "codeflask": "^1.4.1",
63
63
  "filesize": "^10.0.7",
64
64
  "filesize-parser": "^1.5.0",
@@ -67,23 +67,23 @@
67
67
  "sortablejs": "^1.15.0",
68
68
  "tinycolor2": "^1.6.0",
69
69
  "tippy.js": "^6.3.7",
70
- "type-fest": "^3.8.0",
70
+ "type-fest": "^3.9.0",
71
71
  "vue": "^3.2.47",
72
72
  "vue-multiselect": "^3.0.0-beta.1",
73
73
  "vue-router": "^4.1.6",
74
74
  "vue-upload-component": "^3.1.8"
75
75
  },
76
76
  "devDependencies": {
77
- "@ditojs/build": "^2.4.1",
78
- "@vitejs/plugin-vue": "^4.1.0",
77
+ "@ditojs/build": "^2.5.0",
78
+ "@vitejs/plugin-vue": "^4.2.1",
79
79
  "@vue/compiler-sfc": "^3.2.47",
80
80
  "pug": "^3.0.2",
81
- "sass": "1.62.0",
81
+ "sass": "1.62.1",
82
82
  "typescript": "^5.0.4",
83
- "vite": "^4.3.1"
83
+ "vite": "^4.3.4"
84
84
  },
85
85
  "types": "types",
86
- "gitHead": "fa8525e80d6cce3844fb8c62d665c60761a564b1",
86
+ "gitHead": "18658b99dbb5f508c9d76811194831fb243f66ad",
87
87
  "scripts": {
88
88
  "build": "vite build",
89
89
  "watch": "yarn build --mode 'development' --watch",
@@ -29,7 +29,7 @@
29
29
  <script>
30
30
  import DitoComponent from '../DitoComponent.js'
31
31
  import { appendDataPath } from '../utils/data.js'
32
- import { hasSlotContent, hasVNodeContent } from '../utils/vue.js'
32
+ import { hasSlotContent, hasVNodeContent } from '@ditojs/ui/src'
33
33
 
34
34
  // @vue/component
35
35
  export default DitoComponent.component('DitoButtons', {
@@ -273,6 +273,8 @@ export default DitoComponent.component('DitoContainer', {
273
273
  // NOTE: This is not nested inside `.dito-container` so that other
274
274
  // type components can override `.dito-width-fill` class (filter precedence).
275
275
  .dito-component {
276
+ position: relative;
277
+
276
278
  &.dito-width-fill {
277
279
  width: 100%;
278
280
 
@@ -113,7 +113,8 @@ import {
113
113
  getNamedSchemas,
114
114
  getPanelEntries,
115
115
  setDefaultValues,
116
- processData
116
+ processData,
117
+ isForm
117
118
  } from '../utils/schema.js'
118
119
  import { getStoreAccessor } from '../utils/accessor.js'
119
120
 
@@ -159,6 +160,7 @@ export default DitoComponent.component('DitoSchema', {
159
160
  ? data(this.context)
160
161
  : data
161
162
  ),
163
+ currentPath: null,
162
164
  currentTab: null,
163
165
  componentsRegistry: {},
164
166
  panesRegistry: {},
@@ -339,7 +341,20 @@ export default DitoComponent.component('DitoSchema', {
339
341
  '$route.hash': {
340
342
  immediate: true,
341
343
  handler(hash) {
342
- if (this.hasTabs) {
344
+ // Remember the current path to know if tab changes should still be
345
+ // handled, but remove the trailing `/create` or `/:id` from it so that
346
+ // tabs informs that stay open after creation still work.
347
+ const getPath = () =>
348
+ isForm(this.schema)
349
+ ? this.$route.path.replace(/\/.\w*$/, '')
350
+ : this.$route.path
351
+ if (
352
+ this.hasTabs && (
353
+ !this.currentPath ||
354
+ this.currentPath === getPath()
355
+ )
356
+ ) {
357
+ this.currentPath = getPath()
343
358
  this.currentTab = hash?.slice(1) || null
344
359
  if (this.hasErrors) {
345
360
  this.repositionErrors()
@@ -559,7 +559,13 @@ export default {
559
559
  const inlined = isInlined(schema)
560
560
  if (inlined && schema.resource) {
561
561
  throw new Error(
562
- 'Lists with nested forms cannot load data from their own resources'
562
+ `Nested ${
563
+ this.isListSource
564
+ ? 'lists'
565
+ : this.isObjectSource
566
+ ? 'objects'
567
+ : 'schema'
568
+ } cannot load data from their own resources`
563
569
  )
564
570
  }
565
571
  // Use differently named url parameters on each nested level for id as
@@ -248,10 +248,23 @@ export default {
248
248
  },
249
249
 
250
250
  // @overridable
251
- async focusElement() {
252
- await this.focusSchema()
251
+ focusElement() {
252
+ const { element = this } = this.$refs
253
+ if (element.focus) {
254
+ element.focus()
255
+ } else {
256
+ element.$el?.focus?.()
257
+ }
258
+ },
259
+
260
+ // @overridable
261
+ blurElement() {
253
262
  const { element = this } = this.$refs
254
- ;(element.$el || element).focus?.()
263
+ if (element.blur) {
264
+ element.blur()
265
+ } else {
266
+ element.$el?.blur?.()
267
+ }
255
268
  },
256
269
 
257
270
  async focusSchema() {
@@ -260,14 +273,20 @@ export default {
260
273
  await this.tabComponent?.focus()
261
274
  },
262
275
 
263
- focus() {
276
+ async focus() {
277
+ await this.focusSchema()
264
278
  this.scrollIntoView()
265
279
  this.focusElement()
266
280
  },
267
281
 
282
+ blur() {
283
+ this.blurElement()
284
+ },
285
+
268
286
  clear() {
269
287
  this.value = null
270
288
  this.changedValue = undefined
289
+ this.blur()
271
290
  this.onChange()
272
291
  },
273
292
 
@@ -183,28 +183,21 @@
183
183
 
184
184
  .dito-button-overlay {
185
185
  position: absolute;
186
- right: $border-width;
187
- top: $border-width;
188
- bottom: $border-width;
186
+ inset: $border-width;
187
+ left: unset;
188
+ padding: 0;
189
189
  border: 0;
190
190
  border-radius: $border-radius;
191
191
  cursor: pointer;
192
192
 
193
193
  &.dito-button-clear {
194
- $self: &;
195
-
196
- width: $input-height;
194
+ width: 2em;
197
195
  z-index: 1;
198
196
  background: $color-white;
199
197
  display: none;
200
198
 
201
- @at-root .dito-component:hover {
202
- // Support two levels of nesting inside .dito-component, used by TypeColor
203
- & > #{$self},
204
- & > * > #{$self},
205
- & > * > * > #{$self} {
206
- display: block;
207
- }
199
+ @at-root .dito-component:hover:not(:has(.dito-component)) & {
200
+ display: block;
208
201
  }
209
202
 
210
203
  &::before {
@@ -105,13 +105,6 @@ export default DitoTypeComponent.register(
105
105
 
106
106
  span {
107
107
  @include ellipsis;
108
-
109
- #{$self}:not(.dito-width-grow) & {
110
- // Use `position: absolute` to prevent the text from growing over the
111
- // button's width, which would cause the button to grow as well.
112
- position: absolute;
113
- inset: 0;
114
- }
115
108
  }
116
109
  }
117
110
  }
@@ -94,6 +94,10 @@ export default DitoTypeComponent.register('code', {
94
94
  methods: {
95
95
  focusElement() {
96
96
  this.$el.querySelector('textarea')?.focus()
97
+ },
98
+
99
+ blurElement() {
100
+ this.$el.querySelector('textarea')?.focus()
97
101
  }
98
102
  }
99
103
  })
@@ -1,7 +1,7 @@
1
1
  <template lang="pug">
2
2
  Trigger.dito-color(
3
3
  v-model:show="showPopup"
4
- trigger="click"
4
+ trigger="focus"
5
5
  )
6
6
  template(#trigger)
7
7
  .dito-input(:class="{ 'dito-focus': showPopup }")
@@ -80,7 +80,10 @@ export default DitoTypeComponent.register('color', {
80
80
  // TODO: Fix side-effects
81
81
  // eslint-disable-next-line vue/no-side-effects-in-computed-properties
82
82
  this.convertedHexValue = null
83
- } else if (!this.focused) {
83
+ } else if (
84
+ !this.focused ||
85
+ this.$refs.element.hasAttribute('readonly')
86
+ ) {
84
87
  const color = tinycolor(this.value)
85
88
  if (color.isValid()) {
86
89
  // TODO: Fix side-effects
@@ -6,16 +6,23 @@
6
6
  ref="element"
7
7
  v-model="dateValue"
8
8
  :locale="locale"
9
- :dateFormat="{ ...api.formats.date, ...dateFormat }"
9
+ :format="formats"
10
10
  v-bind="attributes"
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
+ @mousedown.stop
18
+ )
12
19
  </template>
13
20
 
14
21
  <script>
15
22
  import DitoTypeComponent from '../DitoTypeComponent.js'
16
23
  import { getSchemaAccessor } from '../utils/accessor.js'
17
24
  import { DatePicker, TimePicker, DateTimePicker } from '@ditojs/ui/src'
18
- import { isDate } from '@ditojs/utils'
25
+ import { isDate, merge } from '@ditojs/utils'
19
26
 
20
27
  export default DitoTypeComponent.register(
21
28
  ['date', 'datetime', 'time'],
@@ -40,9 +47,18 @@ export default DitoTypeComponent.register(
40
47
  }
41
48
  },
42
49
 
43
- dateFormat: getSchemaAccessor('dateFormat', {
50
+ // TODO: Rename to `format`, once `schema.format()` was removed to
51
+ // `formatValue()`.
52
+ formats: getSchemaAccessor('formats', {
44
53
  type: Object,
45
- default: null
54
+ default: null,
55
+ get(formats) {
56
+ const { date, time } = merge({}, this.api.formats, formats)
57
+ return {
58
+ date: ['date', 'datetime'].includes(this.type) ? date : null,
59
+ time: ['time', 'datetime'].includes(this.type) ? time : null
60
+ }
61
+ }
46
62
  })
47
63
  },
48
64
 
@@ -66,14 +66,7 @@ import { ListItem } from '@tiptap/extension-list-item'
66
66
  import { History } from '@tiptap/extension-history'
67
67
 
68
68
  import { Icon } from '@ditojs/ui/src'
69
- import {
70
- isArray,
71
- isObject,
72
- underscore,
73
- hyphenate,
74
- debounce,
75
- camelize
76
- } from '@ditojs/utils'
69
+ import { isArray, isObject, hyphenate, debounce, camelize } from '@ditojs/utils'
77
70
 
78
71
  // @vue/component
79
72
  export default DitoTypeComponent.register('markup', {
@@ -120,10 +113,7 @@ export default DitoTypeComponent.register('markup', {
120
113
  basicNodeButtons() {
121
114
  return this.getButtons('nodes', {
122
115
  paragraph: {
123
- // Do not show the paragraph command as active if any of the block
124
- // commands are also active:
125
- ignoreActive: () =>
126
- this.otherNodeButtons.some(button => button.isActive)
116
+ command: 'setParagraph'
127
117
  },
128
118
  heading: {
129
119
  attribute: 'level',
@@ -132,7 +122,7 @@ export default DitoTypeComponent.register('markup', {
132
122
  })
133
123
  },
134
124
 
135
- otherNodeButtons() {
125
+ advancedNodeButtons() {
136
126
  return this.getButtons('nodes', {
137
127
  bulletList: true,
138
128
  orderedList: true,
@@ -149,17 +139,11 @@ export default DitoTypeComponent.register('markup', {
149
139
  },
150
140
 
151
141
  groupedButtons() {
152
- const {
153
- markButtons,
154
- basicNodeButtons,
155
- otherNodeButtons,
156
- toolButtons
157
- } = this
158
142
  return [
159
- markButtons,
160
- basicNodeButtons,
161
- otherNodeButtons,
162
- toolButtons
143
+ this.markButtons,
144
+ this.basicNodeButtons,
145
+ this.advancedNodeButtons,
146
+ this.toolButtons
163
147
  ].filter(buttons => buttons.length > 0)
164
148
  },
165
149
 
@@ -381,25 +365,22 @@ export default DitoTypeComponent.register('markup', {
381
365
  const list = []
382
366
  const { commands } = this.editor
383
367
 
384
- const addButton = ({ name, icon, attributes, ignoreActive, onClick }) => {
368
+ const addButton = ({ name, icon, command, attributes, onClick }) => {
385
369
  list.push({
386
370
  name,
387
371
  icon,
388
- isActive: (
389
- this.editor.isActive(name, attributes) &&
390
- (ignoreActive == null || !ignoreActive())
391
- ),
372
+ isActive: this.editor.isActive(name, attributes),
392
373
  onClick: () => {
393
- const key =
374
+ command ??=
394
375
  name in commands
395
376
  ? name
396
377
  : `toggle${camelize(name, true)}`
397
- if (key in commands) {
398
- const command = attributes =>
399
- this.editor.chain()[key](attributes).focus().run()
378
+ if (command in commands) {
379
+ const apply = attributes =>
380
+ this.editor.chain()[command](attributes).focus().run()
400
381
  onClick
401
382
  ? onClick(this.editor, attributes)
402
- : command(attributes)
383
+ : apply(attributes)
403
384
  }
404
385
  }
405
386
  })
@@ -407,16 +388,15 @@ export default DitoTypeComponent.register('markup', {
407
388
 
408
389
  const settings = this.schema[settingsName]
409
390
  if (settings) {
410
- for (const [key, description] of Object.entries(descriptions)) {
411
- const settingName = ['undo', 'redo'].includes(key) ? 'history' : key
391
+ for (const [name, description] of Object.entries(descriptions)) {
392
+ const settingName = ['undo', 'redo'].includes(name) ? 'history' : name
412
393
  const setting = settings[settingName]
413
- const name = underscore(key)
414
- const icon = hyphenate(key)
394
+ const icon = hyphenate(name)
415
395
  if (setting) {
416
396
  if (description === true) {
417
397
  addButton({ name, icon })
418
398
  } else if (isObject(description)) {
419
- const { attribute, values, ignoreActive, onClick } = description
399
+ const { command, attribute, values, onClick } = description
420
400
  if (attribute) {
421
401
  if (isArray(values) && isArray(setting)) {
422
402
  // Support heading level attrs:
@@ -425,15 +405,15 @@ export default DitoTypeComponent.register('markup', {
425
405
  addButton({
426
406
  name,
427
407
  icon: `${icon}-${value}`,
408
+ command,
428
409
  attributes: { [attribute]: value },
429
- ignoreActive,
430
410
  onClick
431
411
  })
432
412
  }
433
413
  }
434
414
  }
435
415
  } else {
436
- addButton({ name, icon, ignoreActive, onClick })
416
+ addButton({ name, icon, command, onClick })
437
417
  }
438
418
  }
439
419
  }
@@ -443,7 +423,11 @@ export default DitoTypeComponent.register('markup', {
443
423
  },
444
424
 
445
425
  focusElement() {
446
- this.editor.focus()
426
+ this.editor.commands.focus()
427
+ },
428
+
429
+ blurElement() {
430
+ this.editor.commands.blur()
447
431
  }
448
432
  }
449
433
  })
@@ -174,6 +174,10 @@ export default DitoTypeComponent.register('multiselect', {
174
174
  this.$refs.element.activate()
175
175
  },
176
176
 
177
+ blurElement() {
178
+ this.$refs.element.deactivate()
179
+ },
180
+
177
181
  onOpen() {
178
182
  this.populate = true
179
183
  },
@@ -257,10 +261,6 @@ $tag-line-height: 1em;
257
261
  margin-left: $form-spacing-half;
258
262
  }
259
263
 
260
- .dito-button-clear {
261
- width: $spinner-width;
262
- }
263
-
264
264
  .multiselect {
265
265
  $self: last-selector(&);
266
266
 
@@ -43,25 +43,22 @@ export const filterComponents = {
43
43
  },
44
44
 
45
45
  'date-range'() {
46
- // Use shorter date format in date-range filters:
47
- const dateFormat = {
48
- day: '2-digit',
49
- month: '2-digit',
50
- year: 'numeric'
46
+ const datetime = {
47
+ type: 'datetime',
48
+ width: '1/2',
49
+ formats: {
50
+ // Use shorter date format in date-range filters:
51
+ date: {
52
+ day: '2-digit',
53
+ month: '2-digit',
54
+ year: 'numeric'
55
+ }
56
+ },
57
+ clearable: true
51
58
  }
52
59
  return {
53
- from: {
54
- type: 'datetime',
55
- width: '1/2',
56
- dateFormat,
57
- clearable: true
58
- },
59
- to: {
60
- type: 'datetime',
61
- width: '1/2',
62
- dateFormat,
63
- clearable: true
64
- }
60
+ from: datetime,
61
+ to: datetime
65
62
  }
66
63
  }
67
64
  }
package/src/verbs.js CHANGED
@@ -1,10 +1,12 @@
1
1
  export default [
2
2
  'create', 'created',
3
3
  'save', 'saved',
4
+ 'apply', 'applied',
4
5
  'submit', 'submitted',
5
6
  'delete', 'deleted',
6
- 'edit', 'edited',
7
+ 'remove', 'removed',
7
8
  'clear', 'cleared',
9
+ 'edit', 'edited',
8
10
  'close', 'closed',
9
11
  'cancel', 'cancelled',
10
12
  'drag', 'dragged',
package/src/utils/vue.js DELETED
@@ -1,11 +0,0 @@
1
- import { useSlots, Comment } from 'vue'
2
- import { isString, asArray } from '@ditojs/utils'
3
-
4
- export function hasVNodeContent(vnode) {
5
- return vnode && asArray(vnode).some(vnode => vnode.type !== Comment)
6
- }
7
-
8
- export function hasSlotContent(slot, props = {}) {
9
- slot = isString(slot) ? useSlots()[slot] : slot
10
- return hasVNodeContent(slot?.(props))
11
- }