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:
Tolulope Adetula 2021-08-16 07:56:09 +01:00 committed by GitHub
parent e20d616c33
commit f4e0e8adb0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 132 additions and 6 deletions

View File

@ -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

View File

@ -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;

View File

@ -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",

View File

@ -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,

View File

@ -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",

View File

@ -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;

View File

@ -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;