@ditojs/admin 2.86.0 → 2.88.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.
@@ -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
+ })