@ditojs/admin 2.92.0 → 2.94.1

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 (50) hide show
  1. package/dist/dito-admin.es.js +1668 -1518
  2. package/dist/dito-admin.umd.js +6 -6
  3. package/package.json +14 -15
  4. package/src/components/DitoAccount.vue +7 -1
  5. package/src/components/DitoCreateButton.vue +6 -2
  6. package/src/components/DitoDialog.vue +1 -1
  7. package/src/components/DitoErrors.vue +1 -0
  8. package/src/components/DitoForm.vue +1 -0
  9. package/src/components/DitoFormInner.vue +5 -0
  10. package/src/components/DitoHeader.vue +1 -1
  11. package/src/components/DitoLabel.vue +6 -1
  12. package/src/components/DitoMenu.vue +1 -0
  13. package/src/components/DitoNavigation.vue +1 -1
  14. package/src/components/DitoNotifications.vue +1 -1
  15. package/src/components/DitoPane.vue +3 -0
  16. package/src/components/DitoPanel.vue +2 -0
  17. package/src/components/DitoSchema.vue +1 -0
  18. package/src/components/DitoScopes.vue +1 -0
  19. package/src/components/DitoSpinner.vue +2 -0
  20. package/src/components/DitoTableHead.vue +10 -0
  21. package/src/components/DitoTabs.vue +18 -2
  22. package/src/components/DitoTrail.vue +2 -1
  23. package/src/components/DitoTreeItem.vue +10 -0
  24. package/src/mixins/DitoMixin.js +4 -2
  25. package/src/mixins/PulldownMixin.js +9 -0
  26. package/src/mixins/SortableMixin.js +3 -1
  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/src/utils/ui/dist/dito-ui.es.js +2729 -0
  33. package/src/utils/ui/dist/dito-ui.umd.js +2744 -0
  34. package/src/utils/ui/dist/style.css +1139 -0
  35. package/types/index.d.ts +74 -11
  36. package/types/tests/admin.test-d.ts +0 -27
  37. package/types/tests/component-buttons.test-d.ts +0 -44
  38. package/types/tests/component-list.test-d.ts +0 -159
  39. package/types/tests/component-misc.test-d.ts +0 -137
  40. package/types/tests/component-object.test-d.ts +0 -69
  41. package/types/tests/component-section.test-d.ts +0 -174
  42. package/types/tests/component-select.test-d.ts +0 -107
  43. package/types/tests/components.test-d.ts +0 -81
  44. package/types/tests/context.test-d.ts +0 -31
  45. package/types/tests/fixtures.ts +0 -24
  46. package/types/tests/form.test-d.ts +0 -109
  47. package/types/tests/instance.test-d.ts +0 -20
  48. package/types/tests/schema-features.test-d.ts +0 -402
  49. package/types/tests/variance.test-d.ts +0 -125
  50. package/types/tests/view.test-d.ts +0 -146
package/types/index.d.ts CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  TimeFormat
9
9
  } from '@ditojs/utils'
10
10
  import { RequireAtLeastOne, SetOptional } from 'type-fest'
11
- import { Component as VueComponent } from 'vue'
11
+ import { Component as VueComponent, ComponentPublicInstance } from 'vue'
12
12
  import { Router as VueRouter } from 'vue-router'
13
13
 
14
14
  declare global {
@@ -695,7 +695,19 @@ export interface SchemaSourceMixin<$Item> {
695
695
  *
696
696
  * @defaultValue `false`
697
697
  */
698
- creatable?: OrItemAccessor<$Item, {}, boolean | { label: string }>
698
+ creatable?: OrItemAccessor<
699
+ $Item,
700
+ {},
701
+ | boolean
702
+ | {
703
+ label?: string
704
+ /**
705
+ * Returns additional query parameters to include in the
706
+ * URL when navigating to the create form.
707
+ */
708
+ query?: (context: DitoContext) => Record<string, any>
709
+ }
710
+ >
699
711
  /**
700
712
  * Whether to add edit buttons next to the list items.
701
713
  *
@@ -788,11 +800,14 @@ export type SchemaOptions<$Item, $Option = any> =
788
800
  */
789
801
  groupBy?: keyof $Option
790
802
  /**
791
- * Custom equality function for comparing options.
803
+ * Custom equality function for matching a value to an
804
+ * option. Receives a context with `value` and `option`.
792
805
  */
793
806
  equals?: (
794
- a: $Option,
795
- b: $Option
807
+ context: DitoContext & {
808
+ value: any
809
+ option: $Option
810
+ }
796
811
  ) => boolean
797
812
  /**
798
813
  * Relative path to resolve options from data
@@ -1640,6 +1655,12 @@ export interface CheckboxesSchema<$Item = any>
1640
1655
  * The type of the component.
1641
1656
  */
1642
1657
  type: 'checkboxes'
1658
+ /**
1659
+ * Whether multiple values can be selected.
1660
+ *
1661
+ * @defaultValue `true`
1662
+ */
1663
+ multiple?: boolean
1643
1664
  /**
1644
1665
  * @defaultValue `'vertical'`
1645
1666
  */
@@ -1918,7 +1939,7 @@ export type DitoContext<$Item = any> = {
1918
1939
  /**
1919
1940
  * The current search term in select components.
1920
1941
  */
1921
- searchTerm: string | undefined
1942
+ searchTerm: string
1922
1943
  /**
1923
1944
  * Whether a button request is currently running.
1924
1945
  */
@@ -1976,9 +1997,22 @@ export type DitoContext<$Item = any> = {
1976
1997
  getResourceUrl(resource: Resource): string
1977
1998
  /** Displays a notification to the user. */
1978
1999
  notify(options: {
2000
+ /** Notification style. @defaultValue `'info'` */
1979
2001
  type?: LiteralUnion<'warning' | 'error' | 'info' | 'success'>
2002
+ /** Heading text. Defaults to a label matching `type`. */
1980
2003
  title?: string
2004
+ /** Body text, rendered as HTML paragraphs. */
1981
2005
  text: OrArrayOf<string>
2006
+ /**
2007
+ * The original error object. When `type` is `'error'`,
2008
+ * this is logged to the console alongside the message.
2009
+ */
2010
+ error?: unknown
2011
+ /**
2012
+ * Display duration in milliseconds. When omitted, calculated
2013
+ * automatically from content length. Use `0` for sticky.
2014
+ */
2015
+ duration?: number
1982
2016
  }): void
1983
2017
  }
1984
2018
 
@@ -1997,7 +2031,9 @@ export type DitoComponentInstance<
1997
2031
  $Members extends Record<string, any> = {}
1998
2032
  > = DitoComponentInstanceBase<$Item> & $Members
1999
2033
 
2000
- export interface DitoComponentInstanceBase<$Item = any> extends EmitterMixin {
2034
+ export interface DitoComponentInstanceBase<$Item = any>
2035
+ extends ComponentPublicInstance,
2036
+ EmitterMixin {
2001
2037
  // -- Data access (ValueMixin, ContextMixin, TypeMixin) --
2002
2038
 
2003
2039
  /** The current value of the component (getter/setter). */
@@ -2968,6 +3004,31 @@ export type SourceComponent<$Item = any> =
2968
3004
  | TreeListSchema<$Item>
2969
3005
  | TreeObjectSchema<$Item>
2970
3006
 
3007
+ /**
3008
+ * Field components that don't have an options mixin (excludes
3009
+ * select, multiselect, and source components like list/object/tree).
3010
+ */
3011
+ type NonOptionFieldComponent<$Item = any> = Exclude<
3012
+ NonSectionComponent<$Item>,
3013
+ SelectSchema<$Item> | MultiselectSchema<$Item> | SourceComponent<$Item>
3014
+ >
3015
+
3016
+ /**
3017
+ * Select/multiselect with a typed `$Option` for option callbacks.
3018
+ *
3019
+ * Note: This is intentionally an intersection type rather than
3020
+ * `SelectSchema<$Item, $Option> | MultiselectSchema<$Item, $Option>`,
3021
+ * because using the full interfaces in a union breaks TypeScript's
3022
+ * contextual typing for callbacks in the `Components` mapped type.
3023
+ */
3024
+ type OptionComponent<$Item, $Option> = BaseSchema<$Item> &
3025
+ SchemaOptionsMixin<$Item, $Option> &
3026
+ SchemaAffixMixin<$Item> &
3027
+ Pick<
3028
+ MultiselectSchema,
3029
+ 'multiple' | 'searchable' | 'stayOpen' | 'taggable'
3030
+ > & { type: 'select' | 'multiselect' }
3031
+
2971
3032
  /**
2972
3033
  * Strips properties with `never` values from a type.
2973
3034
  */
@@ -3047,14 +3108,16 @@ export type Components<$Item = any> = 0 extends 1 & $Item
3047
3108
  : {
3048
3109
  [K in keyof $Item]?: [$Item[K]] extends [never]
3049
3110
  ? NonSectionComponent<$Item> | SectionSchema<$Item>
3050
- : $Item[K] extends (infer E)[]
3111
+ : NonNullable<$Item[K]> extends (infer E)[]
3051
3112
  ? E extends Record<string, any>
3052
3113
  ? NonSectionComponent<E>
3053
3114
  : NonSectionComponent<$Item>
3054
- : $Item[K] extends Record<string, any>
3115
+ : NonNullable<$Item[K]> extends Record<string, any>
3055
3116
  ?
3056
- | NonSectionComponent<$Item>
3057
- | SectionSchema<$Item, $Item[K]>
3117
+ | NonOptionFieldComponent<$Item>
3118
+ | OptionComponent<$Item, NonNullable<$Item[K]>>
3119
+ | SourceComponent<NonNullable<$Item[K]>>
3120
+ | SectionSchema<$Item, NonNullable<$Item[K]>>
3058
3121
  :
3059
3122
  | NonSectionComponent<$Item>
3060
3123
  | SectionSchema<$Item>
@@ -1,27 +0,0 @@
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
- })
@@ -1,44 +0,0 @@
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
- })
@@ -1,159 +0,0 @@
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
- })
@@ -1,137 +0,0 @@
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
- })
@@ -1,69 +0,0 @@
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
- })