diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/dropdownV2.js b/frontend/src/AppBuilder/WidgetManager/widgets/dropdownV2.js index cb90554e6b..d7534b25a8 100644 --- a/frontend/src/AppBuilder/WidgetManager/widgets/dropdownV2.js +++ b/frontend/src/AppBuilder/WidgetManager/widgets/dropdownV2.js @@ -75,6 +75,18 @@ export const dropdownV2Config = { accordian: 'Options', isFxNotRequired: true, }, + showClearBtn: { + type: 'toggle', + displayName: 'Show clear selection button', + validation: { schema: { type: 'boolean' }, defaultValue: true }, + section: 'additionalActions', + }, + showSearchInput: { + type: 'toggle', + displayName: 'Show search in options', + validation: { schema: { type: 'boolean' }, defaultValue: true }, + section: 'additionalActions', + }, loadingState: { type: 'toggle', displayName: 'Loading state', @@ -314,6 +326,8 @@ export const dropdownV2Config = { optionsLoadingState: { value: '{{false}}' }, sort: { value: 'asc' }, placeholder: { value: 'Select an option' }, + showClearBtn: { value: '{{true}}' }, + showSearchInput: { value: '{{true}}' }, visibility: { value: '{{true}}' }, disabledState: { value: '{{false}}' }, loadingState: { value: '{{false}}' }, diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/multiselectV2.js b/frontend/src/AppBuilder/WidgetManager/widgets/multiselectV2.js index 4ab4af57ce..294423abac 100644 --- a/frontend/src/AppBuilder/WidgetManager/widgets/multiselectV2.js +++ b/frontend/src/AppBuilder/WidgetManager/widgets/multiselectV2.js @@ -142,6 +142,18 @@ export const multiselectV2Config = { accordian: 'Options', isFxNotRequired: true, }, + showClearBtn: { + type: 'toggle', + displayName: 'Show clear selection button', + validation: { schema: { type: 'boolean' }, defaultValue: true }, + section: 'additionalActions', + }, + showSearchInput: { + type: 'toggle', + displayName: 'Show search in options', + validation: { schema: { type: 'boolean' }, defaultValue: true }, + section: 'additionalActions', + }, loadingState: { type: 'toggle', displayName: 'Loading state', @@ -327,6 +339,8 @@ export const multiselectV2Config = { optionsLoadingState: { value: '{{false}}' }, sort: { value: 'asc' }, placeholder: { value: 'Select the options' }, + showClearBtn: { value: '{{true}}' }, + showSearchInput: { value: '{{true}}' }, visibility: { value: '{{true}}' }, disabledState: { value: '{{false}}' }, loadingState: { value: '{{false}}' }, diff --git a/frontend/src/Editor/Components/DropdownV2/CustomMenuList.jsx b/frontend/src/Editor/Components/DropdownV2/CustomMenuList.jsx index 848de076ae..6be9d65bc7 100644 --- a/frontend/src/Editor/Components/DropdownV2/CustomMenuList.jsx +++ b/frontend/src/Editor/Components/DropdownV2/CustomMenuList.jsx @@ -1,87 +1,118 @@ -import React from 'react'; +import React, { useEffect, useRef } from 'react'; import { components } from 'react-select'; import SolidIcon from '@/_ui/Icon/SolidIcons'; import Loader from '@/ToolJetUI/Loader/Loader'; import './dropdownV2.scss'; -import { FormCheck } from 'react-bootstrap'; +// eslint-disable-next-line import/no-unresolved +import { useVirtualizer } from '@tanstack/react-virtual'; import cx from 'classnames'; const { MenuList } = components; // This Menulist also used in MultiselectV2 const CustomMenuList = ({ selectProps, ...props }) => { - const { - onInputChange, - onMenuInputFocus, - showAllOption, - isSelectAllSelected, - optionsLoadingState, - darkMode, - setSelected, - setIsSelectAllSelected, - fireEvent, - inputValue, - menuId, - } = selectProps; + const { onInputChange, onMenuInputFocus, optionsLoadingState, darkMode, inputValue, menuId, showSearchInput } = + selectProps; - const handleSelectAll = (e) => { - e.target.checked && fireEvent(); - if (e.target.checked) { - setSelected(props.options); - } else { - setSelected([]); + const parentRef = useRef(null); + const virtualizer = useVirtualizer({ + count: props?.children?.length || 0, + getScrollElement: () => parentRef.current, + estimateSize: () => 40, + overscan: 15, + }); + + useEffect(() => { + const searchInput = document.querySelector('.dropdown-multiselect-widget-search-box'); + if (searchInput) { + searchInput.focus(); } - setIsSelectAllSelected(e.target.checked); - }; + }, []); + return (
e.stopPropagation()} + onTouchEnd={(e) => e.stopPropagation()} > -
- - - - - onInputChange(e.currentTarget.value, { - action: 'input-change', - }) - } - onMouseDown={(e) => { - e.stopPropagation(); - e.target.focus(); - }} - onTouchEnd={(e) => { - e.stopPropagation(); - e.target.focus(); - }} - onFocus={onMenuInputFocus} - placeholder="Search" - className="dropdown-multiselect-widget-search-box" - /> -
- {showAllOption && !optionsLoadingState && ( - + {showSearchInput && ( +
+ + + + + onInputChange(e.currentTarget.value, { + action: 'input-change', + }) + } + onMouseDown={(e) => { + e.stopPropagation(); + e.target.focus(); + }} + onTouchEnd={(e) => { + e.stopPropagation(); + e.target.focus(); + }} + onFocus={onMenuInputFocus} + placeholder="Search" + className="dropdown-multiselect-widget-search-box" + /> +
)} - - {optionsLoadingState ? ( -
- + {!optionsLoadingState && ( +
+
+ {!virtualizer.getTotalSize() && props.children} + {virtualizer.getVirtualItems().map((virtualItem) => { + const option = props.options[virtualItem.index]; + const child = props.children[virtualItem.index]; + const isSelectAll = option?.value === 'multiselect-custom-menulist-select-all'; + return ( +
+ +
{child}
+
+
+ ); + })}
- ) : ( - props.children - )} - +
+ )} + {optionsLoadingState && ( +
+ +
+ )}
); }; diff --git a/frontend/src/Editor/Components/DropdownV2/CustomOption.jsx b/frontend/src/Editor/Components/DropdownV2/CustomOption.jsx index 73986ba274..d3e85030d6 100644 --- a/frontend/src/Editor/Components/DropdownV2/CustomOption.jsx +++ b/frontend/src/Editor/Components/DropdownV2/CustomOption.jsx @@ -6,7 +6,17 @@ import { highlightText } from './utils'; const CustomOption = (props) => { return ( - + { + e.preventDefault(); + e.stopPropagation(); + props.selectOption(props.data); + }, + }} + >
{props.isSelected && ( diff --git a/frontend/src/Editor/Components/DropdownV2/DropdownV2.jsx b/frontend/src/Editor/Components/DropdownV2/DropdownV2.jsx index 382ba010ed..4e766f89bd 100644 --- a/frontend/src/Editor/Components/DropdownV2/DropdownV2.jsx +++ b/frontend/src/Editor/Components/DropdownV2/DropdownV2.jsx @@ -15,7 +15,6 @@ import Label from '@/_ui/Label'; import cx from 'classnames'; import { getInputBackgroundColor, getInputBorderColor, getInputFocusedColor, sortArray } from './utils'; import { isMobileDevice } from '@/_helpers/appUtils'; -import useStore from '@/AppBuilder/_stores/store'; const { DropdownIndicator, ClearIndicator } = components; const INDICATOR_CONTAINER_WIDTH = 60; @@ -69,6 +68,8 @@ export const DropdownV2 = ({ disabledState, optionsLoadingState, sort, + showClearBtn, + showSearchInput, } = properties; const { selectedTextColor, @@ -104,8 +105,6 @@ export const DropdownV2 = ({ const [isDropdownDisabled, setIsDropdownDisabled] = useState(disabledState); const [searchInputValue, setSearchInputValue] = useState(''); const [userInteracted, setUserInteracted] = useState(false); - const currentMode = useStore((state) => state.currentMode); - const isEditor = currentMode === 'edit'; const _height = padding === 'default' ? `${height}px` : `${height + 4}px`; const labelRef = useRef(); @@ -173,12 +172,43 @@ export const DropdownV2 = ({ setExposedVariable('isValid', validationStatus?.isValid); }; - const handleClickInEditor = (e) => { - if (e.target.className.includes('clear-indicator') || isMenuOpen) return; - e.stopPropagation(); - selectRef.current?.onControlMouseDown(e); + const handleClickInsideSelect = () => { + if (isDropdownDisabled || isDropdownLoading) return; + if (isMenuOpen) { + setIsMenuOpen(false); + fireEvent('onBlur'); + setSearchInputValue(''); + } else { + setIsMenuOpen(true); + fireEvent('onFocus'); + if (!showSearchInput) { + selectRef.current.focus(); + } + } }; + const handleClickOutsideSelect = (event) => { + const menu = document.querySelector(`._tooljet-${componentName}`); + if ( + isMenuOpen && + menu && + dropdownRef.current && + !dropdownRef.current.contains(event.target) && + !menu.contains(event.target) + ) { + setIsMenuOpen(false); + fireEvent('onBlur'); + setSearchInputValue(''); + } + }; + + useEffect(() => { + document.addEventListener('mousedown', handleClickOutsideSelect); + return () => { + document.removeEventListener('mousedown', handleClickOutsideSelect); + }; + }, [isMenuOpen, componentName]); + useEffect(() => { setInputValue(findDefaultItem(advanced ? schema : options)); // eslint-disable-next-line react-hooks/exhaustive-deps @@ -386,7 +416,7 @@ export const DropdownV2 = ({ }), menuList: (provided) => ({ ...provided, - padding: '8px', + padding: '0 8px', borderRadius: '8px', // this is needed otherwise :active state doesn't look nice, gap is required display: 'flex', @@ -446,7 +476,8 @@ export const DropdownV2 = ({
, - ClearIndicator: CustomClearIndicator, + ClearIndicator: showClearBtn ? CustomClearIndicator : () => null, DropdownIndicator: isMultiSelectLoading ? () => null : CustomDropdownIndicator, }} isClearable @@ -498,21 +528,15 @@ export const MultiselectV2 = ({ tabSelectsValue={false} controlShouldRenderValue={false} isSearchable={false} - onMenuOpen={() => { - setIsMultiselectOpen(true); - fireEvent('onFocus'); - }} - onMenuClose={() => { - setIsMultiselectOpen(false); - fireEvent('onBlur'); - }} onKeyDown={(e) => { - if (e.key === 'Enter' && !isMultiselectOpen) { + if (e.key === 'Enter' && !isMultiselectOpen && !isMultiSelectLoading) { setIsMultiselectOpen(true); + fireEvent('onFocus'); e.preventDefault(); } if (e.key === 'Escape' && isMultiselectOpen) { setIsMultiselectOpen(false); + fireEvent('onBlur'); e.preventDefault(); } }} @@ -520,19 +544,9 @@ export const MultiselectV2 = ({ icon={icon} doShowIcon={iconVisibility} containerRef={valueContainerRef} - showAllOption={showAllOption} - isSelectAllSelected={isSelectAllSelected} - setIsSelectAllSelected={(value) => { - setIsSelectAllSelected(value); - if (!value) { - fireEvent('onSelect'); - } - }} - setSelected={setInputValue} iconColor={iconColor} optionsLoadingState={optionsLoadingState && advanced} darkMode={darkMode} - fireEvent={() => fireEvent('onSelect')} menuPlacement="auto" menuPortalTarget={document.body} /> diff --git a/frontend/src/Editor/WidgetManager/configs/dropdownV2.js b/frontend/src/Editor/WidgetManager/configs/dropdownV2.js index 308aff1f36..6b011fd082 100644 --- a/frontend/src/Editor/WidgetManager/configs/dropdownV2.js +++ b/frontend/src/Editor/WidgetManager/configs/dropdownV2.js @@ -75,6 +75,18 @@ export const dropdownV2Config = { accordian: 'Options', isFxNotRequired: true, }, + showClearBtn: { + type: 'toggle', + displayName: 'Show clear selection button', + validation: { schema: { type: 'boolean' }, defaultValue: true }, + section: 'additionalActions', + }, + showSearchInput: { + type: 'toggle', + displayName: 'Show search in options', + validation: { schema: { type: 'boolean' }, defaultValue: true }, + section: 'additionalActions', + }, loadingState: { type: 'toggle', displayName: 'Loading state', @@ -314,6 +326,8 @@ export const dropdownV2Config = { optionsLoadingState: { value: '{{false}}' }, sort: { value: 'asc' }, placeholder: { value: 'Select an option' }, + showClearBtn: { value: '{{true}}' }, + showSearchInput: { value: '{{true}}' }, visibility: { value: '{{true}}' }, disabledState: { value: '{{false}}' }, loadingState: { value: '{{false}}' }, diff --git a/frontend/src/Editor/WidgetManager/configs/multiselectV2.js b/frontend/src/Editor/WidgetManager/configs/multiselectV2.js index b603db9c4a..0985f23d08 100644 --- a/frontend/src/Editor/WidgetManager/configs/multiselectV2.js +++ b/frontend/src/Editor/WidgetManager/configs/multiselectV2.js @@ -142,6 +142,18 @@ export const multiselectV2Config = { accordian: 'Options', isFxNotRequired: true, }, + showClearBtn: { + type: 'toggle', + displayName: 'Show clear selection button', + validation: { schema: { type: 'boolean' }, defaultValue: true }, + section: 'additionalActions', + }, + showSearchInput: { + type: 'toggle', + displayName: 'Show search in options', + validation: { schema: { type: 'boolean' }, defaultValue: true }, + section: 'additionalActions', + }, loadingState: { type: 'toggle', displayName: 'Loading state', @@ -327,6 +339,8 @@ export const multiselectV2Config = { optionsLoadingState: { value: '{{false}}' }, sort: { value: 'asc' }, placeholder: { value: 'Select the options' }, + showClearBtn: { value: '{{true}}' }, + showSearchInput: { value: '{{true}}' }, visibility: { value: '{{true}}' }, disabledState: { value: '{{false}}' }, loadingState: { value: '{{false}}' }, diff --git a/server/src/modules/apps/services/widget-config/dropdownV2.js b/server/src/modules/apps/services/widget-config/dropdownV2.js index cb90554e6b..d7534b25a8 100644 --- a/server/src/modules/apps/services/widget-config/dropdownV2.js +++ b/server/src/modules/apps/services/widget-config/dropdownV2.js @@ -75,6 +75,18 @@ export const dropdownV2Config = { accordian: 'Options', isFxNotRequired: true, }, + showClearBtn: { + type: 'toggle', + displayName: 'Show clear selection button', + validation: { schema: { type: 'boolean' }, defaultValue: true }, + section: 'additionalActions', + }, + showSearchInput: { + type: 'toggle', + displayName: 'Show search in options', + validation: { schema: { type: 'boolean' }, defaultValue: true }, + section: 'additionalActions', + }, loadingState: { type: 'toggle', displayName: 'Loading state', @@ -314,6 +326,8 @@ export const dropdownV2Config = { optionsLoadingState: { value: '{{false}}' }, sort: { value: 'asc' }, placeholder: { value: 'Select an option' }, + showClearBtn: { value: '{{true}}' }, + showSearchInput: { value: '{{true}}' }, visibility: { value: '{{true}}' }, disabledState: { value: '{{false}}' }, loadingState: { value: '{{false}}' }, diff --git a/server/src/modules/apps/services/widget-config/multiselectV2.js b/server/src/modules/apps/services/widget-config/multiselectV2.js index 4ab4af57ce..294423abac 100644 --- a/server/src/modules/apps/services/widget-config/multiselectV2.js +++ b/server/src/modules/apps/services/widget-config/multiselectV2.js @@ -142,6 +142,18 @@ export const multiselectV2Config = { accordian: 'Options', isFxNotRequired: true, }, + showClearBtn: { + type: 'toggle', + displayName: 'Show clear selection button', + validation: { schema: { type: 'boolean' }, defaultValue: true }, + section: 'additionalActions', + }, + showSearchInput: { + type: 'toggle', + displayName: 'Show search in options', + validation: { schema: { type: 'boolean' }, defaultValue: true }, + section: 'additionalActions', + }, loadingState: { type: 'toggle', displayName: 'Loading state', @@ -327,6 +339,8 @@ export const multiselectV2Config = { optionsLoadingState: { value: '{{false}}' }, sort: { value: 'asc' }, placeholder: { value: 'Select the options' }, + showClearBtn: { value: '{{true}}' }, + showSearchInput: { value: '{{true}}' }, visibility: { value: '{{true}}' }, disabledState: { value: '{{false}}' }, loadingState: { value: '{{false}}' },