Skip to content
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
7 changes: 4 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,13 @@ dist
# misc
.DS_Store
*.pem
.vscode

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# local env files
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
Expand All @@ -42,3 +39,7 @@ yarn-error.log*

# schemas
apps/preview/public/schema/react/*.json

.env

.env.local
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"
7 changes: 5 additions & 2 deletions packages/runtime/src/widgets/Form/MultiSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
isObject,
isString,
toNumber,
uniqBy,
} from "lodash-es";
import { useDebounce } from "react-use";
import { WidgetRegistry } from "../../registry";
Expand Down Expand Up @@ -123,7 +124,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 +153,9 @@ const MultiSelect: React.FC<MultiSelectProps> = (props) => {
);
}

setOptions(tempOptions);
const uniqueOptions = uniqBy(tempOptions, "value");

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

// handle form instance
Expand Down
162 changes: 162 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,145 @@ describe("MultiSelect Widget", () => {
expect(screen.getByText("Anothe...")).toBeInTheDocument();
});
});

test.only("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.getByText("Bella Davis", {
selector: ".ant-select-selection-item-content",
}),
).toBeInTheDocument();
expect(
screen.getByText("Sophia Lee", {
selector: ".ant-select-selection-item-content",
}),
).toBeInTheDocument();
});
}, 10000);
});
/* eslint-enable react/no-children-prop */