Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/nasty-cats-provide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@ensembleui/react-kitchen-sink": patch
"@ensembleui/react-runtime": patch
---

Keep selected options in search multiselect
48 changes: 48 additions & 0 deletions apps/kitchen-sink/src/ensemble/screens/help.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,20 @@
View:
onLoad:
executeCode: |
ensemble.storage.set('testlistoptions', [
{ label: 'William Smith', value: 'william_smith' },
{ label: 'Evelyn Johnson', value: 'evelyn_johnson' },
{ label: 'Liam Brown', value: 'liam_brown' },
{ label: 'Bella Davis', value: 'bella_davis' },
{ label: 'James Wilson', value: 'james_wilson' },
{ label: 'Zachary Taylor', value: 'zachary_taylor' },
{ label: 'Nolan Martinez', value: 'nolan_martinez' },
{ label: 'Emma Thompson', value: 'emma_thompson' },
{ label: 'Oliver White', value: 'oliver_white' },
{ label: 'Sophia Lee', value: 'sophia_lee' },
])
ensemble.storage.set('testoptions', [])

styles:
scrollableView: true

Expand Down Expand Up @@ -28,6 +44,31 @@ View:
inputs:
input1: "hello"
input2: "world"

- Button:
label: toggle modal
onTap:
showDialog:
options:
minWidth: 500px
body:
MultiSelect:
label: test
data: ${ensemble.storage.get('testoptions')}
labelKey: label
valueKey: value
value: ${ensemble.storage.get('testselect') || []}
onChange:
executeCode: |
ensemble.storage.set('testselect', option)
onSearch:
executeCode: |
const tempOptions = ensemble.storage.get('testlistoptions')
const filteredResult = tempOptions.filter(option =>
option.label.toLowerCase().startsWith(search.toLowerCase())
)
ensemble.storage.set('testoptions', filteredResult)

- Text:
styles:
names: heading-1
Expand Down Expand Up @@ -75,3 +116,10 @@ View:
styles:
maxWidth: 500px
source: https://img.huffingtonpost.com/asset/5ab4d4ac2000007d06eb2c56.jpeg?cache=sih0jwle4e&ops=1910_1000

API:
getProducts:
method: GET
inputs:
- search
uri: "https://dummyjson.com/users/search?q=${search}&limit=10"
12 changes: 10 additions & 2 deletions packages/runtime/src/widgets/Form/MultiSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ const MultiSelect: React.FC<MultiSelectProps> = (props) => {

// load data and items
useEffect(() => {
const tempOptions: MultiSelectOption[] = [];
const tempOptions: MultiSelectOption[] = values?.selectedValues || [];

if (isArray(rawData)) {
tempOptions.push(
Expand Down Expand Up @@ -152,7 +152,15 @@ const MultiSelect: React.FC<MultiSelectProps> = (props) => {
);
}

setOptions(tempOptions);
const uniqueOptions = tempOptions.reduce((acc, current) => {
const x = acc.find((item) => item.value === current.value);
if (!x) {
return acc.concat([current]);
}
return acc;
}, [] as MultiSelectOption[]);

setOptions(uniqueOptions);
}, [rawData, values?.labelKey, values?.valueKey, values?.items]);

// handle form instance
Expand Down
154 changes: 154 additions & 0 deletions packages/runtime/src/widgets/Form/__tests__/MultiSelect.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,30 @@
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import "@testing-library/jest-dom";
import userEvent from "@testing-library/user-event";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { type ReactNode } from "react";
import { BrowserRouter } from "react-router-dom";
import { Form } from "../../index";
import { FormTestWrapper } from "./__shared__/fixtures";
import { EnsembleScreen } from "../../../runtime/screen";

const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
});

interface BrowserRouterProps {
children: ReactNode;
}

const BrowserRouterWrapper = ({ children }: BrowserRouterProps) => (
<QueryClientProvider client={queryClient}>
<BrowserRouter>{children}</BrowserRouter>
</QueryClientProvider>
);

const defaultFormButton = [
{
Expand Down Expand Up @@ -645,5 +667,137 @@ describe("MultiSelect Widget", () => {
expect(screen.getByText("Anothe...")).toBeInTheDocument();
});
});

test("multiselect with search action", async () => {
render(
<EnsembleScreen
screen={{
name: "test_cache",
id: "test_cache",
onLoad: {
executeCode: {
body: `
ensemble.storage.set('test_list_options', [
{ label: 'William Smith', value: 'william_smith' },
{ label: 'Evelyn Johnson', value: 'evelyn_johnson' },
{ label: 'Liam Brown', value: 'liam_brown' },
{ label: 'Bella Davis', value: 'bella_davis' },
{ label: 'James Wilson', value: 'james_wilson' },
{ label: 'Zachary Taylor', value: 'zachary_taylor' },
{ label: 'Nolan Martinez', value: 'nolan_martinez' },
{ label: 'Emma Thompson', value: 'emma_thompson' },
{ label: 'Oliver White', value: 'oliver_white' },
{ label: 'Sophia Lee', value: 'sophia_lee' },
])
ensemble.storage.set('test_options', [])
`,
},
},
body: {
name: "Button",
properties: {
label: "Show Dialog",
onTap: {
showDialog: {
widget: {
Column: {
children: [
{
Text: { text: "This is modal" },
},
{
MultiSelect: {
data: `\${ensemble.storage.get('test_options')}`,
labelKey: "label",
valueKey: "value",
value: `\${ensemble.storage.get('test_select') || []}`,
onChange: {
executeCode: `
ensemble.storage.set('test_select', option)
console.log('test_select', ensemble.storage.get('test_select'))
`,
},
onSearch: {
executeCode: `
const tempOptions = ensemble.storage.get('test_list_options')
const filteredResult = tempOptions.filter(option =>
option.label.toLowerCase().startsWith(search.toLowerCase())
)
ensemble.storage.set('test_options', filteredResult)
`,
},
},
},
{
Button: {
label: "Close modal",
onTap: {
executeCode: "ensemble.closeAllDialogs()",
},
},
},
],
},
},
},
},
},
},
}}
/>,
{ wrapper: BrowserRouterWrapper },
);

const option = { selector: ".ant-select-item-option-content" };
const selected = { selector: ".ant-select-selection-item-content" };

userEvent.click(screen.getByText("Show Dialog"));

await waitFor(() => {
expect(screen.getByText("This is modal")).toBeInTheDocument();
});

userEvent.type(screen.getByRole("combobox"), "Bella");

await waitFor(() => {
expect(screen.getByText("Bella Davis", option)).toBeInTheDocument();
});

userEvent.click(screen.getByText("Bella Davis", option));

await waitFor(() => {
expect(screen.getByText("Bella Davis", selected)).toBeVisible();
});

userEvent.click(screen.getByText("Close modal"));

// Open 2nd time to check if the selected values are retained

userEvent.click(screen.getByText("Show Dialog"));

userEvent.type(screen.getByRole("combobox"), "Sophia");

await waitFor(() => {
expect(screen.getByText("Sophia Lee", option)).toBeInTheDocument();
});

userEvent.click(screen.getByText("Sophia Lee", option));

await waitFor(() => {
expect(screen.queryByText("Bella Davis", selected)).toBeVisible();
expect(screen.queryByText("Sophia Lee", selected)).toBeVisible();
});

userEvent.click(screen.getByText("Close modal"));

// Open 3rd time to check if the selected values are retained

userEvent.click(screen.getByText("Show Dialog"));

await waitFor(() => {
expect(screen.queryByText("Bella Davis", selected)).toBeVisible();
expect(screen.queryByText("Sophia Lee", selected)).toBeVisible();
});
}, 10000);
});
/* eslint-enable react/no-children-prop */