vueless 1.4.12-beta.2 → 1.4.12-beta.21
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 +1 -1
- package/composables/useUI.ts +19 -1
- package/index.d.ts +128 -0
- package/index.ts +128 -0
- package/package.json +10 -9
- package/ui.button/config.ts +1 -1
- package/ui.button-link/config.ts +1 -1
- package/ui.button-toggle/UToggle.vue +6 -4
- package/ui.button-toggle/storybook/stories.ts +4 -1
- package/ui.button-toggle/tests/UToggle.test-d.ts +48 -0
- package/ui.button-toggle/tests/UToggleTypes.vue +55 -0
- package/ui.button-toggle/types.ts +18 -6
- package/ui.container-accordion/types.ts +5 -2
- package/ui.container-collapsible/config.ts +1 -1
- package/ui.container-drawer/tests/UDrawer.test.ts +10 -17
- package/ui.container-modal/tests/UModal.test.ts +10 -17
- package/ui.data-list/UDataList.vue +48 -17
- package/ui.data-list/config.ts +2 -2
- package/ui.data-list/storybook/stories.ts +5 -2
- package/ui.data-list/tests/UDataList.test-d.ts +76 -0
- package/ui.data-list/tests/UDataList.test.ts +52 -3
- package/ui.data-list/tests/UDataListTypes.vue +110 -0
- package/ui.data-list/types.ts +37 -5
- package/ui.data-table/UTable.vue +202 -24
- package/ui.data-table/UTableRow.vue +4 -3
- package/ui.data-table/config.ts +11 -2
- package/ui.data-table/storybook/stories.ts +34 -1
- package/ui.data-table/tests/UTable.test-d.ts +179 -0
- package/ui.data-table/tests/UTable.test.ts +85 -7
- package/ui.data-table/tests/UTableEmptyColumnsTypes.vue +27 -0
- package/ui.data-table/tests/UTableTypes.vue +193 -0
- package/ui.data-table/types.ts +106 -9
- package/ui.data-table/utilTable.ts +1 -1
- package/ui.dropdown/UDropdown.vue +19 -8
- package/ui.dropdown/storybook/stories.ts +4 -1
- package/ui.dropdown/tests/UDropdown.test-d.ts +61 -0
- package/ui.dropdown/tests/UDropdown.test.ts +8 -1
- package/ui.dropdown/tests/UDropdownTypes.vue +111 -0
- package/ui.dropdown/types.ts +27 -3
- package/ui.dropdown-badge/UDropdownBadge.vue +9 -4
- package/ui.dropdown-badge/storybook/stories.ts +6 -1
- package/ui.dropdown-badge/tests/UDropdownBadge.test-d.ts +55 -0
- package/ui.dropdown-badge/tests/UDropdownBadge.test.ts +8 -1
- package/ui.dropdown-badge/tests/UDropdownBadgeTypes.vue +107 -0
- package/ui.dropdown-badge/types.ts +17 -3
- package/ui.dropdown-button/UDropdownButton.vue +9 -4
- package/ui.dropdown-button/storybook/stories.ts +6 -1
- package/ui.dropdown-button/tests/UDropdownButton.test-d.ts +57 -0
- package/ui.dropdown-button/tests/UDropdownButton.test.ts +8 -1
- package/ui.dropdown-button/tests/UDropdownButtonTypes.vue +107 -0
- package/ui.dropdown-button/types.ts +17 -3
- package/ui.dropdown-link/UDropdownLink.vue +9 -4
- package/ui.dropdown-link/config.ts +1 -1
- package/ui.dropdown-link/storybook/stories.ts +4 -1
- package/ui.dropdown-link/tests/UDropdownLink.test-d.ts +55 -0
- package/ui.dropdown-link/tests/UDropdownLink.test.ts +8 -1
- package/ui.dropdown-link/tests/UDropdownLinkTypes.vue +107 -0
- package/ui.dropdown-link/types.ts +17 -3
- package/ui.form-calendar/config.ts +1 -1
- package/ui.form-checkbox/config.ts +1 -1
- package/ui.form-checkbox/tests/UCheckbox.test.ts +4 -4
- package/ui.form-checkbox/types.ts +4 -2
- package/ui.form-checkbox-group/UCheckboxGroup.vue +8 -2
- package/ui.form-date-picker/UDatePicker.vue +11 -1
- package/ui.form-date-picker/tests/UDatePicker.test.ts +47 -0
- package/ui.form-input/UInput.vue +1 -0
- package/ui.form-input/config.ts +1 -1
- package/ui.form-input/tests/UInput.test.ts +16 -0
- package/ui.form-input-file/config.ts +1 -1
- package/ui.form-listbox/UListbox.vue +29 -12
- package/ui.form-listbox/config.ts +1 -1
- package/ui.form-listbox/storybook/stories.ts +4 -1
- package/ui.form-listbox/tests/UListbox.test-d.ts +63 -0
- package/ui.form-listbox/tests/UListboxTypes.vue +107 -0
- package/ui.form-listbox/types.ts +34 -2
- package/ui.form-radio/config.ts +1 -1
- package/ui.form-radio-group/URadioGroup.vue +15 -3
- package/ui.form-radio-group/config.ts +8 -3
- package/ui.form-radio-group/types.ts +2 -3
- package/ui.form-select/USelect.vue +49 -17
- package/ui.form-select/config.ts +4 -4
- package/ui.form-select/storybook/stories.ts +4 -1
- package/ui.form-select/tests/USelect.test-d.ts +76 -0
- package/ui.form-select/tests/USelect.test.ts +65 -1
- package/ui.form-select/tests/USelectTypes.vue +113 -0
- package/ui.form-select/types.ts +55 -4
- package/ui.form-switch/USwitch.vue +13 -12
- package/ui.form-switch/config.ts +2 -1
- package/ui.form-switch/tests/USwitch.test.ts +151 -0
- package/ui.form-textarea/config.ts +1 -1
- package/ui.image-avatar/types.ts +5 -2
- package/ui.image-avatar-group/types.ts +5 -2
- package/ui.image-icon/UIcon.vue +3 -2
- package/ui.image-icon/types.ts +2 -2
- package/ui.navigation-breadcrumbs/types.ts +5 -2
- package/ui.navigation-tab/UTab.vue +14 -9
- package/ui.navigation-tab/tests/UTab.test.ts +14 -0
- package/ui.navigation-tabs/UTabs.vue +158 -6
- package/ui.navigation-tabs/config.ts +2 -1
- package/ui.navigation-tabs/constants.ts +2 -0
- package/ui.navigation-tabs/storybook/stories.ts +12 -1
- package/ui.navigation-tabs/tests/UTabs.test-d.ts +50 -0
- package/ui.navigation-tabs/tests/UTabs.test.ts +213 -1
- package/ui.navigation-tabs/tests/UTabsTypes.vue +65 -0
- package/ui.navigation-tabs/types.ts +26 -4
- package/ui.skeleton-text/constants.ts +1 -1
- package/ui.text-badge/config.ts +1 -1
- package/utils/node/loaderIcon.js +1 -1
- package/v.tooltip/vTooltip.ts +1 -1
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { describe, it, expectTypeOf } from "vitest";
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
Props,
|
|
5
|
+
RowId,
|
|
6
|
+
SelectedSlotRow,
|
|
7
|
+
SlotRow,
|
|
8
|
+
TableRow,
|
|
9
|
+
UTableDynamicSlots,
|
|
10
|
+
UTableSlots,
|
|
11
|
+
} from "../types";
|
|
12
|
+
|
|
13
|
+
interface UserRow {
|
|
14
|
+
id: number;
|
|
15
|
+
name: string;
|
|
16
|
+
qty: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type CellSlot = NonNullable<UTableSlots<UserRow>["cell-name"]>;
|
|
20
|
+
type CellProps = Parameters<CellSlot>[0];
|
|
21
|
+
|
|
22
|
+
/* The caller's own row members must all stay optional — see `SlotRow`. */
|
|
23
|
+
type RequiredKeys<T> = {
|
|
24
|
+
[K in keyof T]-?: object extends Pick<T, K> ? never : K;
|
|
25
|
+
}[keyof T];
|
|
26
|
+
|
|
27
|
+
describe("UTable types", () => {
|
|
28
|
+
it("infers the caller's row shape in a cell slot", () => {
|
|
29
|
+
expectTypeOf<CellProps["row"]["name"]>().toEqualTypeOf<string | undefined>();
|
|
30
|
+
expectTypeOf<CellProps["row"]["qty"]>().toEqualTypeOf<number | undefined>();
|
|
31
|
+
expectTypeOf<CellProps["index"]>().toEqualTypeOf<number>();
|
|
32
|
+
expectTypeOf<CellProps["cellIndex"]>().toEqualTypeOf<number>();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("does not expose undeclared row properties to a cell slot", () => {
|
|
36
|
+
/* An index signature would make any key legal, so `row.nonExistent` must not resolve. */
|
|
37
|
+
type HasIndexSignature = string extends keyof CellProps["row"] ? true : false;
|
|
38
|
+
|
|
39
|
+
expectTypeOf<HasIndexSignature>().toEqualTypeOf<false>();
|
|
40
|
+
expectTypeOf<keyof CellProps["row"]>().not.toEqualTypeOf<string>();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("keeps the row flattening metadata available", () => {
|
|
44
|
+
expectTypeOf<CellProps["row"]>().toHaveProperty("nestedLevel");
|
|
45
|
+
expectTypeOf<SlotRow<UserRow>["nestedLevel"]>().toEqualTypeOf<number>();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("types the static slots", () => {
|
|
49
|
+
type SelectedRows = Parameters<NonNullable<UTableSlots<UserRow>["header-actions"]>>[0];
|
|
50
|
+
|
|
51
|
+
expectTypeOf<SelectedRows["selectedRows"]>().toEqualTypeOf<SelectedSlotRow<UserRow>[]>();
|
|
52
|
+
|
|
53
|
+
type Expand = NonNullable<UTableSlots<UserRow>["expand"]>;
|
|
54
|
+
|
|
55
|
+
expectTypeOf<Parameters<Expand>[0]["expanded"]>().toEqualTypeOf<boolean>();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("does not promise the caller's members on selected rows", () => {
|
|
59
|
+
/* Select-all flattens nested children into the selection, and those need not carry
|
|
60
|
+
* `TRow`'s members — so the slot must not type them as guaranteed. */
|
|
61
|
+
expectTypeOf<SelectedSlotRow<UserRow>["name"]>().toEqualTypeOf<string | undefined>();
|
|
62
|
+
expectTypeOf<SelectedSlotRow<UserRow>["qty"]>().toEqualTypeOf<number | undefined>();
|
|
63
|
+
|
|
64
|
+
/* A nested child, as `getFlatRows` actually produces it, must be assignable. */
|
|
65
|
+
expectTypeOf<{
|
|
66
|
+
id: number;
|
|
67
|
+
nestedLevel: number;
|
|
68
|
+
parentRowId: number;
|
|
69
|
+
}>().toExtend<SelectedSlotRow<UserRow>>();
|
|
70
|
+
|
|
71
|
+
/* Flattening metadata stays guaranteed. */
|
|
72
|
+
expectTypeOf<SelectedSlotRow<UserRow>["nestedLevel"]>().toEqualTypeOf<number>();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("does not promise the caller's members on any row slot", () => {
|
|
76
|
+
/* `getFlatRows` recurses into `row.row`, and a nested child is typed `TableRow`, not `TRow`.
|
|
77
|
+
* Every row slot receives those children verbatim; `nested-row` receives nothing else. */
|
|
78
|
+
type Expand = Parameters<NonNullable<UTableSlots<UserRow>["expand"]>>[0];
|
|
79
|
+
type NestedRow = Parameters<NonNullable<UTableSlots<UserRow>["nested-row"]>>[0];
|
|
80
|
+
|
|
81
|
+
expectTypeOf<CellProps["row"]["name"]>().toEqualTypeOf<string | undefined>();
|
|
82
|
+
expectTypeOf<Expand["row"]["name"]>().toEqualTypeOf<string | undefined>();
|
|
83
|
+
expectTypeOf<NestedRow["row"]["name"]>().toEqualTypeOf<string | undefined>();
|
|
84
|
+
expectTypeOf<NestedRow["row"]["qty"]>().toEqualTypeOf<number | undefined>();
|
|
85
|
+
|
|
86
|
+
/* The real nested-child shape recorded at these slots must be assignable to all of them. */
|
|
87
|
+
type NestedChild = {
|
|
88
|
+
id: number;
|
|
89
|
+
label: string;
|
|
90
|
+
nestedLevel: number;
|
|
91
|
+
parentRowId: number;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
expectTypeOf<NestedChild>().toExtend<CellProps["row"]>();
|
|
95
|
+
expectTypeOf<NestedChild>().toExtend<Expand["row"]>();
|
|
96
|
+
expectTypeOf<NestedChild>().toExtend<NestedRow["row"]>();
|
|
97
|
+
|
|
98
|
+
/* Flattening metadata is always set by `getFlatRows`, so it stays guaranteed. */
|
|
99
|
+
expectTypeOf<Expand["row"]["nestedLevel"]>().toEqualTypeOf<number>();
|
|
100
|
+
expectTypeOf<NestedRow["row"]["nestedLevel"]>().toEqualTypeOf<number>();
|
|
101
|
+
expectTypeOf<NestedRow["row"]["id"]>().toExtend<RowId>();
|
|
102
|
+
expectTypeOf<undefined>().not.toExtend<NestedRow["row"]["id"]>();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("resolves Props with no type argument", () => {
|
|
106
|
+
expectTypeOf<Props>().toEqualTypeOf<Props<TableRow>>();
|
|
107
|
+
expectTypeOf<Props<UserRow>["rows"]>().toEqualTypeOf<UserRow[]>();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("narrows the dynamic slot keys to the known column keys", () => {
|
|
111
|
+
type Narrowed = keyof UTableDynamicSlots<UserRow, "name" | "qty">;
|
|
112
|
+
|
|
113
|
+
expectTypeOf<"cell-name">().toExtend<Narrowed>();
|
|
114
|
+
expectTypeOf<"header-qty">().toExtend<Narrowed>();
|
|
115
|
+
expectTypeOf<"cell-typo">().not.toExtend<Narrowed>();
|
|
116
|
+
expectTypeOf<"header-typo">().not.toExtend<Narrowed>();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("accepts every dynamic slot key when the columns are not statically known", () => {
|
|
120
|
+
/* Columns from a `ref`, an API, or a plain `string[]` widen `TCol` to `string`. Erroring on
|
|
121
|
+
* a valid slot there would break existing callers, so the keys must stay open. */
|
|
122
|
+
type Loose = keyof UTableDynamicSlots<UserRow>;
|
|
123
|
+
|
|
124
|
+
expectTypeOf<"cell-name">().toExtend<Loose>();
|
|
125
|
+
expectTypeOf<"cell-anything-at-all">().toExtend<Loose>();
|
|
126
|
+
expectTypeOf<"header-whatever">().toExtend<Loose>();
|
|
127
|
+
|
|
128
|
+
expectTypeOf<UTableDynamicSlots<UserRow>>().toEqualTypeOf<
|
|
129
|
+
UTableDynamicSlots<UserRow, string>
|
|
130
|
+
>();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("accepts every dynamic slot key when the columns are empty", () => {
|
|
134
|
+
/* An empty `columns` array infers `TCol` as `never`, which means "not statically known",
|
|
135
|
+
* not "no keys allowed". Without the `[TCol]` tuple guard the mapped type collapses to `{}`
|
|
136
|
+
* and every slot is rejected — the "columns load async, start empty" pattern breaks. */
|
|
137
|
+
type Empty = keyof UTableDynamicSlots<UserRow, never>;
|
|
138
|
+
|
|
139
|
+
/* `toEqualTypeOf`, not `toExtend`: the bug made `Empty` collapse to `never`, and `never`
|
|
140
|
+
* satisfies every `toExtend` assertion vacuously. */
|
|
141
|
+
expectTypeOf<Empty>().toEqualTypeOf<`cell-${string}` | `header-${string}`>();
|
|
142
|
+
expectTypeOf<Empty>().not.toEqualTypeOf<never>();
|
|
143
|
+
|
|
144
|
+
expectTypeOf<UTableDynamicSlots<UserRow, never>>().toEqualTypeOf<
|
|
145
|
+
UTableDynamicSlots<UserRow, string>
|
|
146
|
+
>();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("keeps the row shape intact when the keys are narrowed", () => {
|
|
150
|
+
type NarrowCell = Parameters<NonNullable<UTableSlots<UserRow, "name" | "qty">["cell-name"]>>[0];
|
|
151
|
+
|
|
152
|
+
expectTypeOf<NarrowCell["row"]["name"]>().toEqualTypeOf<string | undefined>();
|
|
153
|
+
expectTypeOf<NarrowCell["row"]["nestedLevel"]>().toEqualTypeOf<number>();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("keeps the slot payloads identical however the keys resolve", () => {
|
|
157
|
+
/* Narrowing must only change which keys exist, never what a slot receives. */
|
|
158
|
+
type WideCell = Parameters<NonNullable<UTableSlots<UserRow>["cell-name"]>>[0];
|
|
159
|
+
type NarrowCell = Parameters<NonNullable<UTableSlots<UserRow, "name" | "qty">["cell-name"]>>[0];
|
|
160
|
+
type EmptyCell = Parameters<NonNullable<UTableSlots<UserRow, never>["cell-name"]>>[0];
|
|
161
|
+
|
|
162
|
+
expectTypeOf<NarrowCell>().toEqualTypeOf<WideCell>();
|
|
163
|
+
expectTypeOf<EmptyCell>().toEqualTypeOf<WideCell>();
|
|
164
|
+
|
|
165
|
+
type WideHeader = Parameters<NonNullable<UTableSlots<UserRow>["header-name"]>>[0];
|
|
166
|
+
type EmptyHeader = Parameters<NonNullable<UTableSlots<UserRow, never>["header-name"]>>[0];
|
|
167
|
+
|
|
168
|
+
expectTypeOf<EmptyHeader>().toEqualTypeOf<WideHeader>();
|
|
169
|
+
|
|
170
|
+
/* No row slot may promise the caller's members, whatever `TCol` is. Only the flattening
|
|
171
|
+
* metadata stays required; `name`/`qty` must never appear among the required keys. */
|
|
172
|
+
expectTypeOf<RequiredKeys<EmptyCell["row"]>>().toEqualTypeOf<RequiredKeys<WideCell["row"]>>();
|
|
173
|
+
expectTypeOf<RequiredKeys<NarrowCell["row"]>>().toEqualTypeOf<RequiredKeys<WideCell["row"]>>();
|
|
174
|
+
|
|
175
|
+
type CallerKeys = Extract<RequiredKeys<SlotRow<UserRow>>, "name" | "qty">;
|
|
176
|
+
|
|
177
|
+
expectTypeOf<CallerKeys>().toEqualTypeOf<never>();
|
|
178
|
+
});
|
|
179
|
+
});
|
|
@@ -7,6 +7,7 @@ import UTableRow from "../UTableRow.vue";
|
|
|
7
7
|
import UEmpty from "../../ui.container-empty/UEmpty.vue";
|
|
8
8
|
import ULoaderProgress from "../../ui.loader-progress/ULoaderProgress.vue";
|
|
9
9
|
import UDivider from "../../ui.container-divider/UDivider.vue";
|
|
10
|
+
import USkeleton from "../../ui.skeleton/USkeleton.vue";
|
|
10
11
|
import {
|
|
11
12
|
LoaderProgressSymbol,
|
|
12
13
|
createLoaderProgress,
|
|
@@ -77,7 +78,7 @@ describe("UTable.vue", () => {
|
|
|
77
78
|
|
|
78
79
|
function mountUTable(props: Props, options = {}) {
|
|
79
80
|
return mount(UTable, {
|
|
80
|
-
props,
|
|
81
|
+
props: { ...props },
|
|
81
82
|
global: getGlobalOptions(),
|
|
82
83
|
...options,
|
|
83
84
|
});
|
|
@@ -149,6 +150,29 @@ describe("UTable.vue", () => {
|
|
|
149
150
|
expect(emptyComponent.exists()).toBe(true);
|
|
150
151
|
});
|
|
151
152
|
|
|
153
|
+
it("Skeleton loading – renders skeleton body instead of empty state", () => {
|
|
154
|
+
const component = mountUTable(getDefaultProps({ rows: [], skeletonLoading: true }));
|
|
155
|
+
|
|
156
|
+
expect(component.findComponent(UEmpty).exists()).toBe(false);
|
|
157
|
+
expect(component.findAllComponents(USkeleton).length).toBeGreaterThan(0);
|
|
158
|
+
expect(component.find("table").exists()).toBe(true);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("Skeleton loading – applies skeleton rows and widths from props", () => {
|
|
162
|
+
const component = mountUTable(
|
|
163
|
+
getDefaultProps({
|
|
164
|
+
rows: [],
|
|
165
|
+
skeletonLoading: true,
|
|
166
|
+
skeletonRows: 2,
|
|
167
|
+
skeletonWidths: ["w-1/3"],
|
|
168
|
+
}),
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
expect(component.findAll("tbody tr")).toHaveLength(2);
|
|
172
|
+
expect(component.findAllComponents(USkeleton)).toHaveLength(defaultColumns.length * 2);
|
|
173
|
+
expect(component.findComponent(USkeleton).classes()).toContain("w-1/3");
|
|
174
|
+
});
|
|
175
|
+
|
|
152
176
|
it("Selectable – renders select all checkbox when selectable is true", () => {
|
|
153
177
|
const component = mountUTable(getDefaultProps({ selectable: true }));
|
|
154
178
|
|
|
@@ -221,6 +245,20 @@ describe("UTable.vue", () => {
|
|
|
221
245
|
expect(loader.props("loading")).toBe(false);
|
|
222
246
|
});
|
|
223
247
|
|
|
248
|
+
it("Skeleton loading – hides progress loader while skeleton is shown", () => {
|
|
249
|
+
const component = mountUTable(
|
|
250
|
+
getDefaultProps({
|
|
251
|
+
rows: [],
|
|
252
|
+
loading: true,
|
|
253
|
+
skeletonLoading: true,
|
|
254
|
+
}),
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
const loader = component.findComponent(ULoaderProgress);
|
|
258
|
+
|
|
259
|
+
expect(loader.props("loading")).toBe(false);
|
|
260
|
+
});
|
|
261
|
+
|
|
224
262
|
it("Data Test – applies correct data-test attributes", () => {
|
|
225
263
|
const dataTest = "test-table";
|
|
226
264
|
|
|
@@ -644,7 +682,14 @@ describe("UTable.vue", () => {
|
|
|
644
682
|
name: "Parent Row",
|
|
645
683
|
email: "parent@example.com",
|
|
646
684
|
role: "Admin",
|
|
647
|
-
row: [
|
|
685
|
+
row: [
|
|
686
|
+
{
|
|
687
|
+
id: "1-1",
|
|
688
|
+
name: "Child",
|
|
689
|
+
email: "child@example.com",
|
|
690
|
+
role: "User",
|
|
691
|
+
},
|
|
692
|
+
],
|
|
648
693
|
},
|
|
649
694
|
];
|
|
650
695
|
|
|
@@ -710,7 +755,14 @@ describe("UTable.vue", () => {
|
|
|
710
755
|
name: "Parent Row",
|
|
711
756
|
email: "parent@example.com",
|
|
712
757
|
role: "Admin",
|
|
713
|
-
row: [
|
|
758
|
+
row: [
|
|
759
|
+
{
|
|
760
|
+
id: "1-1",
|
|
761
|
+
name: "Child",
|
|
762
|
+
email: "child@example.com",
|
|
763
|
+
role: "User",
|
|
764
|
+
},
|
|
765
|
+
],
|
|
714
766
|
},
|
|
715
767
|
];
|
|
716
768
|
|
|
@@ -792,7 +844,14 @@ describe("UTable.vue", () => {
|
|
|
792
844
|
name: "Parent Row",
|
|
793
845
|
email: "parent@example.com",
|
|
794
846
|
role: "Admin",
|
|
795
|
-
row: [
|
|
847
|
+
row: [
|
|
848
|
+
{
|
|
849
|
+
id: "1-1",
|
|
850
|
+
name: "Child",
|
|
851
|
+
email: "child@example.com",
|
|
852
|
+
role: "User",
|
|
853
|
+
},
|
|
854
|
+
],
|
|
796
855
|
};
|
|
797
856
|
|
|
798
857
|
const component = mountUTable(getDefaultProps({ rows: [expandableRow] }));
|
|
@@ -813,7 +872,14 @@ describe("UTable.vue", () => {
|
|
|
813
872
|
name: "Parent Row",
|
|
814
873
|
email: "parent@example.com",
|
|
815
874
|
role: "Admin",
|
|
816
|
-
row: [
|
|
875
|
+
row: [
|
|
876
|
+
{
|
|
877
|
+
id: "1-1",
|
|
878
|
+
name: "Child",
|
|
879
|
+
email: "child@example.com",
|
|
880
|
+
role: "User",
|
|
881
|
+
},
|
|
882
|
+
],
|
|
817
883
|
};
|
|
818
884
|
|
|
819
885
|
const component = mountUTable(
|
|
@@ -879,7 +945,12 @@ describe("UTable.vue", () => {
|
|
|
879
945
|
email: "child1@example.com",
|
|
880
946
|
role: "User",
|
|
881
947
|
row: [
|
|
882
|
-
{
|
|
948
|
+
{
|
|
949
|
+
id: "1-1-1",
|
|
950
|
+
name: "Grandchild",
|
|
951
|
+
email: "grandchild@example.com",
|
|
952
|
+
role: "Guest",
|
|
953
|
+
},
|
|
883
954
|
],
|
|
884
955
|
},
|
|
885
956
|
],
|
|
@@ -902,7 +973,14 @@ describe("UTable.vue", () => {
|
|
|
902
973
|
name: "Parent Row",
|
|
903
974
|
email: "parent@example.com",
|
|
904
975
|
role: "Admin",
|
|
905
|
-
row: [
|
|
976
|
+
row: [
|
|
977
|
+
{
|
|
978
|
+
id: "1-1",
|
|
979
|
+
name: "Child",
|
|
980
|
+
email: "child@example.com",
|
|
981
|
+
role: "User",
|
|
982
|
+
},
|
|
983
|
+
],
|
|
906
984
|
};
|
|
907
985
|
|
|
908
986
|
const component = mountUTable(getDefaultProps({ rows: [expandableRow] }));
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<!-- Type-level fixture: checked by `npm run ts:check`, never mounted.
|
|
2
|
+
Regression guard for an inline empty `columns` array, which infers `TCol` as `never`.
|
|
3
|
+
`never` means "not statically known", not "no keys allowed", so every `cell-*` / `header-*`
|
|
4
|
+
slot must still compile. This shipped broken once and rejected every slot.
|
|
5
|
+
|
|
6
|
+
It needs its own file: next to other `#cell-*` call sites (as in `UTableTypes.vue`) Vue
|
|
7
|
+
reuses a widened instantiation, and the guard stops proving anything. -->
|
|
8
|
+
<template>
|
|
9
|
+
<UTable :columns="[]" :rows="rows">
|
|
10
|
+
<template #cell-name="{ row }">{{ expectOptionalString(row.name) }}</template>
|
|
11
|
+
<template #header-name="{ column }">{{ expectString(column.key) }}</template>
|
|
12
|
+
</UTable>
|
|
13
|
+
</template>
|
|
14
|
+
|
|
15
|
+
<script setup lang="ts">
|
|
16
|
+
import UTable from "../UTable.vue";
|
|
17
|
+
|
|
18
|
+
const rows = [{ id: 1, name: "a", qty: 2 }];
|
|
19
|
+
|
|
20
|
+
function expectString(value: string) {
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function expectOptionalString(value: string | undefined) {
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
</script>
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
<!-- Type-level fixture: checked by `npm run ts:check`, never mounted.
|
|
2
|
+
Proves slot props infer from the `rows` prop at a real call site. -->
|
|
3
|
+
<template>
|
|
4
|
+
<UTable :columns="columns" :rows="rows">
|
|
5
|
+
<!-- A flattened nested child may lack the parent's members, so they are optional;
|
|
6
|
+
the flattening metadata (`id`, `nestedLevel`) is always set and stays guaranteed. -->
|
|
7
|
+
<template #cell-name="{ row, value, index, cellIndex }">
|
|
8
|
+
{{ expectOptionalString(row.name) }}
|
|
9
|
+
{{ expectOptionalNumber(row.qty) }}
|
|
10
|
+
{{ expectNumber(row.id) }}
|
|
11
|
+
{{ expectNumber(row.nestedLevel) }}
|
|
12
|
+
{{ expectNumber(index) }}
|
|
13
|
+
{{ expectNumber(cellIndex) }}
|
|
14
|
+
{{ value }}
|
|
15
|
+
</template>
|
|
16
|
+
|
|
17
|
+
<template #expand="{ row, expanded, index }">
|
|
18
|
+
{{ expectOptionalString(row.name) }}
|
|
19
|
+
{{ expectNumber(row.nestedLevel) }}
|
|
20
|
+
{{ expectBoolean(expanded) }}
|
|
21
|
+
{{ expectNumber(index) }}
|
|
22
|
+
</template>
|
|
23
|
+
|
|
24
|
+
<template #nested-row="{ row, nestedLevel }">
|
|
25
|
+
{{ expectOptionalString(row.name) }}
|
|
26
|
+
{{ expectNumber(row.nestedLevel) }}
|
|
27
|
+
{{ expectNumber(nestedLevel) }}
|
|
28
|
+
</template>
|
|
29
|
+
|
|
30
|
+
<template #header-actions="{ selectedRows }">
|
|
31
|
+
{{ expectNumber(selectedRows.length) }}
|
|
32
|
+
</template>
|
|
33
|
+
|
|
34
|
+
<template #header-name="{ column, index }">
|
|
35
|
+
{{ expectString(column.key) }}
|
|
36
|
+
{{ expectNumber(index) }}
|
|
37
|
+
</template>
|
|
38
|
+
|
|
39
|
+
<template #header-counter="{ total }">{{ expectNumber(total) }}</template>
|
|
40
|
+
<template #footer="{ colsCount }">{{ expectNumber(colsCount) }}</template>
|
|
41
|
+
|
|
42
|
+
<template #cell-qty="{ row }">
|
|
43
|
+
<!-- @vue-expect-error `nonExistent` is not a key of the inferred row. -->
|
|
44
|
+
{{ row.nonExistent }}
|
|
45
|
+
</template>
|
|
46
|
+
|
|
47
|
+
<!-- @vue-expect-error `typo` is not one of the literal column keys. -->
|
|
48
|
+
<template #cell-typo>{{ "" }}</template>
|
|
49
|
+
|
|
50
|
+
<!-- @vue-expect-error `typo` is not one of the literal column keys. -->
|
|
51
|
+
<template #header-typo>{{ "" }}</template>
|
|
52
|
+
</UTable>
|
|
53
|
+
|
|
54
|
+
<!-- Literal object columns narrow the slot keys the same way as literal strings. -->
|
|
55
|
+
<UTable :columns="objectColumns" :rows="rows">
|
|
56
|
+
<template #cell-name="{ row }">{{ expectOptionalString(row.name) }}</template>
|
|
57
|
+
<template #header-qty="{ column }">{{ expectString(column.key) }}</template>
|
|
58
|
+
|
|
59
|
+
<!-- @vue-expect-error `typo` is not one of the literal column keys. -->
|
|
60
|
+
<template #cell-typo>{{ "" }}</template>
|
|
61
|
+
</UTable>
|
|
62
|
+
|
|
63
|
+
<!-- Rows from a non-literal source still expose the declared row shape. -->
|
|
64
|
+
<UTable :columns="columns" :rows="typedRows">
|
|
65
|
+
<template #cell-name="{ row }">{{ expectOptionalString(row.name) }}</template>
|
|
66
|
+
</UTable>
|
|
67
|
+
|
|
68
|
+
<!-- Graceful degradation: columns from a `ref<Column[]>` cannot be narrowed, so EVERY
|
|
69
|
+
`cell-*` / `header-*` slot must still compile. A false error here would break
|
|
70
|
+
existing users, so it matters more than catching a typo. -->
|
|
71
|
+
<UTable :columns="dynamicColumns" :rows="rows">
|
|
72
|
+
<template #cell-name="{ row, index }">
|
|
73
|
+
{{ expectOptionalString(row.name) }}{{ expectNumber(index) }}
|
|
74
|
+
</template>
|
|
75
|
+
<template #cell-anything-at-all="{ row }">{{ expectOptionalNumber(row.qty) }}</template>
|
|
76
|
+
<template #header-name="{ column }">{{ expectString(column.key) }}</template>
|
|
77
|
+
<template #header-whatever="{ index }">{{ expectNumber(index) }}</template>
|
|
78
|
+
</UTable>
|
|
79
|
+
|
|
80
|
+
<!-- Same degradation for a plain `string[]` with no literal types. -->
|
|
81
|
+
<UTable :columns="looseColumns" :rows="rows">
|
|
82
|
+
<template #cell-name="{ row }">{{ expectOptionalString(row.name) }}</template>
|
|
83
|
+
<template #cell-unknown-key="{ row }">{{ expectOptionalNumber(row.qty) }}</template>
|
|
84
|
+
</UTable>
|
|
85
|
+
|
|
86
|
+
<!-- A `const` array without `as const` widens to `string[]`, so it degrades too. Narrowing
|
|
87
|
+
is opt-in via `as const`; it must never cost a false error on the plain form. -->
|
|
88
|
+
<UTable :columns="widenedColumns" :rows="rows">
|
|
89
|
+
<template #cell-name="{ row }">{{ expectOptionalString(row.name) }}</template>
|
|
90
|
+
<template #cell-not-a-column="{ row }">{{ expectOptionalNumber(row.qty) }}</template>
|
|
91
|
+
</UTable>
|
|
92
|
+
|
|
93
|
+
<!-- Regression guard: an EMPTY `columns` array infers `TCol` as `never`. That is "not statically
|
|
94
|
+
known", not "no keys allowed" — every `cell-*` / `header-*` slot must still compile.
|
|
95
|
+
"columns load async, start empty" is the common real-world pattern; this shipped broken once. -->
|
|
96
|
+
<UTable :columns="emptyRefColumns" :rows="rows">
|
|
97
|
+
<template #cell-name="{ row }">{{ expectOptionalString(row.name) }}</template>
|
|
98
|
+
<template #header-name="{ column }">{{ expectString(column.key) }}</template>
|
|
99
|
+
</UTable>
|
|
100
|
+
|
|
101
|
+
<UTable :columns="emptyInferredRefColumns" :rows="rows">
|
|
102
|
+
<template #cell-name="{ row }">{{ expectOptionalString(row.name) }}</template>
|
|
103
|
+
<template #header-name="{ column }">{{ expectString(column.key) }}</template>
|
|
104
|
+
</UTable>
|
|
105
|
+
|
|
106
|
+
<UTable :columns="emptyComputedColumns" :rows="rows">
|
|
107
|
+
<template #cell-name="{ row }">{{ expectOptionalString(row.name) }}</template>
|
|
108
|
+
<template #header-name="{ column }">{{ expectString(column.key) }}</template>
|
|
109
|
+
</UTable>
|
|
110
|
+
|
|
111
|
+
<UTable :columns="emptyConstColumns" :rows="rows">
|
|
112
|
+
<template #cell-name="{ row }">{{ expectOptionalString(row.name) }}</template>
|
|
113
|
+
<template #header-name="{ column }">{{ expectString(column.key) }}</template>
|
|
114
|
+
</UTable>
|
|
115
|
+
|
|
116
|
+
<!-- Regression guard: a row slot must never promise the caller's members as present.
|
|
117
|
+
`getFlatRows` recurses into `row.row`, and a nested child is typed `TableRow`, not `TRow` —
|
|
118
|
+
`#nested-row` in particular fires ONLY for such children. -->
|
|
119
|
+
<UTable :columns="columns" :rows="nestedRows">
|
|
120
|
+
<template #nested-row="{ row }">
|
|
121
|
+
<!-- @vue-expect-error a flattened nested child need not carry `name`. -->
|
|
122
|
+
{{ expectString(row.name) }}
|
|
123
|
+
</template>
|
|
124
|
+
<template #expand="{ row }">
|
|
125
|
+
<!-- @vue-expect-error a flattened nested child need not carry `qty`. -->
|
|
126
|
+
{{ expectNumber(row.qty) }}
|
|
127
|
+
</template>
|
|
128
|
+
<template #cell-name="{ row }">
|
|
129
|
+
<!-- @vue-expect-error a flattened nested child need not carry `name`. -->
|
|
130
|
+
{{ expectString(row.name) }}
|
|
131
|
+
</template>
|
|
132
|
+
<template #header-actions="{ selectedRows }">
|
|
133
|
+
<!-- @vue-expect-error select-all flattens nested children into the selection. -->
|
|
134
|
+
{{ expectString(selectedRows[0].name) }}
|
|
135
|
+
</template>
|
|
136
|
+
</UTable>
|
|
137
|
+
</template>
|
|
138
|
+
|
|
139
|
+
<script setup lang="ts">
|
|
140
|
+
import { computed, ref } from "vue";
|
|
141
|
+
|
|
142
|
+
import UTable from "../UTable.vue";
|
|
143
|
+
|
|
144
|
+
import type { Column } from "../types";
|
|
145
|
+
|
|
146
|
+
interface UserRow {
|
|
147
|
+
id: number;
|
|
148
|
+
name: string;
|
|
149
|
+
qty: number;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const columns = ["name", "qty"] as const;
|
|
153
|
+
const objectColumns = [{ key: "name" }, { key: "qty" }] as const;
|
|
154
|
+
|
|
155
|
+
/* Non-literal column sources — slot keys must degrade to `cell-${string}`. */
|
|
156
|
+
const dynamicColumns = ref<Column[]>([]);
|
|
157
|
+
const looseColumns: string[] = ["name", "qty"];
|
|
158
|
+
const widenedColumns = ["name", "qty"];
|
|
159
|
+
|
|
160
|
+
/* Empty column sources — `TCol` infers as `never` and must degrade, not collapse.
|
|
161
|
+
The inline `:columns="[]"` form lives in `UTableEmptyColumnsTypes.vue`: alongside other
|
|
162
|
+
`#cell-*` call sites here, Vue reuses a widened instantiation and the guard goes vacuous. */
|
|
163
|
+
const emptyRefColumns = ref<Column[]>([]);
|
|
164
|
+
const emptyInferredRefColumns = ref([]);
|
|
165
|
+
const emptyComputedColumns = computed(() => []);
|
|
166
|
+
const emptyConstColumns = [] as const;
|
|
167
|
+
|
|
168
|
+
const rows = [{ id: 1, name: "a", qty: 2 }];
|
|
169
|
+
const typedRows = ref<UserRow[]>([]);
|
|
170
|
+
|
|
171
|
+
/* A parent whose child lacks the parent's members — exactly what `getFlatRows` emits. */
|
|
172
|
+
const nestedRows = [{ id: 1, name: "parent", qty: 2, row: [{ id: 2, label: "child" }] }];
|
|
173
|
+
|
|
174
|
+
function expectString(value: string) {
|
|
175
|
+
return value;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function expectNumber(value: number) {
|
|
179
|
+
return value;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function expectBoolean(value: boolean) {
|
|
183
|
+
return value;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function expectOptionalString(value: string | undefined) {
|
|
187
|
+
return value;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function expectOptionalNumber(value: number | undefined) {
|
|
191
|
+
return value;
|
|
192
|
+
}
|
|
193
|
+
</script>
|