@agilant/toga-blox 1.0.76 → 1.0.79

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.
@@ -27,7 +27,7 @@ const MultiSelectInput = ({ id, name, options = [], selectedValue = [], onChange
27
27
  return "All items selected";
28
28
  }
29
29
  // Default behavior: comma-separated labels
30
- return selected.map((s) => s.label).join(", ");
30
+ return selected.map((s) => s.label).join("; ");
31
31
  };
32
32
  return (_jsx("div", { className: `${width}`, children: _jsx(MultiSelect, { id: id, name: name, className: className, options: multiSelectOptions, value: multiSelectValue, onChange: (selectedOptions) => {
33
33
  // Convert back to OptionType when passing to onChange
@@ -191,13 +191,13 @@ describe("SearchInput Component", () => {
191
191
  uuid: "1",
192
192
  name: "Option 1",
193
193
  value: "option1",
194
- label: "Option 1",
194
+ // label: "Option 1",
195
195
  },
196
196
  {
197
197
  uuid: "2",
198
198
  name: "Option 2",
199
199
  value: "option2",
200
- label: "Option 2",
200
+ // label: "Option 2",
201
201
  },
202
202
  ] }));
203
203
  // Open dropdown
@@ -222,13 +222,13 @@ describe("SearchInput Component", () => {
222
222
  uuid: "1",
223
223
  name: "Option 1",
224
224
  value: "option1",
225
- label: "Option 1",
225
+ // label: "Option 1",
226
226
  },
227
227
  {
228
228
  uuid: "2",
229
229
  name: "Option 2",
230
230
  value: "option2",
231
- label: "Option 2",
231
+ // label: "Option 2",
232
232
  },
233
233
  ] }));
234
234
  const dropdown = screen.getByText(/Search/i);
@@ -245,13 +245,13 @@ describe("SearchInput Component", () => {
245
245
  uuid: "1",
246
246
  name: "Option 1",
247
247
  value: "option1",
248
- label: "Option 1",
248
+ // label: "Option 1",
249
249
  },
250
250
  {
251
251
  uuid: "2",
252
252
  name: "Option 2",
253
253
  value: "option2",
254
- label: "Option 2",
254
+ // label: "Option 2",
255
255
  },
256
256
  ] }));
257
257
  const dropdown = screen.getByText(/Search/i);
@@ -269,13 +269,13 @@ describe("SearchInput Component", () => {
269
269
  uuid: "1",
270
270
  name: "True",
271
271
  value: "true",
272
- label: "True",
272
+ // label: "True",
273
273
  },
274
274
  {
275
275
  uuid: "2",
276
276
  name: "False",
277
277
  value: "false",
278
- label: "False",
278
+ // label: "False",
279
279
  },
280
280
  ] }));
281
281
  expect(screen.getByText(/Search/i)).toBeInTheDocument();
@@ -286,13 +286,13 @@ describe("SearchInput Component", () => {
286
286
  uuid: "1",
287
287
  name: "True",
288
288
  value: "true",
289
- label: "True",
289
+ // label: "True",
290
290
  },
291
291
  {
292
292
  uuid: "2",
293
293
  name: "False",
294
294
  value: "false",
295
- label: "False",
295
+ // label: "False",
296
296
  },
297
297
  ] }));
298
298
  // Open dropdown
@@ -308,13 +308,13 @@ describe("SearchInput Component", () => {
308
308
  uuid: "1",
309
309
  name: "True",
310
310
  value: "true",
311
- label: "True",
311
+ // label: "True",
312
312
  },
313
313
  {
314
314
  uuid: "2",
315
315
  name: "False",
316
316
  value: "false",
317
- label: "False",
317
+ // label: "False",
318
318
  },
319
319
  ] }));
320
320
  // Open dropdown
@@ -477,10 +477,13 @@ describe("SearchInput Component", () => {
477
477
  });
478
478
  test("renders selectedStartDate if provided, otherwise shows default text", () => {
479
479
  const mockStartDate = new Date(2023, 5, 15); // June 15, 2023
480
+ // Helper to format date as YYYY-MM-DD
481
+ const formatDate = (date) => `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
480
482
  const { rerender } = render(_jsx(SearchInput, { column: undefined, setSearchCriteria: undefined, ...defaultProps, inputType: "date", toggleStatus: true, selectedStartDate: null }));
481
483
  expect(screen.getByText("Start Date")).toBeInTheDocument();
482
484
  rerender(_jsx(SearchInput, { column: undefined, setSearchCriteria: undefined, ...defaultProps, inputType: "date", toggleStatus: true, selectedStartDate: mockStartDate }));
483
- expect(screen.getByText(mockStartDate.toLocaleDateString())).toBeInTheDocument();
485
+ // Use the formatted date string
486
+ expect(screen.getByText(formatDate(mockStartDate))).toBeInTheDocument();
484
487
  });
485
488
  test("calls onStartDateSelect when a start date is selected", () => {
486
489
  render(_jsx(SearchInput, { column: undefined, setSearchCriteria: undefined, ...defaultProps, inputType: "date", toggleStatus: true }));
@@ -1,5 +1,10 @@
1
1
  import React from "react";
2
2
  import { Control } from "react-hook-form";
3
+ export type MixedOption = string | {
4
+ uuid: string;
5
+ name: string;
6
+ value: string;
7
+ };
3
8
  export type SearchInputProps<T extends object> = {
4
9
  onChange(newSelected: OptionType[]): unknown;
5
10
  selectedValue: any[];
@@ -8,7 +13,7 @@ export type SearchInputProps<T extends object> = {
8
13
  bgColor?: string;
9
14
  textHighlight?: string;
10
15
  inputType?: "text" | "number" | "date" | "boolean" | "multiSelect";
11
- dropdownOptions?: string[] | OptionType[] | number[];
16
+ dropdownOptions?: MixedOption[] | number[];
12
17
  selectedDropdownOption?: string | OptionType | number;
13
18
  onDropdownOptionSelect?: (option: string) => void;
14
19
  dropdownIconProp?: searchDropdownIconProps;
@@ -51,7 +51,6 @@ searchItems = [], setSearchItems, handleFilter, setSearchCriteria, column, setEd
51
51
  const parsed = JSON.parse(stored);
52
52
  const existing = parsed.find((criterion) => criterion.searchColumn.id === column.id);
53
53
  if (existing && setSearchItems) {
54
- // e.g. "2025-02-27" or "2024-12-01 - 2024-12-28"
55
54
  setSearchItems([existing.submittedSearchText]);
56
55
  }
57
56
  }
@@ -1,11 +1,10 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEffect, useRef } from "react";
2
+ import { useEffect, useRef, useCallback } from "react";
3
3
  import { Input } from "../Input";
4
4
  import Dropdown from "../Dropdown/Dropdown";
5
5
  import ToggleButton from "../ToggleButton/ToggleButton";
6
6
  import BaseButton from "../BaseButton";
7
7
  import Text from "../Text";
8
- import updateLocalStorage from "../../utils/updateLocalStorage";
9
8
  const DEFAULT_STORAGE_KEY = "searchCriteria";
10
9
  /**
11
10
  * A numeric filter component that:
@@ -22,35 +21,41 @@ const SearchNumberInput = ({ textHighlight = "text-sky-500", dropdownIconProp =
22
21
  searchItems = [], setSearchItems, setSearchCriteria, column, setEditingHeader, localStorageKey = DEFAULT_STORAGE_KEY, }) => {
23
22
  const containerRef = useRef(null);
24
23
  const inputRef = useRef(null);
24
+ /** Decide if we can store for this column */
25
25
  const canStore = !!column?.id && !!setSearchCriteria;
26
+ /** On mount, read localStorage once => if there's a saved filter => parse & set */
26
27
  useEffect(() => {
27
28
  if (!canStore)
28
29
  return;
30
+ // Make sure this runs only once
29
31
  const stored = localStorage.getItem(localStorageKey);
30
32
  if (!stored)
31
33
  return;
32
34
  try {
33
35
  const parsed = JSON.parse(stored);
34
- const existing = parsed.find((c) => c.searchColumn?.id === column.id);
36
+ const existing = parsed.find((c) => c.searchColumn?.id === column?.id);
35
37
  if (existing) {
36
38
  const savedString = existing.submittedSearchText;
37
39
  if (savedString && typeof savedString === "string") {
38
40
  if (savedString.includes(" - ")) {
41
+ // It's a range: "100 - 200"
39
42
  setToggleStatus?.(true);
40
43
  const [minPart, maxPart] = savedString.split(" - ");
41
44
  setMinValue?.(minPart.trim());
42
45
  setMaxValue?.(maxPart.trim());
43
46
  }
44
47
  else {
48
+ // Single e.g. "Exactly 123" or "default 2" or just "2"
45
49
  setToggleStatus?.(false);
46
50
  const segments = savedString.split(" ");
51
+ // If the first word is in your dropdown => use it, else treat as pure number
47
52
  if (segments.length > 1 &&
48
53
  dropdownOptions.includes(segments[0])) {
49
54
  onDropdownOptionSelect?.(segments[0]);
50
55
  setMinValue?.(segments[1]);
51
56
  }
52
57
  else {
53
- // no special prefix => assume it's just the number
58
+ // no prefix => assume it's just the number
54
59
  setMinValue?.(savedString);
55
60
  }
56
61
  }
@@ -62,13 +67,19 @@ searchItems = [], setSearchItems, setSearchCriteria, column, setEditingHeader, l
62
67
  catch (err) {
63
68
  console.error("Error reading stored number filter:", err);
64
69
  }
70
+ // empty dependency array => only once on mount
65
71
  }, []);
66
- // Focus on mount if needed
72
+ /** Focus on mount if needed */
67
73
  useEffect(() => {
68
74
  inputRef.current?.focus();
69
75
  }, []);
70
- /** Build the final string to store. Feel free to tweak logic here. */
71
- const buildNumberString = () => {
76
+ /**
77
+ * Build the final string to store.
78
+ *
79
+ * 1) If toggleStatus => "minValue - maxValue"
80
+ * 2) If single => skip "default" prefix
81
+ */
82
+ const buildNumberString = useCallback(() => {
72
83
  if (toggleStatus) {
73
84
  // Range mode => "min - max"
74
85
  if (minValue && maxValue) {
@@ -85,10 +96,12 @@ searchItems = [], setSearchItems, setSearchCriteria, column, setEditingHeader, l
85
96
  }
86
97
  }
87
98
  else {
88
- // Single value mode => e.g. "Exactly 123" or just "123"
89
- // If you want "Less Than 123", you might do:
90
- if (selectedDropdownOption && minValue) {
91
- return `${selectedDropdownOption} ${minValue}`;
99
+ // Single value mode => e.g. "Exactly 123" or "2"
100
+ const prefix = selectedDropdownOption?.toLowerCase() === "default"
101
+ ? "" // if your dropdown used "default" for something
102
+ : selectedDropdownOption;
103
+ if (prefix && minValue) {
104
+ return `${prefix} ${minValue}`;
92
105
  }
93
106
  else if (minValue) {
94
107
  return minValue; // fallback
@@ -97,16 +110,19 @@ searchItems = [], setSearchItems, setSearchCriteria, column, setEditingHeader, l
97
110
  return "";
98
111
  }
99
112
  }
100
- };
101
- /** Called when user clicks "Filter" button at the bottom. */
113
+ }, [toggleStatus, minValue, maxValue, selectedDropdownOption]);
114
+ /**
115
+ * Called when user clicks "Filter" button at the bottom.
116
+ * We remove or store the built string, then call handleFilter.
117
+ */
102
118
  const handleFilterClick = () => {
103
119
  const finalString = buildNumberString().trim();
104
120
  if (!finalString) {
105
- // If it's empty => remove existing filter for this column
121
+ // remove existing filter for this column
106
122
  if (canStore) {
107
123
  setSearchCriteria?.((prev) => {
108
- const newCriteria = prev.filter((c) => c.searchColumn.id !== column.id);
109
- updateLocalStorage(newCriteria, localStorageKey);
124
+ const newCriteria = prev.filter((c) => c.searchColumn.id !== column?.id);
125
+ localStorage.setItem(localStorageKey, JSON.stringify(newCriteria));
110
126
  return newCriteria;
111
127
  });
112
128
  }
@@ -118,8 +134,8 @@ searchItems = [], setSearchItems, setSearchCriteria, column, setEditingHeader, l
118
134
  }
119
135
  if (canStore) {
120
136
  setSearchCriteria?.((prev) => {
121
- // remove old for this column if you want to keep it unique
122
- const filtered = prev.filter((c) => c.searchColumn.id !== column.id);
137
+ // remove old for this column if you want only one number filter per column
138
+ const filtered = prev.filter((c) => c.searchColumn.id !== column?.id);
123
139
  const newCriteria = [
124
140
  ...filtered,
125
141
  {
@@ -127,7 +143,7 @@ searchItems = [], setSearchItems, setSearchCriteria, column, setEditingHeader, l
127
143
  submittedSearchText: finalString,
128
144
  },
129
145
  ];
130
- updateLocalStorage(newCriteria, localStorageKey);
146
+ localStorage.setItem(localStorageKey, JSON.stringify(newCriteria));
131
147
  return newCriteria;
132
148
  });
133
149
  }
@@ -137,10 +153,11 @@ searchItems = [], setSearchItems, setSearchCriteria, column, setEditingHeader, l
137
153
  // Let the parent know a filter was applied
138
154
  handleFilter?.();
139
155
  };
140
- return (_jsx("div", { ref: containerRef, className: "w-[425px]", children: _jsxs("div", { className: "flex flex-col p-4 h-[130px] border-2 border-navy-200 rounded-md", children: [_jsx("div", { className: `flex flex-[1] ${toggleStatus ? "" : "border-2"} h-full max-h-11 items-center justify-around`, children: toggleStatus ? (
156
+ return (_jsx("div", { ref: containerRef, className: "w-[425px]", children: _jsxs("div", { className: "flex flex-col p-4 h-[130px] border-2 border-navy-200 rounded-md", children: [_jsx("div", { className: `flex flex-[1] ${toggleStatus ? "" : "border-2"}
157
+ h-full max-h-11 items-center justify-around`, children: toggleStatus ? (
141
158
  // Range mode
142
159
  _jsxs(_Fragment, { children: [_jsx(Input, { focusRingColor: "focus:ring-2", hasAutoFocus: true, value: minValue, iconColor: "text-navy-400", required: false, id: "", name: "", type: "number", onChange: (e) => setMinValue?.(e.target.value), additionalClasses: "min-w-[180px] max-w-[180px] h-10 text-gray flex focus:border-l-2 ", placeholder: "Min" }), _jsx(Text, { size: "text-md", tag: "span", text: "to", additionalClasses: "px-2" }), _jsx(Input, { focusRingColor: "focus:ring-2", value: maxValue, iconColor: "text-navy-400", required: false, id: "", name: "", type: "number", onChange: (e) => setMaxValue?.(e.target.value), additionalClasses: "min-w-[180px] max-w-[180px] h-10 text-gray flex border-2 focus:border-l-2 ", placeholder: "Max" })] })) : (
143
160
  // Single value mode
144
- _jsxs(_Fragment, { children: [_jsx(Dropdown, { options: dropdownOptions, selectedOption: selectedDropdownOption, onOptionSelect: onDropdownOptionSelect, optionClasses: "px-4 h-full flex items-center", menuClasses: "bg-white w-min-[150px] top-[-8px] left-[-2px]", dropdownClasses: "border-0 w-auto", icon: dropdownIconProp }), _jsx(Input, { ref: inputRef, focusRingColor: "focus:ring-transparent", hasAutoFocus: true, value: minValue, iconColor: "text-navy-400", required: false, id: "", name: "", type: "number", onChange: (e) => setMinValue?.(e.target.value), additionalClasses: "min-w-[200px] h-10 text-gray flex border-l-2 ", placeholder: "Amount", hasIcons: true, iconPosition: "both" })] })) }), _jsxs("div", { className: "flex flex-[1] justify-between items-end bg-white px-2", children: [_jsx(ToggleButton, { initialStatus: toggleStatus, onClick: () => setToggleStatus?.(!toggleStatus), activeColorBackground: "bg-sky-500", activeColorBorder: "border-sky-500", activeLabel: "Range", activeTextColor: "text-sky-500", additionalClasses: "flex items-center", inactiveColorBackground: "bg-gray-300", inactiveColorBorder: "border-gray-300", inactiveLabel: "Range", inactiveTextColor: "text-gray-500", pillHeight: "h-8", textPosition: "right", textSize: "text-sm", smallToggle: false, borderStyle: false }), _jsx(BaseButton, { text: "Filter", backgroundColor: "bg-sky-500", additionalClasses: "py-1.5 px-6 text-white", borderColor: "border-none", onClick: handleFilterClick, shape: "rounded-full" })] })] }) }));
161
+ _jsxs(_Fragment, { children: [_jsx(Dropdown, { options: dropdownOptions, selectedOption: selectedDropdownOption, onOptionSelect: onDropdownOptionSelect, optionClasses: "px-4 h-full flex items-center", menuClasses: "bg-white w-min-[150px] top-[-8px] left-[-2px]", dropdownClasses: "border-0 w-auto", icon: dropdownIconProp }), _jsx(Input, { ref: inputRef, focusRingColor: "focus:ring-transparent", hasAutoFocus: true, value: minValue, iconColor: "text-navy-400", required: false, id: "", name: "", type: "number", onChange: (e) => setMinValue?.(e.target.value), additionalClasses: "min-w-[200px] h-10 text-gray flex border-l-2 ", placeholder: "Amount", hasIcons: true, iconPosition: "both" })] })) }), _jsxs("div", { className: "flex flex-[1] justify-between items-end bg-white px-2", children: [_jsx(ToggleButton, { initialStatus: toggleStatus, onClick: () => setToggleStatus?.(!toggleStatus), activeColorBackground: "bg-sky-500", activeColorBorder: "border-sky-500", activeLabel: "Range", activeTextColor: "text-sky-500", additionalClasses: "flex items-center", inactiveColorBackground: "bg-gray-300", inactiveColorBorder: "border-gray-300", inactiveLabel: "Range", inactiveTextColor: "text-gray-500", pillHeight: "h-8", textPosition: "right", textSize: "text-sm", smallToggle: false, borderStyle: false }), _jsx(BaseButton, { text: "Filter", backgroundColor: "bg-sky-500", additionalClasses: "py-1.5 px-6 text-white", borderColor: "border-none", onClick: handleFilterClick, shape: "rounded-full" })] })] }) }));
145
162
  };
146
163
  export default SearchNumberInput;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@agilant/toga-blox",
3
3
  "private": false,
4
- "version": "1.0.76",
4
+ "version": "1.0.79",
5
5
  "description": "",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",