## Description This PR fixes search issues with dropdown UQI control. Steps to reproduce: 1. Create zendesk datasource 2. Create a query 3. Check commands dropdown 4. Try searching for "Create ticket" 5. It should show the relevant options Fixes #39164 _or_ Fixes `Issue URL` > [!WARNING] > _If no issue exists, please create an issue first, and check with the maintainers if the issue is valid._ ## 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/13389048508> > Commit: 261eb4369682e2e65fae19101403002debd90c91 > <a href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=13389048508&attempt=1" target="_blank">Cypress dashboard</a>. > Tags: `@tag.All` > Spec: > <hr>Tue, 18 Feb 2025 12:17:03 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 - **New Features** - Enhanced the dropdown control by introducing a search capability. Users can now type keywords to filter options based on their labels, making it easier and faster to locate the desired option while preserving the existing functionality. <!-- end of auto-generated comment: release notes by coderabbit.ai --> Co-authored-by: “sneha122” <“sneha@appsmith.com”>
615 lines
19 KiB
TypeScript
615 lines
19 KiB
TypeScript
import React from "react";
|
|
import memoizeOne from "memoize-one";
|
|
import { get, isEmpty, isNil, uniqBy } from "lodash";
|
|
import {
|
|
Field,
|
|
change,
|
|
getFormValues,
|
|
type WrappedFieldInputProps,
|
|
type WrappedFieldMetaProps,
|
|
} from "redux-form";
|
|
import { connect } from "react-redux";
|
|
import type { AppState } from "ee/reducers";
|
|
import type { ControlProps } from "./BaseControl";
|
|
import BaseControl from "./BaseControl";
|
|
import type { ControlType } from "constants/PropertyControlConstants";
|
|
import {
|
|
FormDataPaths,
|
|
matchExact,
|
|
MATCH_ACTION_CONFIG_PROPERTY,
|
|
} from "workers/Evaluation/formEval";
|
|
import type { Action } from "entities/Action";
|
|
import type { SelectOptionProps } from "@appsmith/ads";
|
|
import { Icon, Option, OptGroup, Select } from "@appsmith/ads";
|
|
import { getFormConfigConditionalOutput } from "selectors/formSelectors";
|
|
import { fetchFormDynamicValNextPage } from "actions/evaluationActions";
|
|
import { objectKeys } from "@appsmith/utils";
|
|
import type {
|
|
ConditionalOutput,
|
|
DynamicValues,
|
|
} from "reducers/evaluationReducers/formEvaluationReducer";
|
|
|
|
export interface DropDownGroupedOptions {
|
|
label: string;
|
|
children: SelectOptionProps[];
|
|
}
|
|
|
|
/**
|
|
* Groups dropdown options based on provided configuration
|
|
* The grouping is only done if the optionGroupConfig is provided
|
|
* The default group is "others" if not provided
|
|
* @param {SelectOptionProps[]} options - Array of options to be grouped
|
|
* @param {Record<string, DropDownGroupedOptions>} [optionGroupConfig] - Configuration for grouping options
|
|
* @returns {DropDownGroupedOptions[] | null} Grouped options array or null if no grouping needed
|
|
*/
|
|
function buildGroupedOptions(
|
|
options: SelectOptionProps[],
|
|
optionGroupConfig?: Record<string, DropDownGroupedOptions>,
|
|
): DropDownGroupedOptions[] | null {
|
|
if (!optionGroupConfig) return null;
|
|
|
|
const defaultGroupKey = "others";
|
|
const defaultGroupConfig: DropDownGroupedOptions = {
|
|
label: "Others",
|
|
children: [],
|
|
};
|
|
|
|
// Copy group config so we don't mutate the original
|
|
const groupMap = { ...optionGroupConfig };
|
|
|
|
// Re-initialize every group's children to an empty array
|
|
objectKeys(groupMap).forEach((key) => {
|
|
groupMap[key] = { ...groupMap[key], children: [] };
|
|
});
|
|
|
|
// Ensure we have an "others" group
|
|
if (!Object.hasOwn(groupMap, defaultGroupKey)) {
|
|
groupMap[defaultGroupKey] = { ...defaultGroupConfig };
|
|
} else {
|
|
// Re-initialize "others" group's children to an empty array
|
|
groupMap[defaultGroupKey] = { ...groupMap[defaultGroupKey], children: [] };
|
|
}
|
|
|
|
// Distribute each option to the correct group
|
|
options.forEach((opt) => {
|
|
const groupKey =
|
|
Object.hasOwn(opt, "optionGroupType") && opt.optionGroupType
|
|
? opt.optionGroupType
|
|
: defaultGroupKey;
|
|
|
|
// If the groupKey doesn't exist in config, fall back to "others"
|
|
if (!Object.hasOwn(groupMap, groupKey)) {
|
|
groupMap[defaultGroupKey].children.push(opt);
|
|
|
|
return;
|
|
}
|
|
|
|
groupMap[groupKey].children.push(opt);
|
|
});
|
|
|
|
// Return only groups that actually have children
|
|
const grouped: DropDownGroupedOptions[] = [];
|
|
|
|
objectKeys(groupMap).forEach((key) => {
|
|
const group = groupMap[key];
|
|
|
|
if (group.children.length > 0) grouped.push(group);
|
|
});
|
|
|
|
return grouped;
|
|
}
|
|
|
|
const memoizedBuildGroupedOptions = memoizeOne(buildGroupedOptions);
|
|
|
|
export interface DropDownControlProps extends ControlProps {
|
|
options: SelectOptionProps[];
|
|
optionGroupConfig?: Record<string, DropDownGroupedOptions>;
|
|
optionWidth?: string;
|
|
maxTagCount?: number;
|
|
placeholderText: string;
|
|
propertyValue: string;
|
|
subtitle?: string;
|
|
isMultiSelect?: boolean;
|
|
isAllowClear?: boolean;
|
|
isSearchable?: boolean;
|
|
fetchOptionsConditionally?: boolean;
|
|
isLoading: boolean;
|
|
formValues: Partial<Action>;
|
|
setFirstOptionAsDefault?: boolean;
|
|
nextPageNeeded?: boolean;
|
|
appendGroupIdentifierToValue?: boolean;
|
|
paginationPayload?: {
|
|
value: ConditionalOutput;
|
|
dynamicFetchedValues: DynamicValues;
|
|
actionId: string;
|
|
datasourceId: string;
|
|
pluginId: string;
|
|
identifier: string;
|
|
};
|
|
}
|
|
|
|
interface ReduxDispatchProps {
|
|
updateConfigPropertyValue: (
|
|
formName: string,
|
|
field: string,
|
|
value: unknown,
|
|
) => void;
|
|
fetchFormTriggerNextPage: (paginationPayload: {
|
|
value: ConditionalOutput;
|
|
dynamicFetchedValues: DynamicValues;
|
|
actionId: string;
|
|
datasourceId: string;
|
|
pluginId: string;
|
|
identifier: string;
|
|
}) => void;
|
|
}
|
|
|
|
type Props = DropDownControlProps & ReduxDispatchProps;
|
|
|
|
class DropDownControl extends BaseControl<Props> {
|
|
componentDidUpdate(prevProps: Props) {
|
|
// If dependencies changed in multi-select, reset values
|
|
if (this.props.fetchOptionsConditionally && this.props.isMultiSelect) {
|
|
const dependencies = matchExact(
|
|
MATCH_ACTION_CONFIG_PROPERTY,
|
|
this.props.conditionals?.fetchDynamicValues?.condition,
|
|
);
|
|
let hasDependenciesChanged = false;
|
|
|
|
if (dependencies?.length) {
|
|
dependencies.forEach((depPath) => {
|
|
const prevValue = get(prevProps.formValues, depPath);
|
|
const currValue = get(this.props.formValues, depPath);
|
|
|
|
if (prevValue !== currValue) hasDependenciesChanged = true;
|
|
});
|
|
}
|
|
|
|
if (hasDependenciesChanged) {
|
|
this.props.updateConfigPropertyValue(
|
|
this.props.formName,
|
|
this.props.configProperty,
|
|
[],
|
|
);
|
|
}
|
|
}
|
|
|
|
// Clear entity type if the command changed
|
|
if (this.props.configProperty === FormDataPaths.ENTITY_TYPE) {
|
|
const prevCommandValue = get(prevProps.formValues, FormDataPaths.COMMAND);
|
|
const currCommandValue = get(
|
|
this.props.formValues,
|
|
FormDataPaths.COMMAND,
|
|
);
|
|
|
|
if (prevCommandValue !== currCommandValue) {
|
|
this.props.updateConfigPropertyValue(
|
|
this.props.formName,
|
|
this.props.configProperty,
|
|
"",
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
getControlType(): ControlType {
|
|
return "DROP_DOWN";
|
|
}
|
|
|
|
render() {
|
|
const styles = {
|
|
...("customStyles" in this.props &&
|
|
typeof this.props.customStyles === "object"
|
|
? this.props.customStyles
|
|
: {}),
|
|
};
|
|
|
|
return (
|
|
<div
|
|
className={`t--${this.props.configProperty} uqi-dropdown-select`}
|
|
data-testid={this.props.configProperty}
|
|
style={styles}
|
|
>
|
|
<Field
|
|
component={renderDropdown}
|
|
name={this.props.configProperty}
|
|
props={{ ...this.props, width: styles.width }}
|
|
type={this.props.isMultiSelect ? "select-multiple" : undefined}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Renders a dropdown component with support for single and multi-select.
|
|
* Handles initialization of selected values, including:
|
|
* - Using initialValue prop if no value is selected
|
|
* - Converting string values to arrays for multi-select
|
|
* - Setting first option as default if configured
|
|
* - Deduplicating selected values in multi-select mode
|
|
* Supports pagination through onPopupScroll handler when nextPageNeeded
|
|
* and paginationPayload props are provided
|
|
* @param {Object} props - Component props
|
|
* @returns {JSX.Element} Rendered dropdown component
|
|
*/
|
|
function renderDropdown(
|
|
props: {
|
|
input?: {
|
|
value?: string | string[];
|
|
onChange: (val: string | string[]) => void;
|
|
} & WrappedFieldInputProps;
|
|
meta?: Partial<WrappedFieldMetaProps>;
|
|
width: string;
|
|
} & DropDownControlProps &
|
|
ReduxDispatchProps,
|
|
): JSX.Element {
|
|
const {
|
|
appendGroupIdentifierToValue,
|
|
input,
|
|
isAllowClear,
|
|
isMultiSelect,
|
|
optionGroupConfig,
|
|
options = [],
|
|
setFirstOptionAsDefault,
|
|
} = props;
|
|
// Safeguard the selectedValue (since it might be empty, null, or a string/string[])
|
|
let selectedValue = input?.value;
|
|
|
|
// If no selectedValue, use `initialValue` or set to empty array/string
|
|
if (isEmpty(selectedValue)) {
|
|
if (isMultiSelect) {
|
|
selectedValue = props.initialValue ? (props.initialValue as string) : [];
|
|
} else {
|
|
selectedValue = props.initialValue
|
|
? (props.initialValue as string[])
|
|
: "";
|
|
|
|
// If user wants the first option as default
|
|
if (setFirstOptionAsDefault && options.length > 0) {
|
|
selectedValue = options[0].value as string;
|
|
input?.onChange(selectedValue);
|
|
}
|
|
}
|
|
}
|
|
|
|
// If multi-select but we have a string, convert it to an array
|
|
if (isMultiSelect && !Array.isArray(selectedValue)) {
|
|
selectedValue = [selectedValue];
|
|
}
|
|
|
|
// Deduplicate if multi-select
|
|
if (isMultiSelect && Array.isArray(selectedValue)) {
|
|
// If your items have stable 'value' keys, use `uniqBy(...)`.
|
|
// For pure strings you can do `uniq([...selectedValue])`.
|
|
selectedValue = uniqBy(selectedValue, (v) => v);
|
|
}
|
|
|
|
// Use memoized grouping
|
|
const groupedOptions = memoizedBuildGroupedOptions(
|
|
options,
|
|
optionGroupConfig,
|
|
);
|
|
|
|
// Find the selected options based on the selectedValue
|
|
// If appendGroupIdentifierToValue is true, we need to check if the selected value includes the group identifier
|
|
// Eg: if the selected value is "group1:1", we need to find the option with value "1"
|
|
// If appendGroupIdentifierToValue is false, we just need to find the option with value "1"
|
|
const selectedOptions = options.filter((opt) => {
|
|
const checkGroupIdentifier =
|
|
appendGroupIdentifierToValue && optionGroupConfig;
|
|
const valueToCompare = checkGroupIdentifier
|
|
? opt.optionGroupType + ":" + opt.value
|
|
: opt.value;
|
|
|
|
return isMultiSelect
|
|
? (selectedValue as string[]).includes(valueToCompare)
|
|
: selectedValue === valueToCompare;
|
|
});
|
|
|
|
// Re-sync multi-select if stale
|
|
if (isMultiSelect && Array.isArray(selectedValue)) {
|
|
const validValues = selectedOptions.map((so) => so.value);
|
|
|
|
if (
|
|
!appendGroupIdentifierToValue &&
|
|
validValues.length !== selectedValue.length
|
|
) {
|
|
input?.onChange(validValues);
|
|
}
|
|
}
|
|
|
|
// Re-sync single-select if stale
|
|
if (!isMultiSelect && selectedOptions.length) {
|
|
const singleVal = selectedOptions[0].value;
|
|
|
|
if (!appendGroupIdentifierToValue && singleVal !== selectedValue) {
|
|
input?.onChange(singleVal);
|
|
}
|
|
}
|
|
|
|
// If required but the chosen single value is disabled, pick first enabled
|
|
if (
|
|
!isMultiSelect &&
|
|
props.isRequired &&
|
|
options.some((opt) => "disabled" in opt)
|
|
) {
|
|
const isCurrentOptionDisabled = options.some(
|
|
(opt) => opt.value === selectedValue && opt.disabled,
|
|
);
|
|
|
|
if (isCurrentOptionDisabled) {
|
|
const firstEnabled = options.find((opt) => !opt.disabled);
|
|
|
|
if (firstEnabled) {
|
|
input?.onChange(firstEnabled.value);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handles the selection of options
|
|
* If multi select is enabled, we need to add the value to the current array
|
|
* If multi select is not enabled, we just set the value
|
|
* If appendGroupIdentifierToValue is true, we need to add the group identifier to the value
|
|
* Eg: if the selected value is "1" of "group1", we need to add "group1:1" to the current array
|
|
* @param {string | undefined} optionValueToSelect - The selected value
|
|
*/
|
|
function onSelectOptions(optionValueToSelect: string | undefined) {
|
|
if (isNil(optionValueToSelect)) return;
|
|
|
|
// If appendGroupIdentifierToValue is true and we have grouped options, add the group identifier
|
|
const shouldAppendGroup = appendGroupIdentifierToValue && optionGroupConfig;
|
|
let valueToStore = optionValueToSelect;
|
|
|
|
if (shouldAppendGroup) {
|
|
const selectedOption = options.find(
|
|
(opt) => opt.value === optionValueToSelect,
|
|
);
|
|
|
|
if (selectedOption) {
|
|
valueToStore = `${selectedOption.optionGroupType || "others"}:${optionValueToSelect}`;
|
|
}
|
|
}
|
|
|
|
if (!isMultiSelect) {
|
|
input?.onChange(valueToStore);
|
|
|
|
return;
|
|
}
|
|
|
|
// In case the component config is changed to multi-select, we need to convert the selectedValue to an array
|
|
const currentArray = Array.isArray(selectedValue) ? [...selectedValue] : [];
|
|
|
|
if (!currentArray.includes(valueToStore)) {
|
|
currentArray.push(valueToStore);
|
|
}
|
|
|
|
input?.onChange(currentArray);
|
|
}
|
|
|
|
/**
|
|
* Handles the removal of options
|
|
* If multi select is enabled, we need to remove the value from the current array
|
|
* If multi select is not enabled, we just set the value to an empty string
|
|
* If appendGroupIdentifierToValue is true, we need to check the value with the group identifier
|
|
* Eg: the function will be called with "1" and the current array is ["group1:1", "others:2"]
|
|
* We need to check if "1" is present in the array after removing the group identifier
|
|
* The function will return ["others:2"]
|
|
* @param {string | undefined} optionValueToRemove - The value to remove
|
|
*/
|
|
function onRemoveOptions(optionValueToRemove: string | undefined) {
|
|
if (isNil(optionValueToRemove)) return;
|
|
|
|
if (!isMultiSelect) {
|
|
input?.onChange("");
|
|
|
|
return;
|
|
}
|
|
|
|
const currentArray = Array.isArray(selectedValue) ? [...selectedValue] : [];
|
|
|
|
const filtered = currentArray.filter((v) => {
|
|
let selectedValueToCheck = v;
|
|
|
|
if (appendGroupIdentifierToValue && optionGroupConfig) {
|
|
// For grouped values, we need to compare just the value part after the group identifier
|
|
selectedValueToCheck = v.split(":")[1];
|
|
}
|
|
|
|
return selectedValueToCheck !== optionValueToRemove;
|
|
});
|
|
|
|
input?.onChange(filtered);
|
|
}
|
|
|
|
/**
|
|
* Clears all options
|
|
* If multi select is enabled, we need to set the value to an empty array
|
|
* If multi select is not enabled, we just set the value to an empty string
|
|
*/
|
|
function clearAllOptions() {
|
|
if (isNil(selectedValue)) return;
|
|
|
|
if (isMultiSelect) {
|
|
input?.onChange([]);
|
|
} else {
|
|
input?.onChange("");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Subscribes to the scroll event of the popup and notifies when end of scroll is reached
|
|
* If pagination is needed and there is a payload, we need to fetch the next page on end of scroll
|
|
* @param {React.UIEvent<HTMLDivElement>} e - The event object
|
|
*/
|
|
function handlePopupScroll(e: React.UIEvent<HTMLDivElement>) {
|
|
if (!props.nextPageNeeded || !props.paginationPayload) return;
|
|
|
|
const target = e.currentTarget;
|
|
|
|
if (target.scrollHeight - target.scrollTop === target.clientHeight) {
|
|
props.fetchFormTriggerNextPage(props.paginationPayload);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<Select
|
|
allowClear={(isMultiSelect || isAllowClear) && !isEmpty(selectedValue)}
|
|
data-testid={`t--dropdown-${props.configProperty}`}
|
|
defaultValue={props.initialValue}
|
|
isDisabled={props.disabled}
|
|
isLoading={props.isLoading}
|
|
isMultiSelect={isMultiSelect}
|
|
maxTagCount={props.maxTagCount}
|
|
onClear={clearAllOptions}
|
|
onDeselect={onRemoveOptions}
|
|
onPopupScroll={handlePopupScroll}
|
|
onSelect={onSelectOptions}
|
|
// Default value of optionFilterProp prop is `value` which searches the dropdown based on value and not label,
|
|
// hence explicitly setting this to label to search based on label.
|
|
// For eg. If value is `Create_ticket` and label is `Create ticket`, we should be able to search using `Create ticket`.
|
|
optionFilterProp="label"
|
|
placeholder={props.placeholderText}
|
|
showSearch={props.isSearchable}
|
|
value={isMultiSelect ? selectedOptions : selectedOptions[0]}
|
|
>
|
|
{groupedOptions
|
|
? groupedOptions.map(({ children, label }) => (
|
|
<OptGroup aria-label={label} key={label}>
|
|
{children.map(renderOptionWithIcon)}
|
|
</OptGroup>
|
|
))
|
|
: options.map(renderOptionWithIcon)}
|
|
</Select>
|
|
);
|
|
}
|
|
|
|
function renderOptionWithIcon(option: SelectOptionProps) {
|
|
return (
|
|
<Option
|
|
aria-label={option.label}
|
|
disabled={option.disabled}
|
|
isDisabled={option.isDisabled}
|
|
key={option.value}
|
|
label={option.label}
|
|
value={option.value}
|
|
>
|
|
{option.icon && <Icon color={option.color} name={option.icon} />}
|
|
{option.label}
|
|
</Option>
|
|
);
|
|
}
|
|
|
|
const mapStateToProps = (
|
|
state: AppState,
|
|
ownProps: DropDownControlProps,
|
|
): {
|
|
isLoading: boolean;
|
|
options: SelectOptionProps[];
|
|
formValues: Partial<Action>;
|
|
nextPageNeeded: boolean;
|
|
paginationPayload?: {
|
|
value: ConditionalOutput;
|
|
dynamicFetchedValues: DynamicValues;
|
|
actionId: string;
|
|
datasourceId: string;
|
|
pluginId: string;
|
|
identifier: string;
|
|
};
|
|
} => {
|
|
let isLoading = false;
|
|
// Start with the user-provided options if not fetching conditionally
|
|
let options = ownProps.fetchOptionsConditionally ? [] : ownProps.options;
|
|
const formValues: Partial<Action> = getFormValues(ownProps.formName)(state);
|
|
|
|
let nextPageNeeded = false;
|
|
let paginationPayload;
|
|
|
|
try {
|
|
if (ownProps.fetchOptionsConditionally) {
|
|
const conditionalOutput = getFormConfigConditionalOutput(state, ownProps);
|
|
const dynamicFetchedValues =
|
|
conditionalOutput.fetchDynamicValues || ({} as DynamicValues);
|
|
|
|
const { data } = dynamicFetchedValues;
|
|
|
|
if (data && data.content && data.startIndex != null) {
|
|
const { content, count, startIndex, total } = data;
|
|
|
|
options = content;
|
|
|
|
if (startIndex + count < total) {
|
|
nextPageNeeded = true;
|
|
|
|
// Prepare the next page request
|
|
const modifiedParams = {
|
|
...dynamicFetchedValues.evaluatedConfig.params,
|
|
parameters: {
|
|
...dynamicFetchedValues.evaluatedConfig.params.parameters,
|
|
startIndex: startIndex + count,
|
|
},
|
|
};
|
|
|
|
const modifiedDFV: DynamicValues = {
|
|
...dynamicFetchedValues,
|
|
evaluatedConfig: {
|
|
...dynamicFetchedValues.evaluatedConfig,
|
|
params: modifiedParams,
|
|
},
|
|
};
|
|
|
|
paginationPayload = {
|
|
value: { ...conditionalOutput, fetchDynamicValues: modifiedDFV },
|
|
dynamicFetchedValues: modifiedDFV,
|
|
actionId: formValues.id || "",
|
|
datasourceId: formValues.datasource?.id || "",
|
|
pluginId: formValues.pluginId || "",
|
|
identifier:
|
|
ownProps.propertyName ||
|
|
ownProps.configProperty ||
|
|
ownProps.identifier ||
|
|
"",
|
|
};
|
|
}
|
|
} else {
|
|
// No pagination, so just use the fetched data
|
|
options = dynamicFetchedValues.data || [];
|
|
}
|
|
|
|
isLoading = dynamicFetchedValues.isLoading;
|
|
}
|
|
} catch (err) {
|
|
// eslint-disable-next-line no-console
|
|
console.error(err);
|
|
}
|
|
|
|
return {
|
|
isLoading,
|
|
options,
|
|
formValues,
|
|
nextPageNeeded,
|
|
paginationPayload,
|
|
};
|
|
};
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const mapDispatchToProps = (dispatch: any): ReduxDispatchProps => ({
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
updateConfigPropertyValue: (formName: string, field: string, value: any) => {
|
|
dispatch(change(formName, field, value));
|
|
},
|
|
fetchFormTriggerNextPage: (paginationPayload?: {
|
|
value: ConditionalOutput;
|
|
dynamicFetchedValues: DynamicValues;
|
|
actionId: string;
|
|
datasourceId: string;
|
|
pluginId: string;
|
|
identifier: string;
|
|
}) => {
|
|
dispatch(fetchFormDynamicValNextPage(paginationPayload));
|
|
},
|
|
});
|
|
|
|
export default connect(mapStateToProps, mapDispatchToProps)(DropDownControl);
|