From f4e0e8adb00d8755cacb2b47ae23a7af651950ef Mon Sep 17 00:00:00 2001 From: Tolulope Adetula <31691737+Tooluloope@users.noreply.github.com> Date: Mon, 16 Aug 2021 07:56:09 +0100 Subject: [PATCH] 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 --- .../appsmith/MultiSelectComponent/index.tsx | 28 +++++++++-- .../blueprint/DropdownComponent.tsx | 15 +++++- .../ActionConstants.tsx | 2 + .../mockResponses/WidgetConfigResponse.tsx | 2 + .../utils/autocomplete/EntityDefinitions.ts | 8 ++++ app/client/src/widgets/DropdownWidget.tsx | 37 +++++++++++++++ app/client/src/widgets/MultiSelectWidget.tsx | 46 ++++++++++++++++++- 7 files changed, 132 insertions(+), 6 deletions(-) diff --git a/app/client/src/components/designSystems/appsmith/MultiSelectComponent/index.tsx b/app/client/src/components/designSystems/appsmith/MultiSelectComponent/index.tsx index 4a13608e52..ed80edbd25 100644 --- a/app/client/src/components/designSystems/appsmith/MultiSelectComponent/index.tsx +++ b/app/client/src/components/designSystems/appsmith/MultiSelectComponent/index.tsx @@ -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 ; @@ -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>, ) => ( - <> +
{options.length ? ( ) : null} {menu} - +
), - [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 ( }> @@ -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 diff --git a/app/client/src/components/designSystems/blueprint/DropdownComponent.tsx b/app/client/src/components/designSystems/blueprint/DropdownComponent.tsx index 01cd8e0676..5b70858116 100644 --- a/app/client/src/components/designSystems/blueprint/DropdownComponent.tsx +++ b/app/client/src/components/designSystems/blueprint/DropdownComponent.tsx @@ -145,6 +145,7 @@ const DropdownStyles = createGlobalStyle` const DropdownContainer = styled.div` ${BlueprintCSSTransform} `; +const DEBOUNCE_TIMEOUT = 800; class DropDownComponent extends React.Component { render() { @@ -170,10 +171,17 @@ class DropDownComponent extends React.Component { 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 { }); 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; diff --git a/app/client/src/constants/AppsmithActionConstants/ActionConstants.tsx b/app/client/src/constants/AppsmithActionConstants/ActionConstants.tsx index 7f8448f4a2..8525e304f1 100644 --- a/app/client/src/constants/AppsmithActionConstants/ActionConstants.tsx +++ b/app/client/src/constants/AppsmithActionConstants/ActionConstants.tsx @@ -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", diff --git a/app/client/src/mockResponses/WidgetConfigResponse.tsx b/app/client/src/mockResponses/WidgetConfigResponse.tsx index caa3cb6018..aa0e2477ba 100644 --- a/app/client/src/mockResponses/WidgetConfigResponse.tsx +++ b/app/client/src/mockResponses/WidgetConfigResponse.tsx @@ -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, diff --git a/app/client/src/utils/autocomplete/EntityDefinitions.ts b/app/client/src/utils/autocomplete/EntityDefinitions.ts index c61457f913..1d8fc0dca1 100644 --- a/app/client/src/utils/autocomplete/EntityDefinitions.ts +++ b/app/client/src/utils/autocomplete/EntityDefinitions.ts @@ -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", diff --git a/app/client/src/widgets/DropdownWidget.tsx b/app/client/src/widgets/DropdownWidget.tsx index 42ceb98ba7..b83b2b24d3 100644 --- a/app/client/src/widgets/DropdownWidget.tsx +++ b/app/client/src/widgets/DropdownWidget.tsx @@ -134,6 +134,16 @@ class DropdownWidget 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 }, + }, ], }, { @@ -148,6 +158,17 @@ class DropdownWidget extends BaseWidget { 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 { 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 { } }; + 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; diff --git a/app/client/src/widgets/MultiSelectWidget.tsx b/app/client/src/widgets/MultiSelectWidget.tsx index b8775c0496..787364b245 100644 --- a/app/client/src/widgets/MultiSelectWidget.tsx +++ b/app/client/src/widgets/MultiSelectWidget.tsx @@ -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 { return { selectedOptionValueArr: "defaultOptionValue", + filterText: "", }; } static getMetaPropertiesMap(): Record { return { selectedOptionValueArr: undefined, + filterText: "", }; } @@ -196,7 +220,6 @@ class MultiSelectWidget extends BaseWidget< const values: string[] = isArray(this.props.selectedOptionValues) ? this.props.selectedOptionValues : []; - return ( ); @@ -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;