@ditojs/admin 2.93.0 → 2.95.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.
Files changed (47) hide show
  1. package/dist/dito-admin.css +1 -1
  2. package/dist/dito-admin.es.js +1672 -1522
  3. package/dist/dito-admin.umd.js +6 -6
  4. package/package.json +6 -6
  5. package/src/components/DitoAccount.vue +7 -1
  6. package/src/components/DitoCreateButton.vue +6 -2
  7. package/src/components/DitoDialog.vue +1 -1
  8. package/src/components/DitoErrors.vue +1 -0
  9. package/src/components/DitoForm.vue +1 -0
  10. package/src/components/DitoFormInner.vue +5 -0
  11. package/src/components/DitoHeader.vue +1 -1
  12. package/src/components/DitoLabel.vue +6 -1
  13. package/src/components/DitoMenu.vue +1 -0
  14. package/src/components/DitoNavigation.vue +1 -1
  15. package/src/components/DitoNotifications.vue +1 -1
  16. package/src/components/DitoPane.vue +3 -0
  17. package/src/components/DitoPanel.vue +2 -0
  18. package/src/components/DitoSchema.vue +1 -0
  19. package/src/components/DitoScopes.vue +1 -0
  20. package/src/components/DitoSpinner.vue +2 -0
  21. package/src/components/DitoTableHead.vue +10 -0
  22. package/src/components/DitoTabs.vue +18 -2
  23. package/src/components/DitoTrail.vue +2 -1
  24. package/src/components/DitoTreeItem.vue +22 -3
  25. package/src/mixins/DitoMixin.js +4 -2
  26. package/src/mixins/PulldownMixin.js +9 -0
  27. package/src/types/DitoTypeCheckboxes.vue +2 -0
  28. package/src/types/DitoTypeList.vue +2 -0
  29. package/src/types/DitoTypeMultiselect.vue +2 -0
  30. package/src/types/DitoTypeRadio.vue +2 -0
  31. package/src/types/DitoTypeUpload.vue +4 -4
  32. package/types/index.d.ts +74 -11
  33. package/types/tests/admin.test-d.ts +0 -27
  34. package/types/tests/component-buttons.test-d.ts +0 -44
  35. package/types/tests/component-list.test-d.ts +0 -159
  36. package/types/tests/component-misc.test-d.ts +0 -137
  37. package/types/tests/component-object.test-d.ts +0 -69
  38. package/types/tests/component-section.test-d.ts +0 -174
  39. package/types/tests/component-select.test-d.ts +0 -107
  40. package/types/tests/components.test-d.ts +0 -81
  41. package/types/tests/context.test-d.ts +0 -31
  42. package/types/tests/fixtures.ts +0 -24
  43. package/types/tests/form.test-d.ts +0 -109
  44. package/types/tests/instance.test-d.ts +0 -20
  45. package/types/tests/schema-features.test-d.ts +0 -402
  46. package/types/tests/variance.test-d.ts +0 -125
  47. package/types/tests/view.test-d.ts +0 -146
@@ -1,81 +0,0 @@
1
- import { expectTypeOf, assertType, describe, it } from 'vitest'
2
- import type { Component, Components, Form } from '../index.d.ts'
3
- import type { Entry, Parent, ParentWithMarkers } from './fixtures.ts'
4
-
5
- describe('Components', () => {
6
- it('accepts data-only components', () => {
7
- assertType<Components<Parent>>({
8
- title: { type: 'text' }
9
- })
10
- })
11
-
12
- it('infers array element type for list components', () => {
13
- assertType<Components<Parent>>({
14
- entries: {
15
- type: 'list',
16
- form: {
17
- type: 'form',
18
- components: {
19
- title: { type: 'text' }
20
- }
21
- } satisfies Form<Entry>
22
- }
23
- })
24
- })
25
-
26
- it('accepts UI-only keys for non-data components', () => {
27
- assertType<Components<ParentWithMarkers>>({
28
- viewButton: {
29
- type: 'button',
30
- text: 'View',
31
- events: {
32
- click({ item }) {
33
- expectTypeOf(item).not.toBeAny()
34
- expectTypeOf(item).toHaveProperty('title')
35
- expectTypeOf(item).toHaveProperty('entries')
36
- expectTypeOf(item).not.toHaveProperty('viewButton')
37
- expectTypeOf(item).not.toHaveProperty('spacer')
38
- }
39
- }
40
- }
41
- })
42
- })
43
-
44
- it('provides typed item in callbacks for data keys', () => {
45
- assertType<Components<Parent>>({
46
- title: {
47
- type: 'text',
48
- format({ item }) {
49
- expectTypeOf(item).not.toBeAny()
50
- expectTypeOf(item.title).toBeString()
51
- expectTypeOf(item.entries).toEqualTypeOf<Entry[]>()
52
- }
53
- }
54
- })
55
- })
56
- })
57
-
58
- describe('Components negative tests', () => {
59
- it('rejects unknown keys', () => {
60
- assertType<Components<Parent>>({
61
- title: { type: 'text' },
62
- // @ts-expect-error 'nonExistent' is not a key of Parent
63
- nonExistent: { type: 'text' }
64
- })
65
- })
66
- })
67
-
68
- describe('Components<any> compatibility', () => {
69
- it('Components<Specific> is assignable to Components<any>', () => {
70
- const specific: Components<Parent> = {
71
- title: { type: 'text' }
72
- }
73
- assertType<Components<any>>(specific)
74
- })
75
-
76
- it('Component<Specific> is assignable to Component<any>', () => {
77
- const specific: Component<Parent> = { type: 'text' }
78
- assertType<Component<any>>(specific)
79
- assertType<Component>(specific)
80
- })
81
- })
@@ -1,31 +0,0 @@
1
- import { expectTypeOf, assertType, describe, it } from 'vitest'
2
- import type { DitoContext, OrItemAccessor } from '../index.d.ts'
3
- import type { Entry, ParentWithMarkers } from './fixtures.ts'
4
-
5
- describe('DitoContext', () => {
6
- it('item strips never keys', () => {
7
- type Ctx = DitoContext<ParentWithMarkers>
8
- expectTypeOf<Ctx['item']>().not.toBeAny()
9
- expectTypeOf<Ctx['item']>().toHaveProperty('id')
10
- expectTypeOf<Ctx['item']>().toHaveProperty('title')
11
- expectTypeOf<Ctx['item']>().toHaveProperty('entries')
12
- expectTypeOf<Ctx['item']>().not.toHaveProperty('viewButton')
13
- expectTypeOf<Ctx['item']>().not.toHaveProperty('spacer')
14
- })
15
-
16
- it('item preserves data keys', () => {
17
- type Ctx = DitoContext<Entry>
18
- expectTypeOf<Ctx['item']>().not.toBeAny()
19
- expectTypeOf<Ctx['item']>().toEqualTypeOf<{
20
- id: number
21
- title: string
22
- }>()
23
- })
24
-
25
- it('OrItemAccessor accepts value or callback', () => {
26
- assertType<OrItemAccessor<Entry, {}, string>>('hello')
27
- assertType<OrItemAccessor<Entry, {}, string>>(
28
- (ctx: DitoContext<Entry>) => ctx.item.title
29
- )
30
- })
31
- })
@@ -1,24 +0,0 @@
1
- export type Entry = {
2
- id: number
3
- title: string
4
- }
5
-
6
- export type Parent = {
7
- id: number
8
- title: string
9
- entries: Entry[]
10
- }
11
-
12
- export type ParentWithMarkers = Parent & {
13
- viewButton: never
14
- spacer: never
15
- }
16
-
17
- export type Address = {
18
- street: string
19
- city: string
20
- }
21
-
22
- export type ParentWithAddress = Parent & {
23
- address: Address
24
- }
@@ -1,109 +0,0 @@
1
- import { expectTypeOf, assertType, describe, it } from 'vitest'
2
- import type { Form, ResolvableForm } from '../index.d.ts'
3
- import type { Entry, Parent } from './fixtures.ts'
4
-
5
- describe('Form assignability', () => {
6
- it('Form<Specific> is assignable to Form<any>', () => {
7
- const specific: Form<Entry> = {
8
- type: 'form',
9
- components: {
10
- title: { type: 'text' }
11
- }
12
- }
13
- assertType<Form<any>>(specific)
14
- })
15
-
16
- it('Form<Specific> is assignable to ResolvableForm', () => {
17
- const specific: Form<Entry> = {
18
- type: 'form',
19
- components: {
20
- title: { type: 'text' }
21
- }
22
- }
23
- assertType<ResolvableForm>(specific)
24
- })
25
-
26
- it('Record<string, Form<$Item>> is assignable to ResolvableForm<$Item>', () => {
27
- const module: Record<string, Form<Entry>> = {
28
- entryForm: { type: 'form', components: {} }
29
- }
30
- assertType<ResolvableForm<Entry>>(module)
31
- })
32
-
33
- it('Record<string, Form<WrongType>> is not assignable to ResolvableForm<$Item>', () => {
34
- const module: Record<string, Form<Parent>> = {
35
- parentForm: { type: 'form', components: {} }
36
- }
37
- // @ts-expect-error Form<Parent> should not be assignable to ResolvableForm<Entry>
38
- assertType<ResolvableForm<Entry>>(module)
39
- })
40
-
41
- it('() => import() pattern is assignable to ResolvableForm', () => {
42
- const importForm = () =>
43
- Promise.resolve({
44
- default: { type: 'form', components: {} } as Form<Entry>
45
- })
46
- assertType<ResolvableForm<Entry>>(importForm)
47
- })
48
-
49
- it('accepts event callbacks with typed item', () => {
50
- assertType<Form<Entry>>({
51
- type: 'form',
52
- events: {
53
- create({ item }) {
54
- expectTypeOf(item).not.toBeAny()
55
- expectTypeOf(item.title).toBeString()
56
- },
57
- submit({ item }) {
58
- expectTypeOf(item).not.toBeAny()
59
- expectTypeOf(item.id).toBeNumber()
60
- },
61
- error({ item, error }) {
62
- expectTypeOf(item).not.toBeAny()
63
- expectTypeOf(item.title).toBeString()
64
- expectTypeOf(error).not.toBeAny()
65
- expectTypeOf(error).toEqualTypeOf<Error>()
66
- }
67
- }
68
- })
69
- })
70
-
71
- it('accepts onCreate/onSubmit/onError top-level callbacks', () => {
72
- assertType<Form<Entry>>({
73
- type: 'form',
74
- onCreate({ item }) {
75
- expectTypeOf(item).not.toBeAny()
76
- expectTypeOf(item.title).toBeString()
77
- },
78
- onSubmit({ item }) {
79
- expectTypeOf(item).not.toBeAny()
80
- expectTypeOf(item.id).toBeNumber()
81
- },
82
- onError({ item, error }) {
83
- expectTypeOf(item).not.toBeAny()
84
- expectTypeOf(error).not.toBeAny()
85
- expectTypeOf(error).toEqualTypeOf<Error>()
86
- }
87
- })
88
- })
89
-
90
- it('typed Form variable used as form prop in list component', () => {
91
- const childForm: Form<Entry> = {
92
- type: 'form',
93
- components: {
94
- title: { type: 'text' }
95
- }
96
- }
97
-
98
- const parentForm: Form<Parent> = {
99
- type: 'form',
100
- components: {
101
- title: { type: 'text' },
102
- entries: {
103
- type: 'list',
104
- form: childForm
105
- }
106
- }
107
- }
108
- })
109
- })
@@ -1,20 +0,0 @@
1
- import { expectTypeOf, describe, it } from 'vitest'
2
- import type { DitoFormInstance, DitoContext } from '../index.d.ts'
3
- import type { Entry } from './fixtures.ts'
4
-
5
- describe('DitoComponentInstanceBase and DitoFormInstance', () => {
6
- it('DitoFormInstance extends base with item and form properties', () => {
7
- type Instance = DitoFormInstance<Entry>
8
- expectTypeOf<Instance['item']>().not.toBeAny()
9
- expectTypeOf<Instance['item']['title']>().toBeString()
10
- expectTypeOf<Instance['isCreating']>().toBeBoolean()
11
- expectTypeOf<Instance['submit']>().returns.toEqualTypeOf<Promise<boolean>>()
12
- })
13
-
14
- it('context is typed with item type', () => {
15
- type Instance = DitoFormInstance<Entry>
16
- expectTypeOf<Instance['context']>().not.toBeAny()
17
- expectTypeOf<Instance['context']>()
18
- .toEqualTypeOf<DitoContext<Entry>>()
19
- })
20
- })
@@ -1,402 +0,0 @@
1
- import { expectTypeOf, assertType, describe, it } from 'vitest'
2
- import type {
3
- Form,
4
- Components,
5
- PanelSchema,
6
- UploadSchema,
7
- DitoComponentInstance,
8
- HiddenSchema
9
- } from '../index.d.ts'
10
- import type { Entry, Parent } from './fixtures.ts'
11
-
12
- describe('SchemaFields: methods', () => {
13
- it('methods have `this` typed as component instance', () => {
14
- assertType<Form<Entry>>({
15
- type: 'form',
16
- components: {
17
- title: { type: 'text' }
18
- },
19
- methods: {
20
- greet() {
21
- expectTypeOf(this).not.toBeAny()
22
- expectTypeOf(this).toEqualTypeOf<DitoComponentInstance<Entry>>()
23
- expectTypeOf(this.item).not.toBeAny()
24
- expectTypeOf(this.item.title).toBeString()
25
- expectTypeOf(this.item.id).toBeNumber()
26
- },
27
- getTitle() {
28
- return this.item.title
29
- }
30
- }
31
- })
32
- })
33
- })
34
-
35
- describe('SchemaFields: computed', () => {
36
- it('computed getter uses `this` typing', () => {
37
- assertType<Form<Entry>>({
38
- type: 'form',
39
- components: {
40
- title: { type: 'text' }
41
- },
42
- computed: {
43
- upperTitle() {
44
- expectTypeOf(this).not.toBeAny()
45
- expectTypeOf(this.item).not.toBeAny()
46
- expectTypeOf(this.item.title).toBeString()
47
- return this.item.title.toUpperCase()
48
- }
49
- }
50
- })
51
- })
52
-
53
- it('computed getter/setter object uses `this` typing', () => {
54
- assertType<Form<Entry>>({
55
- type: 'form',
56
- components: {
57
- title: { type: 'text' }
58
- },
59
- computed: {
60
- upperTitle: {
61
- get() {
62
- expectTypeOf(this).not.toBeAny()
63
- expectTypeOf(this.item).not.toBeAny()
64
- expectTypeOf(this.item.title).toBeString()
65
- return this.item.title.toUpperCase()
66
- },
67
- set(_value) {
68
- expectTypeOf(this).not.toBeAny()
69
- expectTypeOf(this.item.id).toBeNumber()
70
- }
71
- }
72
- }
73
- })
74
- })
75
- })
76
-
77
- describe('SchemaFields: watch', () => {
78
- it('watch with plain handler function', () => {
79
- assertType<Form<Entry>>({
80
- type: 'form',
81
- components: {
82
- title: { type: 'text' }
83
- },
84
- watch: {
85
- title(value, oldValue) {
86
- expectTypeOf(this).not.toBeAny()
87
- expectTypeOf(this.item).not.toBeAny()
88
- expectTypeOf(this.item.title).toBeString()
89
- expectTypeOf(value).toBeAny()
90
- expectTypeOf(oldValue).toBeAny()
91
- }
92
- }
93
- })
94
- })
95
-
96
- it('watch with object containing deep and immediate', () => {
97
- assertType<Form<Entry>>({
98
- type: 'form',
99
- components: {
100
- title: { type: 'text' }
101
- },
102
- watch: {
103
- title: {
104
- handler(value, oldValue) {
105
- expectTypeOf(this).not.toBeAny()
106
- expectTypeOf(this.item).not.toBeAny()
107
- expectTypeOf(this.item.title).toBeString()
108
- expectTypeOf(value).toBeAny()
109
- expectTypeOf(oldValue).toBeAny()
110
- },
111
- deep: true,
112
- immediate: true
113
- }
114
- }
115
- })
116
- })
117
-
118
- it('watch as factory function returning handlers', () => {
119
- assertType<Form<Entry>>({
120
- type: 'form',
121
- components: {
122
- title: { type: 'text' }
123
- },
124
- watch() {
125
- expectTypeOf(this).not.toBeAny()
126
- expectTypeOf(this.item).not.toBeAny()
127
- expectTypeOf(this.item.title).toBeString()
128
- return {
129
- title(value) {
130
- expectTypeOf(this).not.toBeAny()
131
- expectTypeOf(this.item).not.toBeAny()
132
- expectTypeOf(this.item.id).toBeNumber()
133
- }
134
- }
135
- }
136
- })
137
- })
138
- })
139
-
140
- describe('SchemaFields: panels', () => {
141
- it('form with panels containing components and buttons', () => {
142
- assertType<Form<Entry>>({
143
- type: 'form',
144
- components: {
145
- title: { type: 'text' }
146
- },
147
- panels: {
148
- sidebar: {
149
- type: 'panel',
150
- label: 'Sidebar',
151
- components: {
152
- id: { type: 'number' }
153
- },
154
- buttons: {
155
- save: { text: 'Save' }
156
- },
157
- panelButtons: {
158
- refresh: { text: 'Refresh' }
159
- }
160
- }
161
- }
162
- })
163
- })
164
- })
165
-
166
- describe('SchemaFields: lifecycle callbacks', () => {
167
- it('onInitialize and onChange receive typed context', () => {
168
- assertType<Form<Entry>>({
169
- type: 'form',
170
- components: {
171
- title: { type: 'text' }
172
- },
173
- onInitialize({ item }) {
174
- expectTypeOf(item).not.toBeAny()
175
- expectTypeOf(item.title).toBeString()
176
- expectTypeOf(item.id).toBeNumber()
177
- },
178
- onChange({ item }) {
179
- expectTypeOf(item).not.toBeAny()
180
- expectTypeOf(item.title).toBeString()
181
- }
182
- })
183
- })
184
- })
185
-
186
- describe('SchemaTypeMixin: parse and process callbacks', () => {
187
- it('parse callback receives typed item', () => {
188
- assertType<Components<Parent>>({
189
- title: {
190
- type: 'text',
191
- parse({ item }) {
192
- expectTypeOf(item).not.toBeAny()
193
- expectTypeOf(item.title).toBeString()
194
- expectTypeOf(item.entries).toEqualTypeOf<Entry[]>()
195
- }
196
- }
197
- })
198
- })
199
-
200
- it('process callback receives typed item', () => {
201
- assertType<Components<Parent>>({
202
- title: {
203
- type: 'text',
204
- process({ item }) {
205
- expectTypeOf(item).not.toBeAny()
206
- expectTypeOf(item.title).toBeString()
207
- return item.title.trim()
208
- }
209
- }
210
- })
211
- })
212
- })
213
-
214
- describe('SchemaDataMixin: data callback', () => {
215
- it('data callback receives typed item on HiddenSchema', () => {
216
- assertType<HiddenSchema<Entry>>({
217
- type: 'hidden',
218
- data({ item }) {
219
- expectTypeOf(item).not.toBeAny()
220
- expectTypeOf(item.title).toBeString()
221
- expectTypeOf(item.id).toBeNumber()
222
- return { computed: item.title }
223
- }
224
- })
225
- })
226
- })
227
-
228
- describe('UploadSchema', () => {
229
- it('accepts multiple, extensions, accept, and maxSize', () => {
230
- assertType<Components<Parent>>({
231
- entries: {
232
- type: 'upload',
233
- multiple: true,
234
- extensions: ['jpg', 'png', 'gif'],
235
- accept: ['image/png', 'image/jpeg'],
236
- maxSize: '5mb'
237
- }
238
- })
239
- })
240
-
241
- it('accepts extensions as regex', () => {
242
- assertType<Components<Parent>>({
243
- entries: {
244
- type: 'upload',
245
- extensions: /\.(gif|jpe?g|png)$/i
246
- }
247
- })
248
- })
249
-
250
- it('accepts thumbnails and downloadUrl as callbacks', () => {
251
- assertType<UploadSchema<Entry>>({
252
- type: 'upload',
253
- thumbnails({ item }) {
254
- expectTypeOf(item).not.toBeAny()
255
- expectTypeOf(item.title).toBeString()
256
- return true
257
- },
258
- downloadUrl({ item }) {
259
- expectTypeOf(item).not.toBeAny()
260
- expectTypeOf(item.id).toBeNumber()
261
- return `/files/${item.id}`
262
- }
263
- })
264
- })
265
-
266
- it('accepts thumbnailUrl and render as callbacks', () => {
267
- assertType<UploadSchema<Entry>>({
268
- type: 'upload',
269
- thumbnailUrl({ item }) {
270
- expectTypeOf(item).not.toBeAny()
271
- expectTypeOf(item.title).toBeString()
272
- return `/thumbs/${item.id}`
273
- },
274
- render({ item }) {
275
- expectTypeOf(item).not.toBeAny()
276
- return item.title
277
- }
278
- })
279
- })
280
-
281
- it('accepts draggable and deletable as callbacks', () => {
282
- assertType<UploadSchema<Entry>>({
283
- type: 'upload',
284
- draggable({ item }) {
285
- expectTypeOf(item).not.toBeAny()
286
- expectTypeOf(item.id).toBeNumber()
287
- return true
288
- },
289
- deletable({ item }) {
290
- expectTypeOf(item).not.toBeAny()
291
- expectTypeOf(item.title).toBeString()
292
- return false
293
- }
294
- })
295
- })
296
- })
297
-
298
- describe('PanelSchema', () => {
299
- it('constrains component keys to item properties', () => {
300
- assertType<PanelSchema<Entry>>({
301
- type: 'panel',
302
- components: {
303
- id: { type: 'number' },
304
- title: { type: 'text' }
305
- }
306
- })
307
- })
308
-
309
- it('rejects unknown component keys', () => {
310
- assertType<PanelSchema<Entry>>({
311
- type: 'panel',
312
- components: {
313
- // @ts-expect-error — 'extra' is not a key of Entry
314
- extra: { type: 'text' }
315
- }
316
- })
317
- })
318
-
319
- it('accepts buttons and panelButtons', () => {
320
- assertType<PanelSchema<Entry>>({
321
- type: 'panel',
322
- buttons: {
323
- save: {
324
- text: 'Save',
325
- events: {
326
- click({ item }) {
327
- expectTypeOf(item).not.toBeAny()
328
- expectTypeOf(item.title).toBeString()
329
- }
330
- }
331
- }
332
- },
333
- panelButtons: {
334
- refresh: { text: 'Refresh' }
335
- }
336
- })
337
- })
338
-
339
- it('accepts sticky as callback', () => {
340
- assertType<PanelSchema<Entry>>({
341
- type: 'panel',
342
- sticky({ item }) {
343
- expectTypeOf(item).not.toBeAny()
344
- expectTypeOf(item.id).toBeNumber()
345
- return true
346
- }
347
- })
348
- })
349
-
350
- it('component callbacks in panels receive typed item', () => {
351
- assertType<PanelSchema<Parent>>({
352
- type: 'panel',
353
- components: {
354
- title: {
355
- type: 'text',
356
- format({ item }) {
357
- expectTypeOf(item).not.toBeAny()
358
- expectTypeOf(item.title).toBeString()
359
- expectTypeOf(item.entries)
360
- .toEqualTypeOf<Entry[]>()
361
- return ''
362
- }
363
- }
364
- }
365
- })
366
- })
367
-
368
- it('panels on Form inherit item type', () => {
369
- assertType<Form<Parent>>({
370
- type: 'form',
371
- components: {
372
- title: { type: 'text' }
373
- },
374
- panels: {
375
- info: {
376
- type: 'panel',
377
- components: {
378
- title: {
379
- type: 'text',
380
- format({ item }) {
381
- expectTypeOf(item).not.toBeAny()
382
- expectTypeOf(item.title).toBeString()
383
- return ''
384
- }
385
- }
386
- },
387
- buttons: {
388
- save: {
389
- text: 'Save',
390
- events: {
391
- click({ item }) {
392
- expectTypeOf(item).not.toBeAny()
393
- expectTypeOf(item.id).toBeNumber()
394
- }
395
- }
396
- }
397
- }
398
- }
399
- }
400
- })
401
- })
402
- })