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>
       )}

Reply via email to