@ditojs/admin 2.85.2 → 2.87.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/README.md +4 -4
- package/dist/dito-admin.es.js +14 -14
- package/dist/dito-admin.umd.js +2 -2
- package/package.json +12 -12
- package/types/index.d.ts +2535 -431
- package/types/tests/admin.test-d.ts +27 -0
- package/types/tests/component-buttons.test-d.ts +44 -0
- package/types/tests/component-list.test-d.ts +159 -0
- package/types/tests/component-misc.test-d.ts +137 -0
- package/types/tests/component-object.test-d.ts +69 -0
- package/types/tests/component-section.test-d.ts +174 -0
- package/types/tests/component-select.test-d.ts +107 -0
- package/types/tests/components.test-d.ts +81 -0
- package/types/tests/context.test-d.ts +31 -0
- package/types/tests/fixtures.ts +24 -0
- package/types/tests/form.test-d.ts +109 -0
- package/types/tests/instance.test-d.ts +20 -0
- package/types/tests/schema-features.test-d.ts +402 -0
- package/types/tests/variance.test-d.ts +125 -0
- package/types/tests/view.test-d.ts +146 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { expectTypeOf, assertType, describe, it } from 'vitest'
|
|
2
|
+
import type { SelectSchema, MultiselectSchema } from '../index.d.ts'
|
|
3
|
+
import type { Entry } from './fixtures.ts'
|
|
4
|
+
|
|
5
|
+
describe('SelectSchema', () => {
|
|
6
|
+
it('accepts simple string array options', () => {
|
|
7
|
+
assertType<SelectSchema<Entry>>({
|
|
8
|
+
type: 'select',
|
|
9
|
+
options: ['draft', 'published', 'archived']
|
|
10
|
+
})
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('accepts label/value object options', () => {
|
|
14
|
+
assertType<SelectSchema<Entry>>({
|
|
15
|
+
type: 'select',
|
|
16
|
+
options: [
|
|
17
|
+
{ label: 'Draft', value: 'draft' },
|
|
18
|
+
{ label: 'Published', value: 'published' }
|
|
19
|
+
]
|
|
20
|
+
})
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('accepts options with data callback', () => {
|
|
24
|
+
assertType<SelectSchema<Entry>>({
|
|
25
|
+
type: 'select',
|
|
26
|
+
options: {
|
|
27
|
+
data({ item }) {
|
|
28
|
+
expectTypeOf(item).not.toBeAny()
|
|
29
|
+
expectTypeOf(item).toMatchTypeOf<Entry>()
|
|
30
|
+
return [
|
|
31
|
+
{ label: 'Option A', value: 1 },
|
|
32
|
+
{ label: 'Option B', value: 2 }
|
|
33
|
+
]
|
|
34
|
+
},
|
|
35
|
+
label: 'label',
|
|
36
|
+
value: 'value'
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('accepts relate and relateBy', () => {
|
|
42
|
+
assertType<SelectSchema<Entry>>({
|
|
43
|
+
type: 'select',
|
|
44
|
+
relate: true,
|
|
45
|
+
relateBy: 'id'
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
describe('MultiselectSchema', () => {
|
|
51
|
+
it('accepts multiple, searchable, taggable', () => {
|
|
52
|
+
assertType<MultiselectSchema<Entry>>({
|
|
53
|
+
type: 'multiselect',
|
|
54
|
+
multiple: true,
|
|
55
|
+
searchable: true,
|
|
56
|
+
taggable: true,
|
|
57
|
+
stayOpen: true
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('accepts typed $Option with search callback', () => {
|
|
62
|
+
type Tag = { id: number; name: string }
|
|
63
|
+
|
|
64
|
+
assertType<MultiselectSchema<Entry, Tag>>({
|
|
65
|
+
type: 'multiselect',
|
|
66
|
+
searchable: true,
|
|
67
|
+
search({ item, query }) {
|
|
68
|
+
expectTypeOf(item).not.toBeAny()
|
|
69
|
+
expectTypeOf(item).toMatchTypeOf<Entry>()
|
|
70
|
+
expectTypeOf(query).not.toBeAny()
|
|
71
|
+
expectTypeOf(query).toBeString()
|
|
72
|
+
return [] as Tag[]
|
|
73
|
+
},
|
|
74
|
+
options: {
|
|
75
|
+
data: [] as Tag[],
|
|
76
|
+
label: 'name',
|
|
77
|
+
value: 'id'
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('accepts label/value as accessor callbacks with typed option', () => {
|
|
83
|
+
type Tag = { id: number; name: string }
|
|
84
|
+
|
|
85
|
+
assertType<MultiselectSchema<Entry, Tag>>({
|
|
86
|
+
type: 'multiselect',
|
|
87
|
+
options: {
|
|
88
|
+
data: [] as Tag[],
|
|
89
|
+
label({ option }) {
|
|
90
|
+
expectTypeOf(option).not.toBeAny()
|
|
91
|
+
return option.name
|
|
92
|
+
},
|
|
93
|
+
value({ option }) {
|
|
94
|
+
expectTypeOf(option).not.toBeAny()
|
|
95
|
+
return option.id
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
})
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('rejects invalid type', () => {
|
|
102
|
+
assertType<MultiselectSchema<Entry>>({
|
|
103
|
+
// @ts-expect-error 'radio' is not assignable to 'multiselect'
|
|
104
|
+
type: 'radio'
|
|
105
|
+
})
|
|
106
|
+
})
|
|
107
|
+
})
|
|
@@ -0,0 +1,81 @@
|
|
|
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
|
+
})
|
|
@@ -0,0 +1,31 @@
|
|
|
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
|
+
})
|
|
@@ -0,0 +1,24 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
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
|
+
})
|
|
@@ -0,0 +1,20 @@
|
|
|
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
|
+
})
|