@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.
@@ -0,0 +1,27 @@
1
+ import { assertType, describe, it } from 'vitest'
2
+ import type { default as DitoAdmin, View } from '../index.d.ts'
3
+ import type { Entry } from './fixtures.ts'
4
+
5
+ describe('DitoAdmin', () => {
6
+ it('constructor accepts element and views option', () => {
7
+ assertType<ConstructorParameters<typeof DitoAdmin>>([
8
+ document.body,
9
+ {
10
+ views: {
11
+ entries: {
12
+ type: 'view',
13
+ component: {
14
+ type: 'list',
15
+ form: {
16
+ type: 'form',
17
+ components: {
18
+ title: { type: 'text' }
19
+ }
20
+ }
21
+ }
22
+ } satisfies View<Entry>
23
+ }
24
+ }
25
+ ])
26
+ })
27
+ })
@@ -0,0 +1,44 @@
1
+ import { expectTypeOf, assertType, describe, it } from 'vitest'
2
+ import type { Buttons } from '../index.d.ts'
3
+ import type { Entry } from './fixtures.ts'
4
+
5
+ describe('Buttons', () => {
6
+ it('accepts button without type', () => {
7
+ assertType<Buttons<Entry>>({
8
+ save: { text: 'Save' }
9
+ })
10
+ })
11
+
12
+ it('accepts button with type', () => {
13
+ assertType<Buttons<Entry>>({
14
+ save: { type: 'button', text: 'Save' }
15
+ })
16
+ })
17
+
18
+ it('click callback receives typed item', () => {
19
+ assertType<Buttons<Entry>>({
20
+ save: {
21
+ text: 'Save',
22
+ events: {
23
+ click({ item }) {
24
+ expectTypeOf(item).not.toBeAny()
25
+ expectTypeOf(item).toHaveProperty('id')
26
+ expectTypeOf(item).toHaveProperty('title')
27
+ expectTypeOf(item.id).toBeNumber()
28
+ expectTypeOf(item.title).toBeString()
29
+ }
30
+ }
31
+ }
32
+ })
33
+ })
34
+
35
+ it('rejects invalid button type', () => {
36
+ assertType<Buttons<Entry>>({
37
+ save: {
38
+ // @ts-expect-error 'text' is not a valid button type
39
+ type: 'text',
40
+ text: 'Save'
41
+ }
42
+ })
43
+ })
44
+ })
@@ -0,0 +1,159 @@
1
+ import { expectTypeOf, assertType, describe, it } from 'vitest'
2
+ import type {
3
+ Components,
4
+ Form,
5
+ ListSchema,
6
+ ColumnSchema,
7
+ DitoContext
8
+ } from '../index.d.ts'
9
+ import type { Entry, Parent } from './fixtures.ts'
10
+
11
+ describe('ListSchema', () => {
12
+ it('accepts form components typed against $Item', () => {
13
+ assertType<ListSchema<Entry>>({
14
+ type: 'list',
15
+ form: {
16
+ type: 'form',
17
+ components: {
18
+ title: { type: 'text' }
19
+ }
20
+ }
21
+ })
22
+ })
23
+
24
+ it('accepts columns as a record of ColumnSchema', () => {
25
+ assertType<ListSchema<Entry>>({
26
+ type: 'list',
27
+ columns: {
28
+ title: {
29
+ label: 'Title',
30
+ sortable: true
31
+ } satisfies ColumnSchema<Entry>
32
+ }
33
+ })
34
+ })
35
+
36
+ it('column render callback receives typed value', () => {
37
+ assertType<ListSchema<Entry>>({
38
+ type: 'list',
39
+ columns: {
40
+ title: {
41
+ render({ value }) {
42
+ expectTypeOf(value).not.toBeAny()
43
+ expectTypeOf(value).toBeString()
44
+ return value
45
+ }
46
+ }
47
+ }
48
+ })
49
+ })
50
+
51
+ it('accepts columns as an array of item keys', () => {
52
+ assertType<ListSchema<Entry>>({
53
+ type: 'list',
54
+ columns: ['title', 'id']
55
+ })
56
+ })
57
+
58
+ it('rejects unknown keys in columns array', () => {
59
+ assertType<ListSchema<Entry>>({
60
+ type: 'list',
61
+ // @ts-expect-error 'missing' is not a key of Entry
62
+ columns: ['title', 'missing']
63
+ })
64
+ })
65
+
66
+ it('accepts creatable as boolean or callback', () => {
67
+ assertType<ListSchema<Entry>>({
68
+ type: 'list',
69
+ creatable: true
70
+ })
71
+
72
+ assertType<ListSchema<Entry>>({
73
+ type: 'list',
74
+ creatable(ctx) {
75
+ expectTypeOf(ctx).not.toBeAny()
76
+ expectTypeOf(ctx).toMatchTypeOf<DitoContext<Entry>>()
77
+ expectTypeOf(ctx.item.title).toBeString()
78
+ return true
79
+ }
80
+ })
81
+ })
82
+
83
+ it('types list correctly inside Components', () => {
84
+ assertType<Components<Parent>>({
85
+ entries: {
86
+ type: 'list',
87
+ form: {
88
+ type: 'form',
89
+ components: {
90
+ title: { type: 'text' }
91
+ }
92
+ } satisfies Form<Entry>
93
+ }
94
+ })
95
+ })
96
+
97
+ it('rejects wrong form type via typed variable', () => {
98
+ const parentForm: Form<Parent> = {
99
+ type: 'form',
100
+ components: {
101
+ title: { type: 'text' },
102
+ entries: { type: 'list' }
103
+ }
104
+ }
105
+
106
+ assertType<ListSchema<Entry>>({
107
+ type: 'list',
108
+ // @ts-expect-error Form<Parent> is not assignable to ResolvableForm<Entry>
109
+ form: parentForm
110
+ })
111
+ })
112
+
113
+ it('accepts draggable as OrItemAccessor', () => {
114
+ assertType<ListSchema<Entry>>({
115
+ type: 'list',
116
+ draggable(ctx) {
117
+ expectTypeOf(ctx).not.toBeAny()
118
+ expectTypeOf(ctx).toMatchTypeOf<DitoContext<Entry>>()
119
+ return true
120
+ }
121
+ })
122
+ })
123
+
124
+ it('accepts scopes as string array or object', () => {
125
+ assertType<ListSchema<Entry>>({
126
+ type: 'list',
127
+ scopes: ['active', 'archived']
128
+ })
129
+
130
+ assertType<ListSchema<Entry>>({
131
+ type: 'list',
132
+ scopes: {
133
+ active: { label: 'Active', defaultScope: true },
134
+ archived: 'Archived'
135
+ }
136
+ })
137
+ })
138
+
139
+ it('accepts itemLabel as string, callback, or false', () => {
140
+ assertType<ListSchema<Entry>>({
141
+ type: 'list',
142
+ itemLabel: 'title'
143
+ })
144
+
145
+ assertType<ListSchema<Entry>>({
146
+ type: 'list',
147
+ itemLabel: false
148
+ })
149
+
150
+ assertType<ListSchema<Entry>>({
151
+ type: 'list',
152
+ itemLabel({ item }) {
153
+ expectTypeOf(item).not.toBeAny()
154
+ expectTypeOf(item.title).toBeString()
155
+ return item.title
156
+ }
157
+ })
158
+ })
159
+ })
@@ -0,0 +1,137 @@
1
+ import { expectTypeOf, assertType, describe, it } from 'vitest'
2
+ import type { MenuSchema, View, NumberSchema, Form } from '../index.d.ts'
3
+ import type { Entry } from './fixtures.ts'
4
+
5
+ describe('MenuSchema', () => {
6
+ it('accepts menu with items containing views', () => {
7
+ assertType<MenuSchema<Entry>>({
8
+ type: 'menu',
9
+ label: 'Main Menu',
10
+ name: 'mainMenu',
11
+ items: {
12
+ entries: {
13
+ type: 'view',
14
+ component: {
15
+ type: 'list',
16
+ form: {
17
+ type: 'form',
18
+ components: {
19
+ title: { type: 'text' }
20
+ }
21
+ }
22
+ }
23
+ }
24
+ }
25
+ })
26
+ })
27
+
28
+ it('View union accepts both ViewSchema and MenuSchema', () => {
29
+ const viewSchema: View<Entry> = {
30
+ type: 'view',
31
+ components: {
32
+ title: { type: 'text' }
33
+ }
34
+ }
35
+
36
+ const menuSchema: View<Entry> = {
37
+ type: 'menu',
38
+ label: 'Group',
39
+ items: {
40
+ sub: {
41
+ type: 'view',
42
+ components: {
43
+ title: { type: 'text' }
44
+ }
45
+ }
46
+ }
47
+ }
48
+
49
+ assertType<View<Entry>>(viewSchema)
50
+ assertType<View<Entry>>(menuSchema)
51
+ })
52
+ })
53
+
54
+ describe('NumberSchema', () => {
55
+ it('accepts min, max, step, decimals', () => {
56
+ assertType<NumberSchema<Entry>>({
57
+ type: 'number',
58
+ min: 0,
59
+ max: 100,
60
+ step: 0.5,
61
+ decimals: 2
62
+ })
63
+ })
64
+
65
+ it('accepts range as tuple', () => {
66
+ assertType<NumberSchema<Entry>>({
67
+ type: 'integer',
68
+ range: [0, 100]
69
+ })
70
+ })
71
+
72
+ it('accepts rules including integer', () => {
73
+ assertType<NumberSchema<Entry>>({
74
+ type: 'number',
75
+ rules: {
76
+ min: 0,
77
+ max: 999,
78
+ integer: true
79
+ }
80
+ })
81
+ })
82
+ })
83
+
84
+ describe('SchemaAffixMixin (prefix/suffix)', () => {
85
+ it('accepts prefix as string', () => {
86
+ assertType<NumberSchema<Entry>>({
87
+ type: 'number',
88
+ prefix: '$'
89
+ })
90
+ })
91
+
92
+ it('accepts prefix as array of SchemaAffix objects', () => {
93
+ assertType<NumberSchema<Entry>>({
94
+ type: 'number',
95
+ prefix: [
96
+ { type: 'icon', text: 'currency' },
97
+ { html: '<b>$</b>' }
98
+ ]
99
+ })
100
+ })
101
+
102
+ it('accepts suffix with conditional if callback', () => {
103
+ assertType<NumberSchema<Entry>>({
104
+ type: 'number',
105
+ suffix: {
106
+ text: '%',
107
+ if({ item }) {
108
+ expectTypeOf(item).not.toBeAny()
109
+ return item.id > 0
110
+ }
111
+ }
112
+ })
113
+ })
114
+ })
115
+
116
+ describe('ClipboardConfig', () => {
117
+ it('accepts copy/paste callbacks on a form', () => {
118
+ assertType<Form<Entry>>({
119
+ type: 'form',
120
+ clipboard: {
121
+ copy(context) {
122
+ expectTypeOf(context).not.toBeAny()
123
+ expectTypeOf(context.item).not.toBeAny()
124
+ return context.item
125
+ },
126
+ paste(context) {
127
+ expectTypeOf(context).not.toBeAny()
128
+ expectTypeOf(context.item).not.toBeAny()
129
+ return context.item
130
+ }
131
+ },
132
+ components: {
133
+ title: { type: 'text' }
134
+ }
135
+ })
136
+ })
137
+ })
@@ -0,0 +1,69 @@
1
+ import { expectTypeOf, assertType, describe, it } from 'vitest'
2
+ import type {
3
+ ObjectSchema,
4
+ TreeListSchema,
5
+ Form,
6
+ Components
7
+ } from '../index.d.ts'
8
+ import type { Address, Entry, ParentWithAddress } from './fixtures.ts'
9
+
10
+ describe('ObjectSchema', () => {
11
+ it('accepts typed form', () => {
12
+ assertType<ObjectSchema<Address>>({
13
+ type: 'object',
14
+ form: {
15
+ type: 'form',
16
+ components: {
17
+ street: { type: 'text' },
18
+ city: { type: 'text' }
19
+ }
20
+ } satisfies Form<Address>
21
+ })
22
+ })
23
+
24
+ it('accepts typed columns', () => {
25
+ assertType<ObjectSchema<Address>>({
26
+ type: 'object',
27
+ columns: {
28
+ street: { label: 'Street' },
29
+ city: { label: 'City' }
30
+ }
31
+ })
32
+ })
33
+
34
+ it('rejects unknown key in columns array', () => {
35
+ assertType<ObjectSchema<Address>>({
36
+ type: 'object',
37
+ // @ts-expect-error 'zipCode' is not a key of Address
38
+ columns: ['street', 'zipCode']
39
+ })
40
+ })
41
+
42
+ it('works inside Components for a data key', () => {
43
+ assertType<Components<ParentWithAddress>>({
44
+ address: {
45
+ type: 'object',
46
+ form: {
47
+ type: 'form',
48
+ components: {
49
+ title: { type: 'text' }
50
+ }
51
+ } satisfies Form<ParentWithAddress>
52
+ }
53
+ })
54
+ })
55
+ })
56
+
57
+ describe('TreeListSchema', () => {
58
+ it('accepts typed form', () => {
59
+ assertType<TreeListSchema<Entry>>({
60
+ type: 'tree-list',
61
+ form: {
62
+ type: 'form',
63
+ components: {
64
+ title: { type: 'text' }
65
+ }
66
+ } satisfies Form<Entry>
67
+ })
68
+ })
69
+ })
@@ -0,0 +1,174 @@
1
+ import { expectTypeOf, assertType, describe, it } from 'vitest'
2
+ import type { Components, Form } from '../index.d.ts'
3
+ import type { Address, Entry, Parent, ParentWithAddress } from './fixtures.ts'
4
+
5
+ type ParentWithSection = Parent & {
6
+ details: never
7
+ }
8
+
9
+ describe('Section components', () => {
10
+ it('accepts section with nested components', () => {
11
+ assertType<Components<ParentWithSection>>({
12
+ details: {
13
+ type: 'section',
14
+ components: {
15
+ title: { type: 'text' }
16
+ }
17
+ }
18
+ })
19
+ })
20
+
21
+ it('provides typed item in section callbacks', () => {
22
+ assertType<Components<ParentWithSection>>({
23
+ details: {
24
+ type: 'section',
25
+ label({ item }) {
26
+ expectTypeOf(item).not.toBeAny()
27
+ expectTypeOf(item.title).toBeString()
28
+ expectTypeOf(item.entries).toEqualTypeOf<Entry[]>()
29
+ expectTypeOf(item).not.toHaveProperty('details')
30
+ return item.title
31
+ }
32
+ }
33
+ })
34
+ })
35
+
36
+ it('rejects invalid component keys in section', () => {
37
+ assertType<Components<ParentWithSection>>({
38
+ details: {
39
+ type: 'section',
40
+ components: {
41
+ title: { type: 'text' },
42
+ // @ts-expect-error 'nonExistent' is not a key of ParentWithSection
43
+ nonExistent: { type: 'text' }
44
+ }
45
+ }
46
+ })
47
+ })
48
+
49
+ it('accepts section with form prop', () => {
50
+ const sectionForm: Form<ParentWithSection> = {
51
+ type: 'form',
52
+ components: {
53
+ title: { type: 'text' }
54
+ }
55
+ }
56
+
57
+ assertType<Components<ParentWithSection>>({
58
+ details: {
59
+ type: 'section',
60
+ form: sectionForm
61
+ }
62
+ })
63
+ })
64
+ })
65
+
66
+ describe('Section tabs', () => {
67
+ it('accepts tabs on a non-nested section', () => {
68
+ assertType<Components<ParentWithSection>>({
69
+ details: {
70
+ type: 'section',
71
+ tabs: {
72
+ general: {
73
+ type: 'tab',
74
+ components: {
75
+ title: { type: 'text' }
76
+ }
77
+ }
78
+ }
79
+ }
80
+ })
81
+ })
82
+
83
+ it('accepts tabs on a nested section typed against nested data', () => {
84
+ assertType<Components<ParentWithAddress>>({
85
+ address: {
86
+ type: 'section',
87
+ nested: true,
88
+ tabs: {
89
+ main: {
90
+ type: 'tab',
91
+ components: {
92
+ street: { type: 'text' },
93
+ city: { type: 'text' }
94
+ }
95
+ }
96
+ }
97
+ }
98
+ })
99
+ })
100
+ })
101
+
102
+ describe('Nested sections', () => {
103
+ it('types nested section components against the value type', () => {
104
+ assertType<Components<ParentWithAddress>>({
105
+ address: {
106
+ type: 'section',
107
+ nested: true,
108
+ components: {
109
+ street: { type: 'text' },
110
+ city: { type: 'text' }
111
+ }
112
+ }
113
+ })
114
+ })
115
+
116
+ it('rejects invalid keys in nested section components', () => {
117
+ assertType<Components<ParentWithAddress>>({
118
+ address: {
119
+ type: 'section',
120
+ nested: true,
121
+ components: {
122
+ street: { type: 'text' },
123
+ // @ts-expect-error 'zipCode' is not a key of Address
124
+ zipCode: { type: 'text' }
125
+ }
126
+ }
127
+ })
128
+ })
129
+
130
+ it('types item as Address in nested section callbacks', () => {
131
+ assertType<Components<ParentWithAddress>>({
132
+ address: {
133
+ type: 'section',
134
+ nested: true,
135
+ components: {
136
+ street: {
137
+ type: 'text',
138
+ onChange({ item }) {
139
+ expectTypeOf(item).not.toBeAny()
140
+ expectTypeOf(item).toEqualTypeOf<Address>()
141
+ }
142
+ }
143
+ }
144
+ }
145
+ })
146
+ })
147
+
148
+ it('types nested section form against the value type', () => {
149
+ assertType<Components<ParentWithAddress>>({
150
+ address: {
151
+ type: 'section',
152
+ nested: true,
153
+ form: {
154
+ type: 'form',
155
+ components: {
156
+ street: { type: 'text' },
157
+ city: { type: 'text' }
158
+ }
159
+ } satisfies Form<Address>
160
+ }
161
+ })
162
+ })
163
+
164
+ it('non-nested section on data key accepts parent item keys', () => {
165
+ assertType<Components<ParentWithAddress>>({
166
+ address: {
167
+ type: 'section',
168
+ components: {
169
+ title: { type: 'text' }
170
+ }
171
+ }
172
+ })
173
+ })
174
+ })