Skip to content

Commit

Permalink
feat: select widget lazy loading (#38867)
Browse files Browse the repository at this point in the history
## Description
This PR adds capability of server side pagination to the dropdown form
component. There is another PR in works to add server side search. To
ensure both grouping and pagination work correctly, the dropdown control
component is refactored by adding memoization and fixing some rendering
issues.


Fixes #38079 

## Automation

/ok-to-test tags="@tag.All"

### 🔍 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/13306719132>
> Commit: 01f4649
> <a
href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=13306719132&attempt=1"
target="_blank">Cypress dashboard</a>.
> Tags: `@tag.All`
> Spec:
> <hr>Thu, 13 Feb 2025 12:58:12 UTC
<!-- end of auto-generated comment: Cypress test results  -->


## Communication
Should the DevRel and Marketing teams inform users about this change?
- [ ] Yes
- [ ] No


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Enabled dynamic pagination for form options, allowing users to load
additional choices smoothly.
- Enhanced dropdown controls for both single and multi-select modes with
improved responsiveness and clearer grouping.
- Improved form evaluation processes for a more seamless and performant
user experience.
- Introduced new functionality for fetching paginated dynamic values,
enhancing the overall data handling experience.
- Added new function to retrieve conditional output based on form
configuration.

- **Bug Fixes**
- Improved error handling and logging for dynamic value fetching
processes.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
  • Loading branch information
ayushpahwa authored Feb 13, 2025
1 parent 7764d3d commit fdf7793
Show file tree
Hide file tree
Showing 8 changed files with 701 additions and 332 deletions.
20 changes: 20 additions & 0 deletions app/client/src/actions/evaluationActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ import {
LINT_REDUX_ACTIONS,
LOG_REDUX_ACTIONS,
} from "ee/actions/evaluationActionsList";
import type {
ConditionalOutput,
DynamicValues,
} from "reducers/evaluationReducers/formEvaluationReducer";

export const shouldTriggerEvaluation = (action: ReduxAction<unknown>) => {
return (
Expand Down Expand Up @@ -126,3 +130,19 @@ const FORCE_EVAL_ACTIONS = {
export const shouldForceEval = (action: ReduxAction<unknown>) => {
return !!FORCE_EVAL_ACTIONS[action.type];
};

export const fetchFormDynamicValNextPage = (payload?: {
value: ConditionalOutput;
dynamicFetchedValues: DynamicValues;
actionId: string;
datasourceId: string;
pluginId: string;
identifier: string;
}) => {
if (payload) {
return {
type: ReduxActionTypes.FETCH_FORM_DYNAMIC_VAL_NEXT_PAGE_INIT,
payload,
};
}
};
4 changes: 4 additions & 0 deletions app/client/src/ce/constants/ReduxActionConstants.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -751,6 +751,10 @@ const UQIFormActionTypes = {
FETCH_TRIGGER_VALUES_SUCCESS: "FETCH_TRIGGER_VALUES_SUCCESS",
SET_TRIGGER_VALUES_LOADING: "SET_TRIGGER_VALUES_LOADING",
FORM_EVALUATION_EMPTY_BUFFER: "FORM_EVALUATION_EMPTY_BUFFER",
FETCH_FORM_DYNAMIC_VAL_NEXT_PAGE_INIT:
"FETCH_FORM_DYNAMIC_VAL_NEXT_PAGE_INIT",
FETCH_FORM_DYNAMIC_VAL_NEXT_PAGE_SUCCESS:
"FETCH_FORM_DYNAMIC_VAL_NEXT_PAGE_SUCCESS",
};

const ActionActionTypes = {
Expand Down
5 changes: 4 additions & 1 deletion app/client/src/ce/sagas/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ import debuggerSagas from "sagas/DebuggerSagas";
import editorContextSagas from "sagas/editorContextSagas";
import errorSagas from "sagas/ErrorSagas";
import evaluationsSaga from "sagas/EvaluationsSaga";
import formEvaluationChangeListener from "sagas/FormEvaluationSaga";
import formEvaluationChangeListener, {
formEvaluationSagas,
} from "sagas/FormEvaluationSaga";
import gitSyncSagas from "sagas/GitSyncSagas";
import globalSearchSagas from "sagas/GlobalSearchSagas";
import initSagas from "sagas/InitSagas";
Expand Down Expand Up @@ -81,6 +83,7 @@ export const sagas = [
onboardingSagas,
actionExecutionChangeListeners,
formEvaluationChangeListener,
formEvaluationSagas,
globalSearchSagas,
debuggerSagas,
saaSPaneSagas,
Expand Down
210 changes: 143 additions & 67 deletions app/client/src/components/formControls/DropDownControl.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,12 @@ import { reduxForm } from "redux-form";
import "@testing-library/jest-dom";
import { Provider } from "react-redux";
import configureStore from "redux-mock-store";
import type { SelectOptionProps } from "@appsmith/ads";

const mockStore = configureStore([]);

const initialValues = {
actionConfiguration: {
testPath: ["option1", "option2"],
},
actionConfiguration: { testPath: ["option1", "option2"] },
};

// TODO: Fix this the next time the file is edited
Expand All @@ -20,10 +19,9 @@ function TestForm(props: any) {
return <div>{props.children}</div>;
}

const ReduxFormDecorator = reduxForm({
form: "TestForm",
initialValues,
})(TestForm);
const ReduxFormDecorator = reduxForm({ form: "TestForm", initialValues })(
TestForm,
);

const mockOptions = [
{ label: "Option 1", value: "option1", children: "Option 1" },
Expand All @@ -34,10 +32,7 @@ const mockOptions = [
const mockAction = {
type: "API_ACTION",
name: "Test API Action",
datasource: {
id: "datasource1",
name: "Datasource 1",
},
datasource: { id: "datasource1", name: "Datasource 1" },
actionConfiguration: {
body: "",
headers: [],
Expand Down Expand Up @@ -68,14 +63,15 @@ describe("DropDownControl", () => {

beforeEach(() => {
store = mockStore({
form: {
TestForm: {
values: initialValues,
},
},
form: { TestForm: { values: initialValues } },
appState: {},
});
});

afterEach(() => {
jest.clearAllMocks();
});

it("should renders dropdownControl and options properly", async () => {
render(
<Provider store={store}>
Expand Down Expand Up @@ -118,6 +114,107 @@ describe("DropDownControl", () => {
expect(options.length).toBe(0);
});
});

it("should handle single select mode correctly", async () => {
const singleSelectProps = {
...dropDownProps,
isMultiSelect: false,
configProperty: "actionConfiguration.singlePath",
inputIcon: undefined,
showArrow: undefined,
};

render(
<Provider store={store}>
<ReduxFormDecorator>
<DropDownControl {...singleSelectProps} />
</ReduxFormDecorator>
</Provider>,
);

// Wait for component to be fully rendered
const dropdownSelect = await screen.findByTestId(
"t--dropdown-actionConfiguration.singlePath",
);

// Open dropdown
fireEvent.mouseDown(dropdownSelect.querySelector(".rc-select-selector")!);

// Wait for dropdown to be visible
await waitFor(() => {
expect(screen.getByRole("listbox")).toBeVisible();
});

// Select Option 1
const option = screen.getByRole("option", { name: "Option 1" });

fireEvent.click(option);

// Wait for selection to be applied and verify
await waitFor(() => {
// Check if the selection is displayed
const selectedItem = screen.getByText("Option 1", {
selector: ".rc-select-selection-item",
});

expect(selectedItem).toBeInTheDocument();
});

// Check if the option is marked as selected
const selectedOption = screen.getByRole("option", { name: "Option 1" });

expect(selectedOption).toHaveClass("rc-select-item-option-selected");

// Check that other options are not selected
const options = screen.getAllByRole("option");
const selectedCount = options.filter((opt: SelectOptionProps) =>
opt.classList.contains("rc-select-item-option-selected"),
).length;

expect(selectedCount).toBe(1);
});

it("should handle multi-select mode correctly", async () => {
render(
<Provider store={store}>
<ReduxFormDecorator>
<DropDownControl {...dropDownProps} />
</ReduxFormDecorator>
</Provider>,
);

const dropdownSelect = await screen.findByTestId(
"t--dropdown-actionConfiguration.testPath",
);

// Open dropdown
fireEvent.mouseDown(dropdownSelect.querySelector(".rc-select-selector")!);

await waitFor(() => {
expect(screen.getByRole("listbox")).toBeVisible();
});

// Verify initial selections (Option 1 and Option 2 are selected from initialValues)
const initialSelectedOptions = screen
.getAllByRole("option")
.filter((opt) => opt.getAttribute("aria-selected") === "true");

expect(initialSelectedOptions).toHaveLength(2);
});

it("should show placeholder if no option is selected", async () => {
const updatedProps = { ...dropDownProps, options: [] };

render(
<Provider store={store}>
<ReduxFormDecorator>
<DropDownControl {...updatedProps} />
</ReduxFormDecorator>
</Provider>,
);

expect(screen.getByText("Select Columns")).toBeInTheDocument();
});
});

describe("DropDownControl grouping tests", () => {
Expand All @@ -128,53 +225,41 @@ describe("DropDownControl grouping tests", () => {
beforeEach(() => {
store = mockStore({
form: {
GroupingTestForm: {
values: {
actionConfiguration: { testPath: [] },
},
},
GroupingTestForm: { values: { actionConfiguration: { testPath: [] } } },
},
});
});

it("should render grouped options correctly when optionGroupConfig is provided", async () => {
// These config & options demonstrate grouping
it("should render grouped options correctly", async () => {
const mockOptionGroupConfig = {
testGrp1: {
label: "Group 1",
children: [],
},
testGrp2: {
label: "Group 2",
children: [],
},
group1: { label: "Group 1", children: [] },
group2: { label: "Group 2", children: [] },
};

const mockGroupedOptions = [
const mockOptions = [
{
label: "Option 1",
value: "option1",
children: "Option 1",
optionGroupType: "testGrp1",
value: "1",
optionGroupType: "group1",
children: [],
},
{
label: "Option 2",
value: "option2",
children: "Option 2",
// Intentionally no optionGroupType => Should fall under default "Others" group
value: "2",
children: [],
// No group - should go to Others
},
{
label: "Option 3",
value: "option3",
children: "Option 3",
optionGroupType: "testGrp2",
value: "3",
optionGroupType: "group2",
children: [],
},
];

const props = {
...dropDownProps,
controlType: "DROP_DOWN",
options: mockGroupedOptions,
options: mockOptions,
optionGroupConfig: mockOptionGroupConfig,
};

Expand All @@ -191,33 +276,24 @@ describe("DropDownControl grouping tests", () => {
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"));
// Open dropdown
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");
// Verify group headers are present
await waitFor(() => {
expect(screen.getByText("Group 1")).toBeInTheDocument();
});
expect(screen.getByText("Group 2")).toBeInTheDocument();
expect(screen.getByText("Others")).toBeInTheDocument();

expect(othersGroupLabel).toBeInTheDocument();
// Verify options are in correct groups
const group1Option = screen.getByText("Option 1");
const group2Option = screen.getByText("Option 3");
const othersOption = screen.getByText("Option 2");

// 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();
expect(group1Option).toBeInTheDocument();
expect(group2Option).toBeInTheDocument();
expect(othersOption).toBeInTheDocument();
});
});

Expand Down
Loading

0 comments on commit fdf7793

Please sign in to comment.