## Description
This PR adds grouping capabilities to our dropdown control component
(using `rc-select`). Specifically:
- Introduces an `optionGroupConfig` object that maps each group key to a
label and collects relevant options under it.
- Defaults any ungrouped options to the “Others” group if no matching
group is found.
- Includes refactoring to maintain backward compatibility for
non-grouped dropdown usage.
Additionally:
- New tests are added to validate grouped dropdown behaviour.
- Existing multi-select and clear-all functionality is unaffected.
Sample config for the grouping to be enabled
```
{
"label": "Command",
"description": "Choose method you would like to use to query the database",
"configProperty": "actionConfiguration.formData.command.data",
"controlType": "DROP_DOWN",
"initialValue": "FIND",
"options": [
{
"label": "Find document(s)",
"value": "FIND",
"optionGroupType": "testGrp1"
},
{
"label": "Insert document(s)",
"value": "INSERT",
"optionGroupType": "testGrp1"
},
{
"label": "Update document(s)",
"value": "UPDATE",
"optionGroupType": "testGrp2"
},
{
"label": "Delete document(s)",
"value": "DELETE",
"optionGroupType": "testGrp2"
},
{
"label": "Count",
"value": "COUNT",
"optionGroupType": "testGrp2"
},
{
"label": "Distinct",
"value": "DISTINCT",
"optionGroupType": "testGrp3"
},
{
"label": "Aggregate",
"value": "AGGREGATE",
"optionGroupType": "testGrp3"
},
{
"label": "Raw",
"value": "RAW",
"optionGroupType": "testGrp3"
}
],
"optionGroupConfig": {
"testGrp1": {
"label": "Group 1",
"children": []
},
"testGrp2": {
"label": "Group 2",
"children": []
},
"testGrp3": {
"label": "Group 3",
"children": []
}
}
}
```
Fixes #38079
## Automation
/ok-to-test tags="@tag.Sanity, @tag.IDE"
### 🔍 Cypress test results
<!-- This is an auto-generated comment: Cypress test results -->
> [!TIP]
> 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉
> Workflow run:
<https://github.com/appsmithorg/appsmith/actions/runs/13059919318>
> Commit: f08c31b3e5d81318144e3a71d652526fd1b01a00
> <a
href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=13059919318&attempt=1"
target="_blank">Cypress dashboard</a>.
> Tags: `@tag.Sanity, @tag.IDE`
> Spec:
> <hr>Thu, 30 Jan 2025 20:22:48 UTC
<!-- end of auto-generated comment: Cypress test results -->
## Communication
Should the DevRel and Marketing teams inform users about this change?
- [ ] Yes
- [x] No
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
## Release Notes
- **New Features**
- Added option grouping functionality to the Select component.
- Introduced the ability to organize dropdown options into labeled
groups.
- Enhanced dropdown visual hierarchy with group-based option display.
- **Improvements**
- Updated Select component type definitions to support option grouping.
- Added CSS styles for improved presentation of option groups and
grouped options.
- **Testing**
- Added comprehensive test coverage for dropdown option grouping
functionality.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
222 lines
5.8 KiB
TypeScript
222 lines
5.8 KiB
TypeScript
import React from "react";
|
|
import { render, screen, waitFor, fireEvent } from "test/testUtils";
|
|
import DropDownControl from "./DropDownControl";
|
|
import { reduxForm } from "redux-form";
|
|
import "@testing-library/jest-dom";
|
|
import { Provider } from "react-redux";
|
|
import configureStore from "redux-mock-store";
|
|
|
|
const mockStore = configureStore([]);
|
|
|
|
const initialValues = {
|
|
actionConfiguration: {
|
|
testPath: ["option1", "option2"],
|
|
},
|
|
};
|
|
|
|
// TODO: Fix this the next time the file is edited
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
function TestForm(props: any) {
|
|
return <div>{props.children}</div>;
|
|
}
|
|
|
|
const ReduxFormDecorator = reduxForm({
|
|
form: "TestForm",
|
|
initialValues,
|
|
})(TestForm);
|
|
|
|
const mockOptions = [
|
|
{ label: "Option 1", value: "option1", children: "Option 1" },
|
|
{ label: "Option 2", value: "option2", children: "Option 2" },
|
|
{ label: "Option 3", value: "option3", children: "Option 3" },
|
|
];
|
|
|
|
const mockAction = {
|
|
type: "API_ACTION",
|
|
name: "Test API Action",
|
|
datasource: {
|
|
id: "datasource1",
|
|
name: "Datasource 1",
|
|
},
|
|
actionConfiguration: {
|
|
body: "",
|
|
headers: [],
|
|
testPath: ["option1", "option2"],
|
|
},
|
|
};
|
|
|
|
const dropDownProps = {
|
|
options: mockOptions,
|
|
placeholderText: "Select Columns",
|
|
isMultiSelect: true,
|
|
configProperty: "actionConfiguration.testPath",
|
|
controlType: "PROJECTION",
|
|
propertyValue: "",
|
|
label: "Columns",
|
|
id: "column",
|
|
formName: "",
|
|
isValid: true,
|
|
formValues: mockAction,
|
|
isLoading: false,
|
|
};
|
|
|
|
describe("DropDownControl", () => {
|
|
// TODO: Fix this the next time the file is edited
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
let store: any;
|
|
|
|
beforeEach(() => {
|
|
store = mockStore({
|
|
form: {
|
|
TestForm: {
|
|
values: initialValues,
|
|
},
|
|
},
|
|
appState: {},
|
|
});
|
|
});
|
|
it("should renders dropdownControl and options properly", async () => {
|
|
render(
|
|
<Provider store={store}>
|
|
<ReduxFormDecorator>
|
|
<DropDownControl {...dropDownProps} />
|
|
</ReduxFormDecorator>
|
|
</Provider>,
|
|
);
|
|
|
|
const dropdownSelect = await waitFor(async () =>
|
|
screen.findByTestId("t--dropdown-actionConfiguration.testPath"),
|
|
);
|
|
|
|
expect(dropdownSelect).toBeInTheDocument();
|
|
|
|
const options = screen.getAllByText(/Optio.../);
|
|
const optionCount = options.length;
|
|
|
|
expect(optionCount).toBe(2);
|
|
});
|
|
|
|
it("should clear all selected options", async () => {
|
|
render(
|
|
<Provider store={store}>
|
|
<ReduxFormDecorator>
|
|
<DropDownControl {...dropDownProps} />
|
|
</ReduxFormDecorator>
|
|
</Provider>,
|
|
);
|
|
|
|
const clearAllButton = document.querySelector(".rc-select-clear");
|
|
|
|
expect(clearAllButton).toBeInTheDocument();
|
|
|
|
fireEvent.click(clearAllButton!);
|
|
|
|
await waitFor(() => {
|
|
const options = screen.queryAllByText(/Option.../);
|
|
|
|
expect(options.length).toBe(0);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("DropDownControl grouping tests", () => {
|
|
// TODO: Fix this the next time the file is edited
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
let store: any;
|
|
|
|
beforeEach(() => {
|
|
store = mockStore({
|
|
form: {
|
|
GroupingTestForm: {
|
|
values: {
|
|
actionConfiguration: { testPath: [] },
|
|
},
|
|
},
|
|
},
|
|
});
|
|
});
|
|
|
|
it("should render grouped options correctly when optionGroupConfig is provided", async () => {
|
|
// These config & options demonstrate grouping
|
|
const mockOptionGroupConfig = {
|
|
testGrp1: {
|
|
label: "Group 1",
|
|
children: [],
|
|
},
|
|
testGrp2: {
|
|
label: "Group 2",
|
|
children: [],
|
|
},
|
|
};
|
|
|
|
const mockGroupedOptions = [
|
|
{
|
|
label: "Option 1",
|
|
value: "option1",
|
|
children: "Option 1",
|
|
optionGroupType: "testGrp1",
|
|
},
|
|
{
|
|
label: "Option 2",
|
|
value: "option2",
|
|
children: "Option 2",
|
|
// Intentionally no optionGroupType => Should fall under default "Others" group
|
|
},
|
|
{
|
|
label: "Option 3",
|
|
value: "option3",
|
|
children: "Option 3",
|
|
optionGroupType: "testGrp2",
|
|
},
|
|
];
|
|
|
|
const props = {
|
|
...dropDownProps,
|
|
controlType: "DROP_DOWN",
|
|
options: mockGroupedOptions,
|
|
optionGroupConfig: mockOptionGroupConfig,
|
|
};
|
|
|
|
render(
|
|
<Provider store={store}>
|
|
<ReduxFormDecorator>
|
|
<DropDownControl {...props} />
|
|
</ReduxFormDecorator>
|
|
</Provider>,
|
|
);
|
|
|
|
// 1. Grab the dropdown container
|
|
const dropdownSelect = await waitFor(async () =>
|
|
screen.findByTestId("t--dropdown-actionConfiguration.testPath"),
|
|
);
|
|
|
|
expect(dropdownSelect).toBeInTheDocument();
|
|
|
|
// 2. Click to open the dropdown
|
|
// @ts-expect-error: the test will fail if component doesn't exist
|
|
fireEvent.mouseDown(dropdownSelect.querySelector(".rc-select-selector"));
|
|
|
|
// 3. We expect to see group labels from the config
|
|
// 'Group 1' & 'Group 2' come from the mockOptionGroupConfig
|
|
const group1Label = await screen.findByText("Group 1");
|
|
const group2Label = await screen.findByText("Group 2");
|
|
|
|
expect(group1Label).toBeInTheDocument();
|
|
expect(group2Label).toBeInTheDocument();
|
|
|
|
// 4. Check that the 'Others' group also exists because at least one option did not have optionGroupType
|
|
// The default group label is 'Others' (in your code)
|
|
const othersGroupLabel = await screen.findByText("Others");
|
|
|
|
expect(othersGroupLabel).toBeInTheDocument();
|
|
|
|
// 5. Confirm the correct distribution of options
|
|
// For group1 -> "Option 1"
|
|
expect(screen.getByText("Option 1")).toBeInTheDocument();
|
|
// For group2 -> "Option 3"
|
|
expect(screen.getByText("Option 3")).toBeInTheDocument();
|
|
// For default "Others" -> "Option 2"
|
|
expect(screen.getByText("Option 2")).toBeInTheDocument();
|
|
});
|
|
});
|