@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.
- package/package.json +6 -6
- 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,402 @@
|
|
|
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
|
+
})
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { assertType, describe, it } from 'vitest'
|
|
2
|
+
import type {
|
|
3
|
+
View,
|
|
4
|
+
Form,
|
|
5
|
+
Component,
|
|
6
|
+
PanelSchema,
|
|
7
|
+
ButtonSchema,
|
|
8
|
+
ResolvableForm
|
|
9
|
+
} from '../index.d.ts'
|
|
10
|
+
import type { Entry, Parent } from './fixtures.ts'
|
|
11
|
+
|
|
12
|
+
type DeepItem = {
|
|
13
|
+
title: string
|
|
14
|
+
items: { type: string }[]
|
|
15
|
+
tags: string[]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe('Variance: typed schemas assignable to any', () => {
|
|
19
|
+
it('Component<Entry> is assignable to Component<any>', () => {
|
|
20
|
+
const component: Component<Entry> = {
|
|
21
|
+
type: 'text'
|
|
22
|
+
}
|
|
23
|
+
assertType<Component<any>>(component)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('ButtonSchema<Entry> is assignable to Component<any>', () => {
|
|
27
|
+
const button: ButtonSchema<Entry> = {
|
|
28
|
+
type: 'button',
|
|
29
|
+
text: 'Click me'
|
|
30
|
+
}
|
|
31
|
+
assertType<Component<any>>(button)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('PanelSchema<Entry> is assignable to PanelSchema<any>', () => {
|
|
35
|
+
const panel: PanelSchema<Entry> = {
|
|
36
|
+
type: 'panel',
|
|
37
|
+
components: {
|
|
38
|
+
title: { type: 'text' }
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
assertType<PanelSchema<any>>(panel)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('View<Entry> is assignable to View<any>', () => {
|
|
45
|
+
const view: View<Entry> = {
|
|
46
|
+
type: 'view',
|
|
47
|
+
components: {
|
|
48
|
+
title: { type: 'text' }
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
assertType<View<any>>(view)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('View<Entry> with panels is assignable to View<any>', () => {
|
|
55
|
+
const view: View<Entry> = {
|
|
56
|
+
type: 'view',
|
|
57
|
+
components: {
|
|
58
|
+
title: { type: 'text' }
|
|
59
|
+
},
|
|
60
|
+
panels: {
|
|
61
|
+
sidebar: {
|
|
62
|
+
type: 'panel',
|
|
63
|
+
components: {
|
|
64
|
+
title: { type: 'text' }
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
assertType<View<any>>(view)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('Record of typed views assignable to Record<string, View<any>>', () => {
|
|
73
|
+
const views = {
|
|
74
|
+
entries: {
|
|
75
|
+
type: 'view',
|
|
76
|
+
components: {
|
|
77
|
+
title: { type: 'text' }
|
|
78
|
+
}
|
|
79
|
+
} satisfies View<Entry>
|
|
80
|
+
}
|
|
81
|
+
assertType<Record<string, View<any>>>(views)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('Form<Entry> is assignable to ResolvableForm<any>', () => {
|
|
85
|
+
const form: Form<Entry> = {
|
|
86
|
+
type: 'form',
|
|
87
|
+
components: {
|
|
88
|
+
title: { type: 'text' }
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
assertType<ResolvableForm<any>>(form)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('Form<Parent> (array props) is assignable to ResolvableForm<any>', () => {
|
|
95
|
+
const form: Form<Parent> = {
|
|
96
|
+
type: 'form',
|
|
97
|
+
components: {
|
|
98
|
+
title: { type: 'text' }
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
assertType<ResolvableForm<any>>(form)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('Form with primitive + nested arrays is assignable to ResolvableForm<any>', () => {
|
|
105
|
+
const form: Form<DeepItem> = {
|
|
106
|
+
type: 'form',
|
|
107
|
+
components: {
|
|
108
|
+
title: { type: 'text' }
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
assertType<ResolvableForm<any>>(form)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('Record of typed forms assignable to Record<string, ResolvableForm>', () => {
|
|
115
|
+
const forms = {
|
|
116
|
+
entry: {
|
|
117
|
+
type: 'form',
|
|
118
|
+
components: {
|
|
119
|
+
title: { type: 'text' }
|
|
120
|
+
}
|
|
121
|
+
} satisfies Form<Entry>
|
|
122
|
+
}
|
|
123
|
+
assertType<Record<string, ResolvableForm>>(forms)
|
|
124
|
+
})
|
|
125
|
+
})
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { expectTypeOf, assertType, describe, it } from 'vitest'
|
|
2
|
+
import type { Form, View } from '../index.d.ts'
|
|
3
|
+
import type { Entry } from './fixtures.ts'
|
|
4
|
+
|
|
5
|
+
describe('View with list component', () => {
|
|
6
|
+
it('accepts view with single list component and inline form', () => {
|
|
7
|
+
const view: View<Entry> = {
|
|
8
|
+
type: 'view',
|
|
9
|
+
component: {
|
|
10
|
+
type: 'list',
|
|
11
|
+
itemLabel: 'title',
|
|
12
|
+
form: {
|
|
13
|
+
type: 'form',
|
|
14
|
+
components: {
|
|
15
|
+
title: { type: 'text' }
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('accepts view with single list component and Form variable', () => {
|
|
23
|
+
const entryForm: Form<Entry> = {
|
|
24
|
+
type: 'form',
|
|
25
|
+
components: {
|
|
26
|
+
title: { type: 'text' }
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const view: View<Entry> = {
|
|
31
|
+
type: 'view',
|
|
32
|
+
component: {
|
|
33
|
+
type: 'list',
|
|
34
|
+
itemLabel: 'title',
|
|
35
|
+
form: entryForm
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('accepts view with single list component and promised form', () => {
|
|
41
|
+
const promisedForm: Promise<Record<string, Form<Entry>>> = Promise.resolve({
|
|
42
|
+
entryForm: { type: 'form', components: {} }
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
const view: View<Entry> = {
|
|
46
|
+
type: 'view',
|
|
47
|
+
component: {
|
|
48
|
+
type: 'list',
|
|
49
|
+
itemLabel: 'title',
|
|
50
|
+
form: promisedForm
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('accepts view with components map', () => {
|
|
56
|
+
const view: View<Entry> = {
|
|
57
|
+
type: 'view',
|
|
58
|
+
components: {
|
|
59
|
+
title: {
|
|
60
|
+
type: 'text',
|
|
61
|
+
format({ item }) {
|
|
62
|
+
expectTypeOf(item).not.toBeAny()
|
|
63
|
+
expectTypeOf(item.title).toBeString()
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
describe('View tabs and events', () => {
|
|
72
|
+
it('accepts tabs on a view', () => {
|
|
73
|
+
const view: View<Entry> = {
|
|
74
|
+
type: 'view',
|
|
75
|
+
tabs: {
|
|
76
|
+
overview: {
|
|
77
|
+
type: 'tab',
|
|
78
|
+
label: 'Overview'
|
|
79
|
+
},
|
|
80
|
+
details: {
|
|
81
|
+
type: 'tab',
|
|
82
|
+
label: 'Details',
|
|
83
|
+
defaultTab: true
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('accepts events on a view', () => {
|
|
90
|
+
const view: View<Entry> = {
|
|
91
|
+
type: 'view',
|
|
92
|
+
events: {
|
|
93
|
+
open({ item, open }) {
|
|
94
|
+
expectTypeOf(item).not.toBeAny()
|
|
95
|
+
expectTypeOf(item.title).toBeString()
|
|
96
|
+
expectTypeOf(open).toBeBoolean()
|
|
97
|
+
},
|
|
98
|
+
change({ item }) {
|
|
99
|
+
expectTypeOf(item).not.toBeAny()
|
|
100
|
+
expectTypeOf(item.id).toBeNumber()
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
describe('List filters', () => {
|
|
108
|
+
it('accepts filter with custom components using arbitrary keys', () => {
|
|
109
|
+
const view: View<Entry> = {
|
|
110
|
+
type: 'view',
|
|
111
|
+
component: {
|
|
112
|
+
type: 'list',
|
|
113
|
+
filters: {
|
|
114
|
+
title: {
|
|
115
|
+
filter: 'text',
|
|
116
|
+
operators: ['contains', 'equals']
|
|
117
|
+
},
|
|
118
|
+
createdAt: {
|
|
119
|
+
filter: 'date-range'
|
|
120
|
+
},
|
|
121
|
+
status: {
|
|
122
|
+
components: {
|
|
123
|
+
pattern: {
|
|
124
|
+
type: 'select',
|
|
125
|
+
options: ['active', 'inactive']
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('accepts boolean and sticky filters', () => {
|
|
135
|
+
const view: View<Entry> = {
|
|
136
|
+
type: 'view',
|
|
137
|
+
component: {
|
|
138
|
+
type: 'list',
|
|
139
|
+
filters: {
|
|
140
|
+
sticky: true,
|
|
141
|
+
title: true
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
})
|
|
146
|
+
})
|