Skip to content

Commit 376f97f

Browse files
authored
fix: added support for onSearch action trigger in multiselect (#982)
Added support for onSearch action trigger in multiselect
1 parent 44aff40 commit 376f97f

File tree

4 files changed

+177
-27
lines changed

4 files changed

+177
-27
lines changed

.changeset/fluffy-bananas-share.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@ensembleui/react-kitchen-sink": patch
3+
"@ensembleui/react-runtime": patch
4+
---
5+
6+
Added onSearch support in multiselect

apps/kitchen-sink/src/ensemble/screens/home.yaml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ View:
1010
ensemble.storage.set('bbb' , "0xffb91c1c")
1111
ensemble.storage.set('loading', true);
1212
console.log('>>> secret variable >>>', ensemble.secrets.dummyOauthSecret)
13+
ensemble.storage.set('products', []);
14+
ensemble.invokeAPI('getDummyProducts').then((res) => ensemble.storage.set('products', (res?.body?.users || []).map((i) => ({ ...i, name: i.firstName + ' ' + i.lastName }))));
1315
const res = await ensemble.invokeAPI('getDummyNumbers')
1416
await new Promise((resolve) => setTimeout(resolve, 5000))
1517
return res
@@ -452,6 +454,27 @@ View:
452454
<p id="tag2">Until recently, the prevailing view assumed lorem ipsum was born as a nonsense text. “It's not Latin, though it looks like it, and it actually says nothing,” Before & After magazine answered a curious reader, “Its ‘words’ loosely approximate the frequency with which letters occur in English, which is why at a glance it looks pretty real.”</p>
453455
</div>
454456
457+
- MultiSelect:
458+
id: multiSelect
459+
label: Search multiple from API or Storage
460+
placeholder: "Search or Select From Groups"
461+
labelKey: name
462+
valueKey: email
463+
data: ${ensemble.storage.get('products')}
464+
onSearch:
465+
executeCode: |
466+
ensemble.invokeAPI('getProducts', { search: search }).then((res) => {
467+
const users = res?.body?.users || [];
468+
console.log(users , "users");
469+
const newUsers = users.map((i) => ({ ...i, label: i.firstName + ' ' + i.lastName, name: i.firstName + ' ' + i.lastName, value: i.email }));
470+
console.log(newUsers , "newUsers");
471+
ensemble.storage.set('products', newUsers);
472+
});
473+
console.log("onSearch values: ", search);
474+
onChange:
475+
executeCode: |
476+
console.log("onChange values: ", search);
477+
455478
Global:
456479
scriptName: test.js
457480

@@ -490,3 +513,9 @@ API:
490513
uri: https://661e111b98427bbbef034208.mockapi.io/number?limit=10
491514
onResponse: |
492515
console.log('dummy number fetched')
516+
517+
getProducts:
518+
method: GET
519+
inputs:
520+
- search
521+
uri: "https://dummyjson.com/users/search?q=${search}"

packages/runtime/src/widgets/Form/MultiSelect.tsx

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ import {
1414
} from "@ensembleui/react-framework";
1515
import { PlusCircleOutlined } from "@ant-design/icons";
1616
import { Select as SelectComponent, Space, Form } from "antd";
17-
import { get, isArray, isEqual, isObject, isString } from "lodash-es";
17+
import { get, isArray, isEmpty, isEqual, isObject, isString } from "lodash-es";
18+
import { useDebounce } from "react-use";
1819
import { WidgetRegistry } from "../../registry";
1920
import { useEnsembleAction } from "../../runtime/hooks/useEnsembleAction";
2021
import type {
@@ -53,6 +54,9 @@ export type MultiSelectProps = {
5354
onChange?: EnsembleAction;
5455
/** OnItemSelect is deprecated. Please use onChange instead */
5556
onItemSelect?: EnsembleAction;
57+
onSearch?: {
58+
debounceMs: number;
59+
} & EnsembleAction;
5660
hintStyle?: EnsembleWidgetStyles;
5761
allowCreateOptions?: boolean;
5862
} & EnsembleWidgetProps<MultiSelectStyles> &
@@ -63,9 +67,11 @@ const MultiSelect: React.FC<MultiSelectProps> = (props) => {
6367
const [options, setOptions] = useState<MultiSelectOption[]>([]);
6468
const [newOption, setNewOption] = useState("");
6569
const [selectedValues, setSelectedValues] = useState<MultiSelectOption[]>();
70+
const [searchValue, setSearchValue] = useState<string | null>(null);
6671

6772
const action = useEnsembleAction(props.onChange);
6873
const onItemSelectAction = useEnsembleAction(props.onItemSelect);
74+
const onSearchAction = useEnsembleAction(props.onSearch);
6975

7076
const { rawData } = useTemplateData({ data });
7177
const { id, rootRef, values } = useRegisterBindings(
@@ -146,6 +152,10 @@ const MultiSelect: React.FC<MultiSelectProps> = (props) => {
146152
}, [selectedValues, formInstance]);
147153

148154
const handleSearch = (value: string): void => {
155+
if (props.onSearch) {
156+
setSearchValue(value);
157+
return;
158+
}
149159
const isOptionExist = options.find(
150160
(option) =>
151161
option.label.toString().toLowerCase().search(value.toLowerCase()) > -1,
@@ -158,6 +168,28 @@ const MultiSelect: React.FC<MultiSelectProps> = (props) => {
158168
}
159169
};
160170

171+
useDebounce(
172+
() => {
173+
if (onSearchAction?.callback && !isEmpty(searchValue)) {
174+
onSearchAction.callback({ search: searchValue });
175+
}
176+
},
177+
props?.onSearch?.debounceMs || 0,
178+
[searchValue],
179+
);
180+
181+
const handleFilterOption = (
182+
input: string,
183+
option?: MultiSelectOption,
184+
): boolean => {
185+
return (
186+
option?.label
187+
.toString()
188+
?.toLowerCase()
189+
?.startsWith(input.toLowerCase()) || false
190+
);
191+
};
192+
161193
// handle option change
162194
const handleChange = (
163195
value: MultiSelectOption[],
@@ -298,12 +330,7 @@ const MultiSelect: React.FC<MultiSelectProps> = (props) => {
298330
disabled={values?.enabled === false}
299331
dropdownRender={newOptionRender}
300332
dropdownStyle={values?.styles}
301-
filterOption={(input, option): boolean =>
302-
option?.label
303-
.toString()
304-
?.toLowerCase()
305-
?.startsWith(input.toLowerCase()) || false
306-
}
333+
filterOption={props.onSearch ? false : handleFilterOption}
307334
id={values?.id}
308335
labelRender={labelRender}
309336
mode={values?.allowCreateOptions ? "tags" : "multiple"}

packages/runtime/src/widgets/Form/__tests__/MultiSelect.test.tsx

Lines changed: 108 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -296,34 +296,20 @@ describe("MultiSelect Widget", () => {
296296
userEvent.click(screen.getByTitle("Option 4"));
297297
userEvent.click(screen.getByRole("combobox"));
298298

299+
const selector = ".ant-select-selection-item-content";
300+
299301
// Wait for the combobox to reflect the selected values
300302
await waitFor(() => {
301-
expect(
302-
screen.getByText("Option 2", {
303-
selector: ".ant-select-selection-item-content",
304-
}),
305-
).toBeVisible();
306-
expect(
307-
screen.getByText("Option 4", {
308-
selector: ".ant-select-selection-item-content",
309-
}),
310-
).toBeVisible();
303+
expect(screen.getByText("Option 2", { selector })).toBeVisible();
304+
expect(screen.getByText("Option 4", { selector })).toBeVisible();
311305
});
312306

313307
fireEvent.click(screen.getByText("change bindings"));
314308

315309
// Wait for the combobox to reflect the selected values
316310
await waitFor(() => {
317-
expect(
318-
screen.getByText("Option 1", {
319-
selector: ".ant-select-selection-item-content",
320-
}),
321-
).toBeVisible();
322-
expect(
323-
screen.getByText("Option 3", {
324-
selector: ".ant-select-selection-item-content",
325-
}),
326-
).toBeVisible();
311+
expect(screen.getByText("Option 1", { selector })).toBeVisible();
312+
expect(screen.getByText("Option 3", { selector })).toBeVisible();
327313
});
328314
});
329315

@@ -450,5 +436,107 @@ describe("MultiSelect Widget", () => {
450436
);
451437
});
452438
});
439+
440+
test("supports search in multiselect widget", async () => {
441+
render(
442+
<Form
443+
children={[
444+
{
445+
name: "MultiSelect",
446+
properties: {
447+
id: "searchInput",
448+
label: "Choose Option",
449+
data: [
450+
{ label: "Option 1", value: "option1" },
451+
{ label: "Option 2", value: "option2" },
452+
{ label: "Option 3", value: "option3" },
453+
{ label: "Option 4", value: "option4" },
454+
],
455+
},
456+
},
457+
]}
458+
id="form"
459+
/>,
460+
{ wrapper: FormTestWrapper },
461+
);
462+
463+
const selector = ".ant-select-item-option-content";
464+
465+
userEvent.click(screen.getByRole("combobox"));
466+
userEvent.type(document.querySelector("input") as HTMLElement, "Option 4");
467+
468+
// Wait for the combobox to reflect the selected values
469+
await waitFor(() => {
470+
expect(screen.getByText("Option 4", { selector })).toBeVisible();
471+
expect(screen.queryByText("Option 1")).not.toBeInTheDocument();
472+
expect(screen.queryByText("Option 2")).not.toBeInTheDocument();
473+
expect(screen.queryByText("Option 3")).not.toBeInTheDocument();
474+
});
475+
});
476+
477+
test("supports on search action callback in multiselect widget", async () => {
478+
render(
479+
<Form
480+
children={[
481+
{
482+
name: "MultiSelect",
483+
properties: {
484+
id: "searchInput",
485+
label: "Choose Option",
486+
data: `\${ensemble.storage.get('options')}`,
487+
onSearch: {
488+
executeCode: `
489+
const list = ensemble.storage.get('list') || [];
490+
const filtered = list.filter((option) => option?.label
491+
?.toString()
492+
?.toLowerCase()
493+
?.startsWith(search.toLowerCase()));
494+
ensemble.storage.set('options', filtered)
495+
`,
496+
},
497+
},
498+
},
499+
{
500+
name: "Button",
501+
properties: {
502+
label: "Set value",
503+
onTap: {
504+
executeCode: `
505+
const options = [
506+
{ label: "Option 1", value: "option1" },
507+
{ label: "Option 2", value: "option2" },
508+
{ label: "Option 3", value: "option3" },
509+
{ label: "Option 4", value: "option4" },
510+
{ label: "Option 44", value: "option44" },
511+
];
512+
ensemble.storage.set("options", options);
513+
ensemble.storage.set("list", options);
514+
`,
515+
},
516+
},
517+
},
518+
]}
519+
id="form"
520+
/>,
521+
{ wrapper: FormTestWrapper },
522+
);
523+
524+
const selector = ".ant-select-item-option-content";
525+
526+
const setValueButton = screen.getByText("Set value");
527+
fireEvent.click(setValueButton);
528+
529+
userEvent.click(screen.getByRole("combobox"));
530+
userEvent.type(document.querySelector("input") as HTMLElement, "Option 4");
531+
532+
// Wait for the combobox to reflect the selected values
533+
await waitFor(() => {
534+
expect(screen.getByText("Option 4", { selector })).toBeVisible();
535+
expect(screen.queryByText("Option 1")).not.toBeInTheDocument();
536+
expect(screen.queryByText("Option 2")).not.toBeInTheDocument();
537+
expect(screen.queryByText("Option 3")).not.toBeInTheDocument();
538+
expect(screen.queryByText("Option 44", { selector })).toBeVisible();
539+
});
540+
});
453541
});
454542
/* eslint-enable react/no-children-prop */

0 commit comments

Comments
 (0)