This is an automated email from the ASF dual-hosted git repository. msyavuz pushed a commit to branch msyavuz/feat/select-deselect-all in repository https://gitbox.apache.org/repos/asf/superset.git
commit 043964dfc347966b36a0e86f1423354ddbab6fa7 Author: Mehmet Salih Yavuz <[email protected]> AuthorDate: Tue Apr 8 22:17:51 2025 +0300 feat(Select): select-deselect all that works on visible items --- superset-frontend/src/components/Select/Select.tsx | 123 ++++++++++++++++++++- superset-frontend/src/components/Select/utils.tsx | 2 + 2 files changed, 122 insertions(+), 3 deletions(-) diff --git a/superset-frontend/src/components/Select/Select.tsx b/superset-frontend/src/components/Select/Select.tsx index 0a3e04f88d..e9775981fd 100644 --- a/superset-frontend/src/components/Select/Select.tsx +++ b/superset-frontend/src/components/Select/Select.tsx @@ -29,6 +29,7 @@ import { } from 'react'; import { + css, ensureIsArray, formatNumber, NumberFormats, @@ -73,6 +74,8 @@ import { DEFAULT_SORT_COMPARATOR, } from './constants'; import { customTagRender } from './CustomTag'; +import Button from '../Button'; +import { Space } from '../Space'; /** * This component is a customized version of the Antdesign 4.X Select component @@ -131,6 +134,8 @@ const Select = forwardRef( const [inputValue, setInputValue] = useState(''); const [isLoading, setIsLoading] = useState(loading); const [isDropdownVisible, setIsDropdownVisible] = useState(false); + const [isSearching, setIsSearching] = useState(false); + const [visibleOptions, setVisibleOptions] = useState<SelectOptionsType>([]); const [maxTagCount, setMaxTagCount] = useState( propsMaxTagCount ?? MAX_TAG_COUNT, ); @@ -220,12 +225,14 @@ const Select = forwardRef( const selectAllEnabled = useMemo( () => + !isSearching && !isSingleMode && allowSelectAll && selectOptions.length > 0 && enabledOptions.length > 1 && !inputValue, [ + isSearching, isSingleMode, allowSelectAll, selectOptions.length, @@ -339,6 +346,7 @@ const Select = forwardRef( const handleOnSearch = debounce((search: string) => { const searchValue = search.trim(); + setIsSearching(!!searchValue); if (allowNewOptions) { const newOption = searchValue && !hasOption(searchValue, fullSelectOptions, true) && { @@ -354,6 +362,15 @@ const Select = forwardRef( : cleanSelectOptions; setSelectOptions(newOptions); } + const filteredOptions = fullSelectOptions.filter(option => + handleFilterOptionHelper( + search, + option as AntdLabeledValue, + ['label', 'value'], + true, + ), + ); + setVisibleOptions(filteredOptions); setInputValue(searchValue); onSearch?.(searchValue); }, FAST_DEBOUNCE); @@ -372,12 +389,110 @@ const Select = forwardRef( if (!isEqual(initialOptionsSorted, selectOptions)) { setSelectOptions(initialOptionsSorted); } + setVisibleOptions(fullSelectOptions); } if (onDropdownVisibleChange) { onDropdownVisibleChange(isDropdownVisible); } }; + const handleSelectAll = useCallback(() => { + if (isSingleMode) return; + + const optionsToSelect = isSearching ? visibleOptions : enabledOptions; + + const currentValues = ensureIsArray(selectValue); + const currentValuesSet = new Set(currentValues.map(getValue)); + + const newValues = [...currentValues] as AntdLabeledValue[]; + optionsToSelect.forEach(option => { + if (!option.disabled && !currentValuesSet.has(option.value)) { + if (labelInValue) { + newValues.push({ + label: option.label, + value: option.value, + }); + } else { + newValues.push(option.value); + } + } + }); + + setSelectValue(newValues); + fireOnChange(); + }, [ + isSingleMode, + isSearching, + visibleOptions, + enabledOptions, + selectValue, + labelInValue, + fireOnChange, + ]); + + const handleDeselectAll = useCallback(() => { + if (isSingleMode) return; + + const optionsToDeselect = isSearching ? visibleOptions : enabledOptions; + const deselectionValues = new Set( + optionsToDeselect.map(opt => opt.value), + ); + + const newValues = ensureIsArray(selectValue).filter(item => { + const itemValue = getValue(item); + return ( + !deselectionValues.has(itemValue) && itemValue !== SELECT_ALL_VALUE + ); + }) as AntdLabeledValue[]; + + setSelectValue(newValues); + fireOnChange(); + }, [ + isSingleMode, + isSearching, + visibleOptions, + enabledOptions, + selectValue, + fireOnChange, + ]); + + useEffect(() => { + console.log({ visibleOptions }); + }, [visibleOptions]); + + const bulkSelectComponent = useMemo( + () => ( + <Space + css={css` + display: flex; + justify-content: space-around; + margin-bottom: 4px; + width: 90%; + `} + > + <Button + onClick={e => { + e.preventDefault(); + e.stopPropagation(); + handleSelectAll(); + }} + > + {t('Select all')} + </Button> + <Button + onClick={e => { + e.preventDefault(); + e.stopPropagation(); + handleDeselectAll(); + }} + > + {t('Deselect all')} + </Button> + </Space> + ), + [handleSelectAll, handleDeselectAll], + ); + const dropdownRender = ( originNode: ReactElement & { ref?: RefObject<HTMLElement> }, ) => @@ -387,6 +502,8 @@ const Select = forwardRef( isLoading, fullSelectOptions.length, helperText, + undefined, + isSearching ? bulkSelectComponent : undefined, ); const handleClear = () => { @@ -399,6 +516,7 @@ const Select = forwardRef( useEffect(() => { // when `options` list is updated from component prop, reset states setSelectOptions(initialOptions); + setVisibleOptions(initialOptions); }, [initialOptions]); useEffect(() => { @@ -651,7 +769,7 @@ const Select = forwardRef( <StyledCheckOutlined iconSize="m" aria-label="check" /> ) } - options={shouldRenderChildrenOptions ? undefined : fullSelectOptions} + options={shouldRenderChildrenOptions ? undefined : visibleOptions} oneLine={oneLine} tagRender={customTagRender} {...props} @@ -667,8 +785,7 @@ const Select = forwardRef( {selectAllLabel()} </Option> )} - {shouldRenderChildrenOptions && - renderSelectOptions(fullSelectOptions)} + {shouldRenderChildrenOptions && renderSelectOptions(visibleOptions)} </StyledSelect> </StyledContainer> ); diff --git a/superset-frontend/src/components/Select/utils.tsx b/superset-frontend/src/components/Select/utils.tsx index 7025b7951a..d0a1c93d0e 100644 --- a/superset-frontend/src/components/Select/utils.tsx +++ b/superset-frontend/src/components/Select/utils.tsx @@ -158,6 +158,7 @@ export const dropDownRenderHelper = ( optionsLength: number, helperText: string | undefined, errorComponent?: JSX.Element, + bulkSelectComponents?: JSX.Element, ) => { if (!isDropdownVisible) { originNode.ref?.current?.scrollTo({ top: 0 }); @@ -170,6 +171,7 @@ export const dropDownRenderHelper = ( } return ( <> + {bulkSelectComponents} {helperText && ( <StyledHelperText role="note">{helperText}</StyledHelperText> )}
