@multiplatform.one/frappe-ui 7.86.0 → 7.87.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 (168) hide show
  1. package/dist/cjs/FileBrowser/index.cjs +1 -1
  2. package/dist/cjs/FileBrowser/index.native.js +1 -1
  3. package/dist/cjs/FileBrowser/index.native.js.map +1 -1
  4. package/dist/cjs/desk/awesomebar.cjs +1 -1
  5. package/dist/cjs/desk/awesomebar.native.js +1 -1
  6. package/dist/cjs/desk/awesomebar.native.js.map +1 -1
  7. package/dist/cjs/fields/CodeEditor/index.cjs +1 -1
  8. package/dist/cjs/fields/CodeEditor/index.native.js +1 -1
  9. package/dist/cjs/fields/CodeEditor/index.native.js.map +1 -1
  10. package/dist/cjs/fields/Duration/index.native.js.map +1 -1
  11. package/dist/cjs/fields/LongText/index.cjs +1 -1
  12. package/dist/cjs/fields/LongText/index.native.js +1 -1
  13. package/dist/cjs/fields/LongText/index.native.js.map +1 -1
  14. package/dist/cjs/fields/MarkdownEditor/index.native.js +1 -1
  15. package/dist/cjs/fields/MarkdownEditor/index.native.js.map +1 -1
  16. package/dist/cjs/fields/RadioGroup/index.cjs +1 -1
  17. package/dist/cjs/fields/RadioGroup/index.native.js +1 -1
  18. package/dist/cjs/fields/RadioGroup/index.native.js.map +1 -1
  19. package/dist/cjs/fields/Slider/index.cjs +1 -1
  20. package/dist/cjs/fields/Slider/index.native.js +1 -1
  21. package/dist/cjs/fields/Slider/index.native.js.map +1 -1
  22. package/dist/cjs/fields/SmallText/index.cjs +1 -1
  23. package/dist/cjs/fields/SmallText/index.native.js +1 -1
  24. package/dist/cjs/fields/SmallText/index.native.js.map +1 -1
  25. package/dist/cjs/fields/TimePicker/index.native.js.map +1 -1
  26. package/dist/cjs/fields/ToggleGroup/index.cjs +1 -1
  27. package/dist/cjs/fields/ToggleGroup/index.native.js +1 -1
  28. package/dist/cjs/fields/ToggleGroup/index.native.js.map +1 -1
  29. package/dist/cjs/insights/InsightsRecords.cjs +2 -7
  30. package/dist/cjs/insights/InsightsRecords.native.js +2 -7
  31. package/dist/cjs/insights/InsightsRecords.native.js.map +1 -1
  32. package/dist/cjs/list/FacetList.cjs +1 -1
  33. package/dist/cjs/list/FacetList.native.js +1 -1
  34. package/dist/cjs/list/FacetList.native.js.map +1 -1
  35. package/dist/cjs/useFrappeAggregate.cjs +6 -1
  36. package/dist/cjs/useFrappeAggregate.native.js +12 -2
  37. package/dist/cjs/useFrappeAggregate.native.js.map +1 -1
  38. package/dist/esm/ChartCard.mjs.map +1 -1
  39. package/dist/esm/ChartCard.native.js.map +1 -1
  40. package/dist/esm/FileBrowser/index.mjs +1 -1
  41. package/dist/esm/FileBrowser/index.mjs.map +1 -1
  42. package/dist/esm/FileBrowser/index.native.js +1 -1
  43. package/dist/esm/FileBrowser/index.native.js.map +1 -1
  44. package/dist/esm/desk/awesomebar.mjs +1 -1
  45. package/dist/esm/desk/awesomebar.mjs.map +1 -1
  46. package/dist/esm/desk/awesomebar.native.js +1 -1
  47. package/dist/esm/desk/awesomebar.native.js.map +1 -1
  48. package/dist/esm/fields/CodeEditor/index.mjs +1 -1
  49. package/dist/esm/fields/CodeEditor/index.mjs.map +1 -1
  50. package/dist/esm/fields/CodeEditor/index.native.js +1 -1
  51. package/dist/esm/fields/CodeEditor/index.native.js.map +1 -1
  52. package/dist/esm/fields/Duration/index.mjs.map +1 -1
  53. package/dist/esm/fields/Duration/index.native.js.map +1 -1
  54. package/dist/esm/fields/LongText/index.mjs +1 -1
  55. package/dist/esm/fields/LongText/index.mjs.map +1 -1
  56. package/dist/esm/fields/LongText/index.native.js +1 -1
  57. package/dist/esm/fields/LongText/index.native.js.map +1 -1
  58. package/dist/esm/fields/MarkdownEditor/index.native.js +1 -1
  59. package/dist/esm/fields/MarkdownEditor/index.native.js.map +1 -1
  60. package/dist/esm/fields/RadioGroup/index.mjs +1 -1
  61. package/dist/esm/fields/RadioGroup/index.mjs.map +1 -1
  62. package/dist/esm/fields/RadioGroup/index.native.js +1 -1
  63. package/dist/esm/fields/RadioGroup/index.native.js.map +1 -1
  64. package/dist/esm/fields/Slider/index.mjs +1 -1
  65. package/dist/esm/fields/Slider/index.mjs.map +1 -1
  66. package/dist/esm/fields/Slider/index.native.js +1 -1
  67. package/dist/esm/fields/Slider/index.native.js.map +1 -1
  68. package/dist/esm/fields/SmallText/index.mjs +1 -1
  69. package/dist/esm/fields/SmallText/index.mjs.map +1 -1
  70. package/dist/esm/fields/SmallText/index.native.js +1 -1
  71. package/dist/esm/fields/SmallText/index.native.js.map +1 -1
  72. package/dist/esm/fields/TimePicker/index.mjs.map +1 -1
  73. package/dist/esm/fields/TimePicker/index.native.js.map +1 -1
  74. package/dist/esm/fields/ToggleGroup/index.mjs +1 -1
  75. package/dist/esm/fields/ToggleGroup/index.mjs.map +1 -1
  76. package/dist/esm/fields/ToggleGroup/index.native.js +1 -1
  77. package/dist/esm/fields/ToggleGroup/index.native.js.map +1 -1
  78. package/dist/esm/insights/InsightsRecords.mjs +3 -8
  79. package/dist/esm/insights/InsightsRecords.mjs.map +1 -1
  80. package/dist/esm/insights/InsightsRecords.native.js +3 -8
  81. package/dist/esm/insights/InsightsRecords.native.js.map +1 -1
  82. package/dist/esm/list/FacetList.mjs +1 -1
  83. package/dist/esm/list/FacetList.mjs.map +1 -1
  84. package/dist/esm/list/FacetList.native.js +1 -1
  85. package/dist/esm/list/FacetList.native.js.map +1 -1
  86. package/dist/esm/useFrappeAggregate.mjs +6 -1
  87. package/dist/esm/useFrappeAggregate.mjs.map +1 -1
  88. package/dist/esm/useFrappeAggregate.native.js +12 -2
  89. package/dist/esm/useFrappeAggregate.native.js.map +1 -1
  90. package/dist/jsx/ChartCard.mjs.map +1 -1
  91. package/dist/jsx/FileBrowser/index.mjs +1 -1
  92. package/dist/jsx/FileBrowser/index.mjs.map +1 -1
  93. package/dist/jsx/FileBrowser/index.native.js +1 -1
  94. package/dist/jsx/FileBrowser/index.native.js.map +1 -1
  95. package/dist/jsx/desk/awesomebar.mjs +1 -1
  96. package/dist/jsx/desk/awesomebar.mjs.map +1 -1
  97. package/dist/jsx/desk/awesomebar.native.js +1 -1
  98. package/dist/jsx/desk/awesomebar.native.js.map +1 -1
  99. package/dist/jsx/fields/CodeEditor/index.mjs +1 -1
  100. package/dist/jsx/fields/CodeEditor/index.mjs.map +1 -1
  101. package/dist/jsx/fields/CodeEditor/index.native.js +1 -1
  102. package/dist/jsx/fields/CodeEditor/index.native.js.map +1 -1
  103. package/dist/jsx/fields/Duration/index.mjs.map +1 -1
  104. package/dist/jsx/fields/Duration/index.native.js.map +1 -1
  105. package/dist/jsx/fields/LongText/index.mjs +1 -1
  106. package/dist/jsx/fields/LongText/index.mjs.map +1 -1
  107. package/dist/jsx/fields/LongText/index.native.js +1 -1
  108. package/dist/jsx/fields/LongText/index.native.js.map +1 -1
  109. package/dist/jsx/fields/MarkdownEditor/index.native.js +1 -1
  110. package/dist/jsx/fields/MarkdownEditor/index.native.js.map +1 -1
  111. package/dist/jsx/fields/RadioGroup/index.mjs +1 -1
  112. package/dist/jsx/fields/RadioGroup/index.mjs.map +1 -1
  113. package/dist/jsx/fields/RadioGroup/index.native.js +1 -1
  114. package/dist/jsx/fields/RadioGroup/index.native.js.map +1 -1
  115. package/dist/jsx/fields/Slider/index.mjs +1 -1
  116. package/dist/jsx/fields/Slider/index.mjs.map +1 -1
  117. package/dist/jsx/fields/Slider/index.native.js +1 -1
  118. package/dist/jsx/fields/Slider/index.native.js.map +1 -1
  119. package/dist/jsx/fields/SmallText/index.mjs +1 -1
  120. package/dist/jsx/fields/SmallText/index.mjs.map +1 -1
  121. package/dist/jsx/fields/SmallText/index.native.js +1 -1
  122. package/dist/jsx/fields/SmallText/index.native.js.map +1 -1
  123. package/dist/jsx/fields/TimePicker/index.mjs.map +1 -1
  124. package/dist/jsx/fields/TimePicker/index.native.js.map +1 -1
  125. package/dist/jsx/fields/ToggleGroup/index.mjs +1 -1
  126. package/dist/jsx/fields/ToggleGroup/index.mjs.map +1 -1
  127. package/dist/jsx/fields/ToggleGroup/index.native.js +1 -1
  128. package/dist/jsx/fields/ToggleGroup/index.native.js.map +1 -1
  129. package/dist/jsx/insights/InsightsRecords.mjs +3 -8
  130. package/dist/jsx/insights/InsightsRecords.mjs.map +1 -1
  131. package/dist/jsx/insights/InsightsRecords.native.js +2 -7
  132. package/dist/jsx/insights/InsightsRecords.native.js.map +1 -1
  133. package/dist/jsx/list/FacetList.mjs +1 -1
  134. package/dist/jsx/list/FacetList.mjs.map +1 -1
  135. package/dist/jsx/list/FacetList.native.js +1 -1
  136. package/dist/jsx/list/FacetList.native.js.map +1 -1
  137. package/dist/jsx/useFrappeAggregate.mjs +6 -1
  138. package/dist/jsx/useFrappeAggregate.mjs.map +1 -1
  139. package/dist/jsx/useFrappeAggregate.native.js +12 -2
  140. package/dist/jsx/useFrappeAggregate.native.js.map +1 -1
  141. package/package.json +16 -16
  142. package/src/ChartCard.tsx +1 -1
  143. package/src/FileBrowser/index.tsx +1 -1
  144. package/src/FrappeInsights.spec.tsx +9 -0
  145. package/src/desk/awesomebar.tsx +1 -1
  146. package/src/fields/CodeEditor/index.tsx +1 -1
  147. package/src/fields/Duration/index.tsx +1 -1
  148. package/src/fields/LongText/index.tsx +1 -1
  149. package/src/fields/MarkdownEditor/index.native.tsx +1 -1
  150. package/src/fields/RadioGroup/index.tsx +1 -1
  151. package/src/fields/Slider/index.tsx +1 -1
  152. package/src/fields/SmallText/index.tsx +1 -1
  153. package/src/fields/TimePicker/index.tsx +1 -1
  154. package/src/fields/ToggleGroup/index.tsx +1 -1
  155. package/src/insights/InsightsRecords.tsx +3 -5
  156. package/src/list/FacetList.spec.tsx +128 -0
  157. package/src/list/FacetList.tsx +1 -1
  158. package/src/list/ListControlsSheet.behaviour.spec.tsx +283 -0
  159. package/src/useFrappeAggregate.groupLabel.spec.tsx +42 -0
  160. package/src/useFrappeAggregate.ts +7 -1
  161. package/types/ChartCard.d.ts +1 -1
  162. package/types/ChartCard.d.ts.map +1 -1
  163. package/types/fields/Duration/index.d.ts +1 -1
  164. package/types/fields/Duration/index.d.ts.map +1 -1
  165. package/types/fields/TimePicker/index.d.ts +1 -1
  166. package/types/fields/TimePicker/index.d.ts.map +1 -1
  167. package/types/insights/InsightsRecords.d.ts.map +1 -1
  168. package/types/useFrappeAggregate.d.ts.map +1 -1
@@ -0,0 +1,283 @@
1
+ import { renderWithProviders } from "@multiplatform.one/test-utils";
2
+ import { cleanup, fireEvent, screen, waitFor, within } from "@testing-library/react";
3
+ import { afterEach, describe, expect, it, vi } from "vitest";
4
+ import { ListControlsSheet, type ListControlsSheetProps } from "./ListControlsSheet";
5
+ import { fixtureControls, fixtureList } from "./listControls.fixture";
6
+ import type { FrappeListState } from "./listState";
7
+
8
+ vi.mock("./RangeFilter", async (importOriginal) => {
9
+ const actual = await importOriginal<typeof import("./RangeFilter")>();
10
+ return {
11
+ ...actual,
12
+ RangeFilter: ({
13
+ label,
14
+ value,
15
+ max,
16
+ onChange,
17
+ }: {
18
+ label: string;
19
+ value: number;
20
+ max: number;
21
+ onChange: (value: number) => void;
22
+ }) => (
23
+ <button type="button" data-max={max} data-value={value} onClick={() => onChange(50)}>
24
+ {label}
25
+ </button>
26
+ ),
27
+ };
28
+ });
29
+
30
+ afterEach(cleanup);
31
+
32
+ const controls = {
33
+ ...fixtureControls,
34
+ facets: fixtureControls.facets!.map((facet) =>
35
+ facet.param === "type"
36
+ ? { ...facet, optionLabel: (value: string) => value.toUpperCase() }
37
+ : facet,
38
+ ),
39
+ views: ["grid", "list", "table"] as const,
40
+ };
41
+
42
+ function renderSheet(
43
+ state: Partial<FrappeListState> = {},
44
+ props: Partial<ListControlsSheetProps> = {},
45
+ listPatch: Partial<ReturnType<typeof fixtureList>> = {},
46
+ ) {
47
+ const update = vi.fn();
48
+ const list = {
49
+ ...fixtureList(state, update),
50
+ facets: { type: { counts: { fire: 5, water: 7 } } },
51
+ ...listPatch,
52
+ } as ReturnType<typeof fixtureList>;
53
+ const result = renderWithProviders(
54
+ <ListControlsSheet
55
+ controls={controls as ListControlsSheetProps["controls"]}
56
+ list={list}
57
+ searchId="roster-search"
58
+ showSearch={false}
59
+ showFavorites
60
+ separator={false}
61
+ noun="Pokemon"
62
+ testID="roster"
63
+ {...props}
64
+ />,
65
+ );
66
+ return { ...result, update, list };
67
+ }
68
+
69
+ async function openSheet() {
70
+ fireEvent.click(screen.getByTestId("roster-filters"));
71
+ return screen.findByTestId("roster-filters-sheet");
72
+ }
73
+
74
+ const confirmLabel = () =>
75
+ screen.getByTestId("roster-filters-header-confirm").getAttribute("aria-label");
76
+ const counted = "Show {{count}} {{noun}}";
77
+
78
+ describe("ListControlsSheet: the filters in force as chips", () => {
79
+ it("shows a facet chip by its option label and drops the facet on a tap", () => {
80
+ const { update } = renderSheet({ facets: { type: "fire" } });
81
+ fireEvent.click(screen.getByRole("button", { name: "FIRE" }));
82
+ expect(update).toHaveBeenCalledWith({ facets: { type: "" } });
83
+ });
84
+
85
+ it("shows a facet chip by its raw value when the facet has no option label", () => {
86
+ renderSheet({ facets: { region: "kanto" } });
87
+ const chip = screen
88
+ .getAllByRole("button", { name: "kanto" })
89
+ .find((node) => !node.closest('[role="radiogroup"]'));
90
+ expect(chip).toBeTruthy();
91
+ });
92
+
93
+ it("shows a range chip as its minimum and drops it on a tap", () => {
94
+ const { update } = renderSheet({ ranges: { xp: 120 } });
95
+ fireEvent.click(screen.getByRole("button", { name: "Min XP 120+" }));
96
+ expect(update).toHaveBeenCalledWith({ ranges: { xp: 0 } });
97
+ });
98
+
99
+ it("shows a Favorites chip and turns favorites-only off on a tap", () => {
100
+ const { update } = renderSheet({ favoritesOnly: true });
101
+ fireEvent.click(screen.getByRole("button", { name: "Favorites" }));
102
+ expect(update).toHaveBeenCalledWith({ favoritesOnly: false });
103
+ });
104
+
105
+ it("draws no chip row with no filter, view or shuffle", () => {
106
+ renderSheet();
107
+ expect(screen.queryByRole("button", { name: "Favorites" })).toBeNull();
108
+ expect(screen.queryByTestId("roster-views")).toBeNull();
109
+ });
110
+ });
111
+
112
+ describe("ListControlsSheet: view switch and shuffle", () => {
113
+ it("switches views from icon segments named Cards, List and Table", () => {
114
+ const onChange = vi.fn();
115
+ renderSheet({}, { view: { current: "grid", onChange } });
116
+ const views = screen.getByTestId("roster-views");
117
+ expect(
118
+ [...views.querySelectorAll("[aria-label]")]
119
+ .map((node) => node.getAttribute("aria-label"))
120
+ .filter((name) => ["Cards", "List", "Table"].includes(name ?? "")),
121
+ ).toEqual(["Cards", "List", "Table"]);
122
+ fireEvent.click(within(views).getByLabelText("Table"));
123
+ expect(onChange).toHaveBeenCalledWith("table");
124
+ });
125
+
126
+ it("toggles the shuffle panel from a dice button wired to the panel", () => {
127
+ const toggle = vi.fn();
128
+ renderSheet({}, { shuffle: { open: true, toggle, panelId: "roster-shuffle" } });
129
+ const dice = screen.getByTestId("roster-shuffle-toggle");
130
+ expect(dice.getAttribute("aria-label")).toBe("Surprise me");
131
+ expect(dice.getAttribute("aria-expanded")).toBe("true");
132
+ expect(dice.getAttribute("aria-controls")).toBe("roster-shuffle");
133
+ fireEvent.click(dice);
134
+ expect(toggle).toHaveBeenCalled();
135
+ });
136
+ });
137
+
138
+ describe("ListControlsSheet: the sheet", () => {
139
+ it("counts its confirm for the whole list when nothing filters it", async () => {
140
+ renderSheet();
141
+ await openSheet();
142
+ expect(confirmLabel()).toBe(counted);
143
+ });
144
+
145
+ it("counts its confirm for the one active facet", async () => {
146
+ renderSheet({ facets: { type: "water" } });
147
+ await openSheet();
148
+ expect(confirmLabel()).toBe(counted);
149
+ });
150
+
151
+ it("counts its confirm for the one active band", async () => {
152
+ renderSheet({ bands: { generation: "2" } });
153
+ await openSheet();
154
+ expect(confirmLabel()).toBe(counted);
155
+ });
156
+
157
+ it("counts its confirm for favorites under favorites-only", async () => {
158
+ renderSheet({ favoritesOnly: true });
159
+ await openSheet();
160
+ expect(confirmLabel()).toBe(counted);
161
+ });
162
+
163
+ it("names Show results when the one active facet has no count yet", async () => {
164
+ renderSheet({ facets: { region: "kanto" } });
165
+ await openSheet();
166
+ expect(confirmLabel()).toBe("Show results");
167
+ });
168
+
169
+ it("falls back to Show results when two filters are in force", async () => {
170
+ renderSheet({ facets: { type: "fire" }, favoritesOnly: true });
171
+ await openSheet();
172
+ expect(confirmLabel()).toBe("Show results");
173
+ });
174
+
175
+ it("falls back to Show results when a range alone has no count", async () => {
176
+ renderSheet({ ranges: { xp: 50 } });
177
+ await openSheet();
178
+ expect(confirmLabel()).toBe("Show results");
179
+ });
180
+
181
+ it("closes on the confirm and on Escape", async () => {
182
+ renderSheet();
183
+ const expanded = () => screen.getByTestId("roster-filters").getAttribute("aria-expanded");
184
+ await openSheet();
185
+ expect(expanded()).toBe("true");
186
+ fireEvent.click(screen.getByTestId("roster-filters-header-confirm"));
187
+ await waitFor(() => expect(expanded()).toBe("false"));
188
+ await openSheet();
189
+ fireEvent.keyDown(document, { key: "Tab" });
190
+ expect(expanded()).toBe("true");
191
+ fireEvent.keyDown(document, { key: "Escape" });
192
+ await waitFor(() => expect(expanded()).toBe("false"));
193
+ });
194
+
195
+ it("resets sort to the first, favorites off, and every facet, range and band", async () => {
196
+ const { update } = renderSheet({ sort: "xp", favoritesOnly: true });
197
+ await openSheet();
198
+ fireEvent.click(screen.getByTestId("roster-reset"));
199
+ expect(update).toHaveBeenCalledWith({
200
+ sort: "number",
201
+ favoritesOnly: false,
202
+ facets: { type: "", region: "" },
203
+ ranges: { xp: 0 },
204
+ bands: { generation: "" },
205
+ });
206
+ });
207
+
208
+ it("sorts from the Sort by select", async () => {
209
+ const { update } = renderSheet();
210
+ const sheet = await openSheet();
211
+ const trigger = within(sheet).getByRole("combobox", { name: "Sort by" });
212
+ trigger.focus();
213
+ fireEvent.keyDown(trigger, { key: "Enter" });
214
+ await screen.findByRole("option", { name: "XP" });
215
+ fireEvent.keyDown(document.activeElement!, { key: "ArrowDown" });
216
+ fireEvent.keyDown(document.activeElement!, { key: "Enter" });
217
+ expect(update).toHaveBeenCalledWith({ sort: "xp" });
218
+ });
219
+
220
+ it("turns favorites-only on from its switch", async () => {
221
+ const { update } = renderSheet();
222
+ const sheet = await openSheet();
223
+ fireEvent.click(within(sheet).getByRole("switch", { name: "Favorites only" }));
224
+ expect(update).toHaveBeenCalledWith({ favoritesOnly: true });
225
+ });
226
+
227
+ it("lists each facet as chips with All first and the counts read so far", async () => {
228
+ renderSheet({ facets: { type: "fire" } });
229
+ const sheet = await openSheet();
230
+ const type = within(sheet).getByRole("radiogroup", { name: "Type" });
231
+ const chips = within(type).getAllByRole("button");
232
+ expect(chips.map((chip) => chip.textContent)).toEqual(["All types 12", "FIRE 5", "WATER 7"]);
233
+ const region = within(sheet).getByRole("radiogroup", { name: "Region" });
234
+ expect(
235
+ within(region)
236
+ .getAllByRole("button")
237
+ .map((chip) => chip.textContent),
238
+ ).toEqual(["All 12", "kanto", "johto"]);
239
+ });
240
+
241
+ it("narrows on a facet chip, clears on the active one, and All clears", async () => {
242
+ const { update } = renderSheet({ facets: { type: "fire" } });
243
+ const sheet = await openSheet();
244
+ const type = within(sheet).getByRole("radiogroup", { name: "Type" });
245
+ fireEvent.click(within(type).getByRole("button", { name: /^WATER/ }));
246
+ expect(update).toHaveBeenLastCalledWith({ facets: { type: "water" } });
247
+ fireEvent.click(within(type).getByRole("button", { name: /^FIRE/ }));
248
+ expect(update).toHaveBeenLastCalledWith({ facets: { type: "" } });
249
+ fireEvent.click(within(type).getByRole("button", { name: /^All types/ }));
250
+ expect(update).toHaveBeenLastCalledWith({ facets: { type: "" } });
251
+ });
252
+
253
+ it("caps each range at the list's highest value and commits a move as a range patch", async () => {
254
+ const { update } = renderSheet();
255
+ const sheet = await openSheet();
256
+ const range = within(sheet).getByRole("button", { name: "Min XP" });
257
+ expect(Number(range.getAttribute("data-max"))).toBeGreaterThanOrEqual(300);
258
+ expect(within(sheet).getByText(`0–${range.getAttribute("data-max")}`)).toBeTruthy();
259
+ fireEvent.click(range);
260
+ expect(update).toHaveBeenCalledWith({ ranges: { xp: 50 } });
261
+ });
262
+
263
+ it("caps a range with no reading at the floor ceiling", async () => {
264
+ renderSheet({}, {}, { highest: {} });
265
+ const sheet = await openSheet();
266
+ const range = within(sheet).getByRole("button", { name: "Min XP" });
267
+ expect(range.getAttribute("data-value")).toBe("0");
268
+ });
269
+
270
+ it("has no Filters button when there is nothing to put in the sheet", () => {
271
+ renderSheet({}, { controls: {}, showFavorites: false });
272
+ expect(screen.queryByTestId("roster-filters")).toBeNull();
273
+ });
274
+ });
275
+
276
+ describe("ListControlsSheet: search", () => {
277
+ it("searches as the text settles", async () => {
278
+ const { update } = renderSheet({}, { showSearch: true, searchPlaceholder: "Find Pokemon" });
279
+ const input = screen.getByLabelText("Find Pokemon") as HTMLInputElement;
280
+ fireEvent.change(input, { target: { value: "pika" } });
281
+ await waitFor(() => expect(update).toHaveBeenCalledWith({ search: "pika" }));
282
+ });
283
+ });
@@ -0,0 +1,42 @@
1
+ import { renderHook, waitFor } from "@testing-library/react";
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3
+ import { useFrappeAggregate } from "./useFrappeAggregate";
4
+
5
+ beforeEach(() => {
6
+ vi.stubGlobal(
7
+ "fetch",
8
+ async () =>
9
+ new Response(
10
+ JSON.stringify({
11
+ data: [
12
+ { status: "Open", _aggregate_value: 4 },
13
+ { status: 3, _aggregate_value: 2 },
14
+ { status: null, _aggregate_value: 1 },
15
+ { status: { level: 2 }, _aggregate_value: 5 },
16
+ ],
17
+ }),
18
+ { headers: { "content-type": "application/json" } },
19
+ ),
20
+ );
21
+ });
22
+
23
+ afterEach(() => vi.unstubAllGlobals());
24
+
25
+ describe("useFrappeAggregate group labels", () => {
26
+ it("writes primitives as text, a missing group as empty and an object group as JSON", async () => {
27
+ const { result } = renderHook(() =>
28
+ useFrappeAggregate(
29
+ { baseURL: "http://group-label.test", realtime: false },
30
+ { doctype: "ToDo", groupBy: "status" },
31
+ ),
32
+ );
33
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
34
+
35
+ expect(result.current.groups).toEqual([
36
+ { label: "Open", value: 4 },
37
+ { label: "3", value: 2 },
38
+ { label: "", value: 1 },
39
+ { label: '{"level":2}', value: 5 },
40
+ ]);
41
+ });
42
+ });
@@ -130,6 +130,12 @@ function toFiniteNumber(raw: unknown): number | undefined {
130
130
  return Number.isFinite(num) ? num : undefined;
131
131
  }
132
132
 
133
+ function groupLabel(raw: unknown): string {
134
+ if (raw === null || raw === undefined) return "";
135
+ if (typeof raw === "object") return JSON.stringify(raw);
136
+ return String(raw);
137
+ }
138
+
133
139
  interface AggregateData {
134
140
  value?: number;
135
141
  groups?: FrappeAggregateGroup[];
@@ -162,7 +168,7 @@ async function fetchLiveAggregate(db: DbModule, spec: FrappeAggregateSpec): Prom
162
168
  const groupBy = spec.groupBy;
163
169
  return {
164
170
  groups: rows.map((row) => ({
165
- label: String(row[groupBy] ?? ""),
171
+ label: groupLabel(row[groupBy]),
166
172
  value: toFiniteNumber(row._aggregate_value) ?? 0,
167
173
  })),
168
174
  };
@@ -9,5 +9,5 @@
9
9
  * invalid SVG), so negatives clamp to the baseline there.
10
10
  */
11
11
  import { type ChartCardProps } from "@multiplatform.one/components";
12
- export declare function ChartCard({ config, ...chartCardProps }: ChartCardProps): import("react/jsx-runtime").JSX.Element;
12
+ export declare function ChartCard({ config, ...chartCardProps }: Readonly<ChartCardProps>): import("react/jsx-runtime").JSX.Element;
13
13
  //# sourceMappingURL=ChartCard.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ChartCard.d.ts","sourceRoot":"","sources":["../src/ChartCard.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAEL,KAAK,cAAc,EAEpB,MAAM,+BAA+B,CAAC;AAGvC,wBAAgB,SAAS,CAAC,EAAE,MAAM,EAAE,GAAG,cAAc,EAAE,EAAE,cAAc,2CAatE"}
1
+ {"version":3,"file":"ChartCard.d.ts","sourceRoot":"","sources":["../src/ChartCard.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAEL,KAAK,cAAc,EAEpB,MAAM,+BAA+B,CAAC;AAGvC,wBAAgB,SAAS,CAAC,EAAE,MAAM,EAAE,GAAG,cAAc,EAAE,EAAE,QAAQ,CAAC,cAAc,CAAC,2CAahF"}
@@ -1,5 +1,5 @@
1
1
  import type { DurationProps as FormsDurationProps } from "@multiplatform.one/forms";
2
2
  export type DurationProps = FormsDurationProps;
3
3
  /** Thin adapter — forwards helperText / readOnly / skeleton / compact via props spread. */
4
- export declare function Duration(props: DurationProps): import("react/jsx-runtime").JSX.Element;
4
+ export declare function Duration(props: Readonly<DurationProps>): import("react/jsx-runtime").JSX.Element;
5
5
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/fields/Duration/index.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,IAAI,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAKpF,MAAM,MAAM,aAAa,GAAG,kBAAkB,CAAC;AAE/C,2FAA2F;AAC3F,wBAAgB,QAAQ,CAAC,KAAK,EAAE,aAAa,2CAW5C"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/fields/Duration/index.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,IAAI,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAKpF,MAAM,MAAM,aAAa,GAAG,kBAAkB,CAAC;AAE/C,2FAA2F;AAC3F,wBAAgB,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC,aAAa,CAAC,2CAWtD"}
@@ -1,5 +1,5 @@
1
1
  import type { TimePickerProps as FormsTimePickerProps } from "@multiplatform.one/forms";
2
2
  export type TimePickerProps = FormsTimePickerProps;
3
3
  /** Thin adapter — forwards helperText / readOnly / skeleton / compact via props spread. */
4
- export declare function TimePicker(props: TimePickerProps): import("react/jsx-runtime").JSX.Element;
4
+ export declare function TimePicker(props: Readonly<TimePickerProps>): import("react/jsx-runtime").JSX.Element;
5
5
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/fields/TimePicker/index.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,IAAI,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAKxF,MAAM,MAAM,eAAe,GAAG,oBAAoB,CAAC;AAEnD,2FAA2F;AAC3F,wBAAgB,UAAU,CAAC,KAAK,EAAE,eAAe,2CAWhD"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/fields/TimePicker/index.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,IAAI,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAKxF,MAAM,MAAM,eAAe,GAAG,oBAAoB,CAAC;AAEnD,2FAA2F;AAC3F,wBAAgB,UAAU,CAAC,KAAK,EAAE,QAAQ,CAAC,eAAe,CAAC,2CAW1D"}
@@ -1 +1 @@
1
- {"version":3,"file":"InsightsRecords.d.ts","sourceRoot":"","sources":["../../src/insights/InsightsRecords.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAE3D,MAAM,WAAW,kBAAkB,CAAC,IAAI;IACtC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,IAAI,CAAC;IACV,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,oBAAoB,CAAC,IAAI;IACxC,KAAK,EAAE,SAAS,kBAAkB,CAAC,IAAI,CAAC,EAAE,CAAC;IAC3C,MAAM,EAAE,kBAAkB,CAAC,IAAI,CAAC,CAAC;IACjC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,oBAAoB,CAAC,IAAI,CAAC,2CA+C1F"}
1
+ {"version":3,"file":"InsightsRecords.d.ts","sourceRoot":"","sources":["../../src/insights/InsightsRecords.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAE3D,MAAM,WAAW,kBAAkB,CAAC,IAAI;IACtC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,IAAI,CAAC;IACV,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,oBAAoB,CAAC,IAAI;IACxC,KAAK,EAAE,SAAS,kBAAkB,CAAC,IAAI,CAAC,EAAE,CAAC;IAC3C,MAAM,EAAE,kBAAkB,CAAC,IAAI,CAAC,CAAC;IACjC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,oBAAoB,CAAC,IAAI,CAAC,2CA6C1F"}
@@ -1 +1 @@
1
- {"version":3,"file":"useFrappeAggregate.d.ts","sourceRoot":"","sources":["../src/useFrappeAggregate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,OAAO,EAML,KAAK,eAAe,EACrB,MAAM,2BAA2B,CAAC;AAGnC,MAAM,MAAM,wBAAwB,GAAG,OAAO,GAAG,KAAK,GAAG,KAAK,GAAG,KAAK,GAAG,KAAK,CAAC;AAE/E,gEAAgE;AAChE,MAAM,WAAW,yBAAyB;IACxC,oEAAoE;IACpE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,6DAA6D;IAC7D,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,oEAAoE;IACpE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,2DAA2D;IAC3D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAChG,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,QAAQ,CAAC,EAAE,eAAe,CAAC;CAC5B;AAED,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,+CAA+C;IAC/C,SAAS,CAAC,EAAE,wBAAwB,CAAC;IACrC,qEAAqE;IACrE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,KAAK,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACrE,uEAAuE;IACvE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,wEAAwE;IACxE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,6DAA6D;IAC7D,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,wBAAwB;IACvC,iDAAiD;IACjD,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,oDAAoD;IACpD,MAAM,EAAE,oBAAoB,EAAE,GAAG,SAAS,CAAC;IAC3C,oDAAoD;IACpD,SAAS,EAAE,OAAO,CAAC;IACnB,gFAAgF;IAChF,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,KAAK,GAAG,SAAS,CAAC;IACzB,yDAAyD;IACzD,iBAAiB,EAAE,OAAO,CAAC;IAC3B,6DAA6D;IAC7D,WAAW,EAAE,IAAI,GAAG,SAAS,CAAC;IAC9B,qDAAqD;IACrD,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AA8HD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAChC,UAAU,EAAE,yBAAyB,GAAG,SAAS,EACjD,IAAI,EAAE,mBAAmB,GACxB,wBAAwB,CA+H1B"}
1
+ {"version":3,"file":"useFrappeAggregate.d.ts","sourceRoot":"","sources":["../src/useFrappeAggregate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,OAAO,EAML,KAAK,eAAe,EACrB,MAAM,2BAA2B,CAAC;AAGnC,MAAM,MAAM,wBAAwB,GAAG,OAAO,GAAG,KAAK,GAAG,KAAK,GAAG,KAAK,GAAG,KAAK,CAAC;AAE/E,gEAAgE;AAChE,MAAM,WAAW,yBAAyB;IACxC,oEAAoE;IACpE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,6DAA6D;IAC7D,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,oEAAoE;IACpE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,2DAA2D;IAC3D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAChG,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,QAAQ,CAAC,EAAE,eAAe,CAAC;CAC5B;AAED,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,+CAA+C;IAC/C,SAAS,CAAC,EAAE,wBAAwB,CAAC;IACrC,qEAAqE;IACrE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,KAAK,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACrE,uEAAuE;IACvE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,wEAAwE;IACxE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,6DAA6D;IAC7D,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,wBAAwB;IACvC,iDAAiD;IACjD,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,oDAAoD;IACpD,MAAM,EAAE,oBAAoB,EAAE,GAAG,SAAS,CAAC;IAC3C,oDAAoD;IACpD,SAAS,EAAE,OAAO,CAAC;IACnB,gFAAgF;IAChF,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,KAAK,GAAG,SAAS,CAAC;IACzB,yDAAyD;IACzD,iBAAiB,EAAE,OAAO,CAAC;IAC3B,6DAA6D;IAC7D,WAAW,EAAE,IAAI,GAAG,SAAS,CAAC;IAC9B,qDAAqD;IACrD,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAoID;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAChC,UAAU,EAAE,yBAAyB,GAAG,SAAS,EACjD,IAAI,EAAE,mBAAmB,GACxB,wBAAwB,CA+H1B"}