Feat/server side filtering multiselect (#5737)
The multi-select and select now has the capability to handle server side filtering, i.e. an Appsmith dev can now choose to call an api on search or options using the onFilterUpdate action
This commit is contained in:
parent
e20d616c33
commit
f4e0e8adb0
|
|
@ -11,6 +11,8 @@ import {
|
|||
CANVAS_CLASSNAME,
|
||||
MODAL_PORTAL_CLASSNAME,
|
||||
} from "constants/WidgetConstants";
|
||||
import debounce from "lodash/debounce";
|
||||
import { Classes } from "@blueprintjs/core";
|
||||
|
||||
const menuItemSelectedIcon = (props: { isSelected: boolean }) => {
|
||||
return <StyledCheckbox checked={props.isSelected} />;
|
||||
|
|
@ -26,15 +28,21 @@ export interface MultiSelectProps
|
|||
mode?: "multiple" | "tags";
|
||||
value: string[];
|
||||
onChange: (value: DefaultValueType) => void;
|
||||
serverSideFiltering: boolean;
|
||||
onFilterChange: (text: string) => void;
|
||||
}
|
||||
|
||||
const DEBOUNCE_TIMEOUT = 800;
|
||||
|
||||
function MultiSelectComponent({
|
||||
disabled,
|
||||
dropdownStyle,
|
||||
loading,
|
||||
onChange,
|
||||
onFilterChange,
|
||||
options,
|
||||
placeholder,
|
||||
serverSideFiltering,
|
||||
value,
|
||||
}: MultiSelectProps): JSX.Element {
|
||||
const [isSelectAll, setIsSelectAll] = useState(false);
|
||||
|
|
@ -76,7 +84,7 @@ function MultiSelectComponent({
|
|||
(
|
||||
menu: React.ReactElement<any, string | React.JSXElementConstructor<any>>,
|
||||
) => (
|
||||
<>
|
||||
<div className={loading ? Classes.SKELETON : ""}>
|
||||
{options.length ? (
|
||||
<StyledCheckbox
|
||||
alignIndicator="left"
|
||||
|
|
@ -86,9 +94,9 @@ function MultiSelectComponent({
|
|||
/>
|
||||
) : null}
|
||||
{menu}
|
||||
</>
|
||||
</div>
|
||||
),
|
||||
[isSelectAll, options],
|
||||
[isSelectAll, options, loading],
|
||||
);
|
||||
|
||||
const filterOption = useCallback(
|
||||
|
|
@ -97,6 +105,16 @@ function MultiSelectComponent({
|
|||
option?.props.value.toLowerCase().indexOf(input.toLowerCase()) >= 0,
|
||||
[],
|
||||
);
|
||||
|
||||
const onClose = useCallback((open) => !open && onFilterChange(""), []);
|
||||
|
||||
const serverSideSearch = React.useMemo(() => {
|
||||
const updateFilter = (filterValue: string) => {
|
||||
onFilterChange(filterValue);
|
||||
};
|
||||
return debounce(updateFilter, DEBOUNCE_TIMEOUT);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<MultiSelectContainer ref={_menu as React.RefObject<HTMLDivElement>}>
|
||||
<DropdownStyles />
|
||||
|
|
@ -110,7 +128,7 @@ function MultiSelectComponent({
|
|||
dropdownClassName="multi-select-dropdown"
|
||||
dropdownRender={dropdownRender}
|
||||
dropdownStyle={dropdownStyle}
|
||||
filterOption={filterOption}
|
||||
filterOption={serverSideFiltering ? false : filterOption}
|
||||
getPopupContainer={() => getDropdownPosition(_menu.current)}
|
||||
inputIcon={inputIcon}
|
||||
loading={loading}
|
||||
|
|
@ -120,6 +138,8 @@ function MultiSelectComponent({
|
|||
mode="multiple"
|
||||
notFoundContent="No item Found"
|
||||
onChange={onChange}
|
||||
onDropdownVisibleChange={onClose}
|
||||
onSearch={serverSideSearch}
|
||||
options={options}
|
||||
placeholder={placeholder || "select option(s)"}
|
||||
showArrow
|
||||
|
|
|
|||
|
|
@ -145,6 +145,7 @@ const DropdownStyles = createGlobalStyle`
|
|||
const DropdownContainer = styled.div`
|
||||
${BlueprintCSSTransform}
|
||||
`;
|
||||
const DEBOUNCE_TIMEOUT = 800;
|
||||
|
||||
class DropDownComponent extends React.Component<DropDownComponentProps> {
|
||||
render() {
|
||||
|
|
@ -170,10 +171,17 @@ class DropDownComponent extends React.Component<DropDownComponentProps> {
|
|||
className={this.props.isLoading ? Classes.SKELETON : ""}
|
||||
disabled={this.props.disabled}
|
||||
filterable={this.props.isFilterable}
|
||||
itemListPredicate={this.itemListPredicate}
|
||||
itemListPredicate={
|
||||
!this.props.serverSideFiltering
|
||||
? this.itemListPredicate
|
||||
: undefined
|
||||
}
|
||||
itemRenderer={this.renderSingleSelectItem}
|
||||
items={this.props.options}
|
||||
onItemSelect={this.onItemSelect}
|
||||
onQueryChange={
|
||||
this.props.serverSideFiltering ? this.serverSideSearch : undefined
|
||||
}
|
||||
popoverProps={{
|
||||
boundary: "window",
|
||||
minimal: true,
|
||||
|
|
@ -218,6 +226,9 @@ class DropDownComponent extends React.Component<DropDownComponentProps> {
|
|||
});
|
||||
return optionIndex === this.props.selectedIndex;
|
||||
};
|
||||
serverSideSearch = _.debounce((filterValue: string) => {
|
||||
this.props.onFilterChange(filterValue);
|
||||
}, DEBOUNCE_TIMEOUT);
|
||||
|
||||
renderSingleSelectItem = (
|
||||
option: DropdownOption,
|
||||
|
|
@ -250,6 +261,8 @@ export interface DropDownComponentProps extends ComponentProps {
|
|||
isFilterable: boolean;
|
||||
width: number;
|
||||
height: number;
|
||||
serverSideFiltering: boolean;
|
||||
onFilterChange: (text: string) => void;
|
||||
}
|
||||
|
||||
export default DropDownComponent;
|
||||
|
|
|
|||
|
|
@ -65,6 +65,8 @@ export enum EventType {
|
|||
ON_DATE_SELECTED = "ON_DATE_SELECTED",
|
||||
ON_DATE_RANGE_SELECTED = "ON_DATE_RANGE_SELECTED",
|
||||
ON_OPTION_CHANGE = "ON_OPTION_CHANGE",
|
||||
ON_FILTER_CHANGE = "ON_FILTER_CHANGE",
|
||||
ON_FILTER_UPDATE = "ON_FILTER_UPDATE",
|
||||
ON_MARKER_CLICK = "ON_MARKER_CLICK",
|
||||
ON_CREATE_MARKER = "ON_CREATE_MARKER",
|
||||
ON_TAB_CHANGE = "ON_TAB_CHANGE",
|
||||
|
|
|
|||
|
|
@ -328,6 +328,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = {
|
|||
{ label: "Green", value: "GREEN" },
|
||||
{ label: "Red", value: "RED" },
|
||||
],
|
||||
serverSideFiltering: false,
|
||||
widgetName: "Select",
|
||||
defaultOptionValue: "GREEN",
|
||||
version: 1,
|
||||
|
|
@ -349,6 +350,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = {
|
|||
{ label: "Naruto Uzumaki", value: "Seventh" },
|
||||
],
|
||||
widgetName: "MultiSelect",
|
||||
serverSideFiltering: false,
|
||||
defaultOptionValue: ["First", "Seventh"],
|
||||
version: 1,
|
||||
isRequired: false,
|
||||
|
|
|
|||
|
|
@ -83,6 +83,10 @@ export const entityDefinitions = {
|
|||
"Select is used to capture user input/s from a specified list of permitted inputs. A Select can capture a single choice as well as multiple choices",
|
||||
"!url": "https://docs.appsmith.com/widget-reference/dropdown",
|
||||
isVisible: isVisible,
|
||||
filterText: {
|
||||
"!type": "[string]",
|
||||
"!doc": "The filter text for Server side filtering",
|
||||
},
|
||||
selectedOptionValue: {
|
||||
"!type": "string",
|
||||
"!doc": "The value selected in a single select dropdown",
|
||||
|
|
@ -111,6 +115,10 @@ export const entityDefinitions = {
|
|||
"MultiSelect is used to capture user input/s from a specified list of permitted inputs. A MultiSelect captures multiple choices from a list of options",
|
||||
"!url": "https://docs.appsmith.com/widget-reference/dropdown",
|
||||
isVisible: isVisible,
|
||||
filterText: {
|
||||
"!type": "[string]",
|
||||
"!doc": "The filter text for Server side filtering",
|
||||
},
|
||||
selectedOptionValues: {
|
||||
"!type": "[string]",
|
||||
"!doc": "The array of values selected in a multi select dropdown",
|
||||
|
|
|
|||
|
|
@ -134,6 +134,16 @@ class DropdownWidget extends BaseWidget<DropdownWidgetProps, WidgetState> {
|
|||
isTriggerProperty: false,
|
||||
validation: { type: ValidationTypes.BOOLEAN },
|
||||
},
|
||||
{
|
||||
helpText: "Enables server side filtering of the data",
|
||||
propertyName: "serverSideFiltering",
|
||||
label: "Server Side Filtering",
|
||||
controlType: "SWITCH",
|
||||
isJSConvertible: true,
|
||||
isBindProperty: true,
|
||||
isTriggerProperty: false,
|
||||
validation: { type: ValidationTypes.BOOLEAN },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
@ -148,6 +158,17 @@ class DropdownWidget extends BaseWidget<DropdownWidgetProps, WidgetState> {
|
|||
isBindProperty: true,
|
||||
isTriggerProperty: true,
|
||||
},
|
||||
{
|
||||
helpText: "Trigger an action on change of filterText",
|
||||
hidden: (props: DropdownWidgetProps) => !props.serverSideFiltering,
|
||||
dependencies: ["serverSideFiltering"],
|
||||
propertyName: "onFilterUpdate",
|
||||
label: "onFilterUpdate",
|
||||
controlType: "ACTION_SELECTOR",
|
||||
isJSConvertible: true,
|
||||
isBindProperty: true,
|
||||
isTriggerProperty: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
|
@ -189,10 +210,12 @@ class DropdownWidget extends BaseWidget<DropdownWidgetProps, WidgetState> {
|
|||
isFilterable={this.props.isFilterable}
|
||||
isLoading={this.props.isLoading}
|
||||
label={`${this.props.label}`}
|
||||
onFilterChange={this.onFilterChange}
|
||||
onOptionSelected={this.onOptionSelected}
|
||||
options={options}
|
||||
placeholder={this.props.placeholderText}
|
||||
selectedIndex={selectedIndex > -1 ? selectedIndex : undefined}
|
||||
serverSideFiltering={this.props.serverSideFiltering}
|
||||
widgetId={this.props.widgetId}
|
||||
width={componentWidth}
|
||||
/>
|
||||
|
|
@ -223,6 +246,18 @@ class DropdownWidget extends BaseWidget<DropdownWidgetProps, WidgetState> {
|
|||
}
|
||||
};
|
||||
|
||||
onFilterChange = (value: string) => {
|
||||
this.props.updateWidgetMetaProperty("filterText", value);
|
||||
|
||||
super.executeAction({
|
||||
triggerPropertyName: "onFilterUpdate",
|
||||
dynamicString: this.props.onFilterUpdate,
|
||||
event: {
|
||||
type: EventType.ON_FILTER_UPDATE,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
getWidgetType(): WidgetType {
|
||||
return "DROP_DOWN_WIDGET";
|
||||
}
|
||||
|
|
@ -251,6 +286,8 @@ export interface DropdownWidgetProps extends WidgetProps, WithMeta {
|
|||
isFilterable: boolean;
|
||||
defaultValue: string;
|
||||
selectedOptionLabel: string;
|
||||
serverSideFiltering: boolean;
|
||||
onFilterUpdate: string;
|
||||
}
|
||||
|
||||
export default DropdownWidget;
|
||||
|
|
|
|||
|
|
@ -151,6 +151,16 @@ class MultiSelectWidget extends BaseWidget<
|
|||
isTriggerProperty: false,
|
||||
validation: { type: ValidationTypes.BOOLEAN },
|
||||
},
|
||||
{
|
||||
helpText: "Enables server side filtering of the data",
|
||||
propertyName: "serverSideFiltering",
|
||||
label: "Server Side Filtering",
|
||||
controlType: "SWITCH",
|
||||
isJSConvertible: true,
|
||||
isBindProperty: true,
|
||||
isTriggerProperty: false,
|
||||
validation: { type: ValidationTypes.BOOLEAN },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
@ -165,6 +175,18 @@ class MultiSelectWidget extends BaseWidget<
|
|||
isBindProperty: true,
|
||||
isTriggerProperty: true,
|
||||
},
|
||||
{
|
||||
helpText: "Trigger an action on change of filterText",
|
||||
hidden: (props: MultiSelectWidgetProps) =>
|
||||
!props.serverSideFiltering,
|
||||
dependencies: ["serverSideFiltering"],
|
||||
propertyName: "onFilterUpdate",
|
||||
label: "onFilterUpdate",
|
||||
controlType: "ACTION_SELECTOR",
|
||||
isJSConvertible: true,
|
||||
isBindProperty: true,
|
||||
isTriggerProperty: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
|
@ -182,12 +204,14 @@ class MultiSelectWidget extends BaseWidget<
|
|||
static getDefaultPropertiesMap(): Record<string, string> {
|
||||
return {
|
||||
selectedOptionValueArr: "defaultOptionValue",
|
||||
filterText: "",
|
||||
};
|
||||
}
|
||||
|
||||
static getMetaPropertiesMap(): Record<string, any> {
|
||||
return {
|
||||
selectedOptionValueArr: undefined,
|
||||
filterText: "",
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -196,7 +220,6 @@ class MultiSelectWidget extends BaseWidget<
|
|||
const values: string[] = isArray(this.props.selectedOptionValues)
|
||||
? this.props.selectedOptionValues
|
||||
: [];
|
||||
|
||||
return (
|
||||
<MultiSelectComponent
|
||||
disabled={this.props.isDisabled ?? false}
|
||||
|
|
@ -205,8 +228,10 @@ class MultiSelectWidget extends BaseWidget<
|
|||
}}
|
||||
loading={this.props.isLoading}
|
||||
onChange={this.onOptionChange}
|
||||
onFilterChange={this.onFilterChange}
|
||||
options={options}
|
||||
placeholder={this.props.placeholderText as string}
|
||||
serverSideFiltering={this.props.serverSideFiltering}
|
||||
value={values}
|
||||
/>
|
||||
);
|
||||
|
|
@ -220,6 +245,21 @@ class MultiSelectWidget extends BaseWidget<
|
|||
type: EventType.ON_OPTION_CHANGE,
|
||||
},
|
||||
});
|
||||
|
||||
// Empty filter after Selection
|
||||
this.onFilterChange("");
|
||||
};
|
||||
|
||||
onFilterChange = (value: string) => {
|
||||
this.props.updateWidgetMetaProperty("filterText", value);
|
||||
|
||||
super.executeAction({
|
||||
triggerPropertyName: "onFilterUpdate",
|
||||
dynamicString: this.props.onFilterUpdate,
|
||||
event: {
|
||||
type: EventType.ON_FILTER_UPDATE,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
getWidgetType(): WidgetType {
|
||||
|
|
@ -240,12 +280,16 @@ export interface MultiSelectWidgetProps extends WidgetProps, WithMeta {
|
|||
selectedOption: DropdownOption;
|
||||
options?: DropdownOption[];
|
||||
onOptionChange: string;
|
||||
onFilterChange: string;
|
||||
defaultOptionValue: string | string[];
|
||||
isRequired: boolean;
|
||||
isLoading: boolean;
|
||||
selectedOptionValueArr: string[];
|
||||
filterText: string;
|
||||
selectedOptionValues: string[];
|
||||
selectedOptionLabels: string[];
|
||||
serverSideFiltering: boolean;
|
||||
onFilterUpdate: string;
|
||||
}
|
||||
|
||||
export default MultiSelectWidget;
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user