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;