This is an automated email from the ASF dual-hosted git repository.

justinpark pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new eda304bda9 chore(explore): Hide non-droppable metric and column list 
(#27717)
eda304bda9 is described below

commit eda304bda970a71e0884a41346eb17263dc9e718
Author: JUST.in DO IT <[email protected]>
AuthorDate: Fri Apr 5 09:29:05 2024 -0700

    chore(explore): Hide non-droppable metric and column list (#27717)
---
 .../DatasourcePanel/DatasourcePanel.test.tsx       |  83 ++++++++-
 .../DatasourcePanel/DatasourcePanelItem.test.tsx   |  31 ++++
 .../DatasourcePanel/DatasourcePanelItem.tsx        |  43 ++++-
 .../explore/components/DatasourcePanel/index.tsx   | 194 +++++++++++----------
 .../ExploreContainer/ExploreContainer.test.tsx     |  42 ++++-
 .../explore/components/ExploreContainer/index.tsx  |  36 +++-
 .../DndColumnSelectControl/DndSelectLabel.test.tsx |  40 +++++
 .../DndColumnSelectControl/DndSelectLabel.tsx      |  31 +++-
 8 files changed, 393 insertions(+), 107 deletions(-)

diff --git 
a/superset-frontend/src/explore/components/DatasourcePanel/DatasourcePanel.test.tsx
 
b/superset-frontend/src/explore/components/DatasourcePanel/DatasourcePanel.test.tsx
index 452ee4609c..a0c7d707d5 100644
--- 
a/superset-frontend/src/explore/components/DatasourcePanel/DatasourcePanel.test.tsx
+++ 
b/superset-frontend/src/explore/components/DatasourcePanel/DatasourcePanel.test.tsx
@@ -17,7 +17,7 @@
  * under the License.
  */
 import React from 'react';
-import { render, screen, waitFor } from 'spec/helpers/testing-library';
+import { render, screen, waitFor, within } from 'spec/helpers/testing-library';
 import userEvent from '@testing-library/user-event';
 import DatasourcePanel, {
   IDatasource,
@@ -29,6 +29,11 @@ import {
 } from 'src/explore/components/DatasourcePanel/fixtures';
 import { DatasourceType } from '@superset-ui/core';
 import DatasourceControl from 
'src/explore/components/controls/DatasourceControl';
+import ExploreContainer from '../ExploreContainer';
+import {
+  DndColumnSelect,
+  DndMetricSelect,
+} from '../controls/DndColumnSelectControl';
 
 jest.mock(
   'react-virtualized-auto-sizer',
@@ -83,6 +88,12 @@ const props: DatasourcePanelProps = {
   width: 300,
 };
 
+const metricProps = {
+  savedMetrics: [],
+  columns: [],
+  onChange: jest.fn(),
+};
+
 const search = (value: string, input: HTMLElement) => {
   userEvent.clear(input);
   userEvent.type(input, value);
@@ -104,7 +115,13 @@ test('should display items in controls', async () => {
 });
 
 test('should render the metrics', async () => {
-  render(<DatasourcePanel {...props} />, { useRedux: true, useDnd: true });
+  render(
+    <ExploreContainer>
+      <DatasourcePanel {...props} />
+      <DndMetricSelect {...metricProps} />
+    </ExploreContainer>,
+    { useRedux: true, useDnd: true },
+  );
   const metricsNum = metrics.length;
   metrics.forEach(metric =>
     expect(screen.getByText(metric.metric_name)).toBeInTheDocument(),
@@ -115,7 +132,13 @@ test('should render the metrics', async () => {
 });
 
 test('should render the columns', async () => {
-  render(<DatasourcePanel {...props} />, { useRedux: true, useDnd: true });
+  render(
+    <ExploreContainer>
+      <DatasourcePanel {...props} />
+      <DndMetricSelect {...metricProps} />
+    </ExploreContainer>,
+    { useRedux: true, useDnd: true },
+  );
   const columnsNum = columns.length;
   columns.forEach(col =>
     expect(screen.getByText(col.column_name)).toBeInTheDocument(),
@@ -134,7 +157,13 @@ test('should render 0 search results', async () => {
 });
 
 test('should search and render matching columns', async () => {
-  render(<DatasourcePanel {...props} />, { useRedux: true, useDnd: true });
+  render(
+    <ExploreContainer>
+      <DatasourcePanel {...props} />
+      <DndMetricSelect {...metricProps} />
+    </ExploreContainer>,
+    { useRedux: true, useDnd: true },
+  );
   const searchInput = screen.getByPlaceholderText('Search Metrics & Columns');
 
   search(columns[0].column_name, searchInput);
@@ -146,7 +175,13 @@ test('should search and render matching columns', async () 
=> {
 });
 
 test('should search and render matching metrics', async () => {
-  render(<DatasourcePanel {...props} />, { useRedux: true, useDnd: true });
+  render(
+    <ExploreContainer>
+      <DatasourcePanel {...props} />
+      <DndMetricSelect {...metricProps} />
+    </ExploreContainer>,
+    { useRedux: true, useDnd: true },
+  );
   const searchInput = screen.getByPlaceholderText('Search Metrics & Columns');
 
   search(metrics[0].metric_name, searchInput);
@@ -211,3 +246,41 @@ test('should not render a save dataset modal when 
datasource is not query or dat
 
   expect(screen.queryByText(/create a dataset/i)).toBe(null);
 });
+
+test('should render only droppable metrics and columns', async () => {
+  const column1FilterProps = {
+    type: 'DndColumnSelect' as const,
+    name: 'Filter',
+    onChange: jest.fn(),
+    options: [{ column_name: columns[1].column_name }],
+    actions: { setControlValue: jest.fn() },
+  };
+  const column2FilterProps = {
+    type: 'DndColumnSelect' as const,
+    name: 'Filter',
+    onChange: jest.fn(),
+    options: [
+      { column_name: columns[1].column_name },
+      { column_name: columns[2].column_name },
+    ],
+    actions: { setControlValue: jest.fn() },
+  };
+  const { getByTestId } = render(
+    <ExploreContainer>
+      <DatasourcePanel {...props} />
+      <DndColumnSelect {...column1FilterProps} />
+      <DndColumnSelect {...column2FilterProps} />
+    </ExploreContainer>,
+    { useRedux: true, useDnd: true },
+  );
+  const selections = getByTestId('fieldSelections');
+  expect(
+    within(selections).queryByText(columns[0].column_name),
+  ).not.toBeInTheDocument();
+  expect(
+    within(selections).queryByText(columns[1].column_name),
+  ).toBeInTheDocument();
+  expect(
+    within(selections).queryByText(columns[2].column_name),
+  ).toBeInTheDocument();
+});
diff --git 
a/superset-frontend/src/explore/components/DatasourcePanel/DatasourcePanelItem.test.tsx
 
b/superset-frontend/src/explore/components/DatasourcePanel/DatasourcePanelItem.test.tsx
index 76c4d58e2d..abe3207e4d 100644
--- 
a/superset-frontend/src/explore/components/DatasourcePanel/DatasourcePanelItem.test.tsx
+++ 
b/superset-frontend/src/explore/components/DatasourcePanel/DatasourcePanelItem.test.tsx
@@ -39,6 +39,8 @@ const mockData = {
   onCollapseMetricsChange: jest.fn(),
   collapseColumns: false,
   onCollapseColumnsChange: jest.fn(),
+  hiddenMetricCount: 0,
+  hiddenColumnCount: 0,
 };
 
 test('renders each item accordingly', () => {
@@ -166,3 +168,32 @@ test('can collapse metrics and columns', () => {
   );
   expect(queryByText('Columns')).toBeInTheDocument();
 });
+
+test('shows ineligible items count', () => {
+  const hiddenColumnCount = 3;
+  const hiddenMetricCount = 1;
+  const dataWithHiddenItems = {
+    ...mockData,
+    hiddenColumnCount,
+    hiddenMetricCount,
+  };
+  const { getByText, rerender } = render(
+    <DatasourcePanelItem index={1} data={dataWithHiddenItems} style={{}} />,
+    { useDnd: true },
+  );
+  expect(
+    getByText(`${hiddenMetricCount} ineligible item(s) are hidden`),
+  ).toBeInTheDocument();
+
+  const startIndexOfColumnSection = mockData.metricSlice.length + 3;
+  rerender(
+    <DatasourcePanelItem
+      index={startIndexOfColumnSection + 1}
+      data={dataWithHiddenItems}
+      style={{}}
+    />,
+  );
+  expect(
+    getByText(`${hiddenColumnCount} ineligible item(s) are hidden`),
+  ).toBeInTheDocument();
+});
diff --git 
a/superset-frontend/src/explore/components/DatasourcePanel/DatasourcePanelItem.tsx
 
b/superset-frontend/src/explore/components/DatasourcePanel/DatasourcePanelItem.tsx
index ab89019da2..85fd8dc3dc 100644
--- 
a/superset-frontend/src/explore/components/DatasourcePanel/DatasourcePanelItem.tsx
+++ 
b/superset-frontend/src/explore/components/DatasourcePanel/DatasourcePanelItem.tsx
@@ -51,6 +51,8 @@ type Props = {
     onCollapseMetricsChange: (collapse: boolean) => void;
     collapseColumns: boolean;
     onCollapseColumnsChange: (collapse: boolean) => void;
+    hiddenMetricCount: number;
+    hiddenColumnCount: number;
   };
 };
 
@@ -130,6 +132,19 @@ const SectionHeader = styled.span`
   `}
 `;
 
+const Box = styled.div`
+  ${({ theme }) => `
+    border: 1px ${theme.colors.grayscale.light4} solid;
+    border-radius: ${theme.gridUnit}px;
+    font-size: ${theme.typography.sizes.s}px;
+    padding: ${theme.gridUnit}px;
+    color: ${theme.colors.grayscale.light1};
+    text-overflow: ellipsis;
+    white-space: nowrap;
+    overflow: hidden;
+  `}
+`;
+
 const DatasourcePanelItem: React.FC<Props> = ({ index, style, data }) => {
   const {
     metricSlice: _metricSlice,
@@ -145,6 +160,8 @@ const DatasourcePanelItem: React.FC<Props> = ({ index, 
style, data }) => {
     onCollapseMetricsChange,
     collapseColumns,
     onCollapseColumnsChange,
+    hiddenMetricCount,
+    hiddenColumnCount,
   } = data;
   const metricSlice = collapseMetrics ? [] : _metricSlice;
 
@@ -169,6 +186,7 @@ const DatasourcePanelItem: React.FC<Props> = ({ index, 
style, data }) => {
     ? onShowAllColumnsChange
     : onShowAllMetricsChange;
   const theme = useTheme();
+  const hiddenCount = isColumnSection ? hiddenColumnCount : hiddenMetricCount;
 
   return (
     <div
@@ -190,10 +208,27 @@ const DatasourcePanelItem: React.FC<Props> = ({ index, 
style, data }) => {
         </SectionHeaderButton>
       )}
       {index === SUBTITLE_LINE && !collapsed && (
-        <div className="field-length">
-          {isColumnSection
-            ? t(`Showing %s of %s`, columnSlice?.length, totalColumns)
-            : t(`Showing %s of %s`, metricSlice?.length, totalMetrics)}
+        <div
+          css={css`
+            display: flex;
+            gap: ${theme.gridUnit * 2}px;
+            justify-content: space-between;
+            align-items: baseline;
+          `}
+        >
+          <div
+            className="field-length"
+            css={css`
+              flex-shrink: 0;
+            `}
+          >
+            {isColumnSection
+              ? t(`Showing %s of %s`, columnSlice?.length, totalColumns)
+              : t(`Showing %s of %s`, metricSlice?.length, totalMetrics)}
+          </div>
+          {hiddenCount > 0 && (
+            <Box>{t(`%s ineligible item(s) are hidden`, hiddenCount)}</Box>
+          )}
         </div>
       )}
       {index > SUBTITLE_LINE && index < BOTTOM_LINE && (
diff --git a/superset-frontend/src/explore/components/DatasourcePanel/index.tsx 
b/superset-frontend/src/explore/components/DatasourcePanel/index.tsx
index 395b70061a..c82f27d011 100644
--- a/superset-frontend/src/explore/components/DatasourcePanel/index.tsx
+++ b/superset-frontend/src/explore/components/DatasourcePanel/index.tsx
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import React, { useEffect, useMemo, useState } from 'react';
+import React, { useContext, useMemo, useState } from 'react';
 import {
   css,
   DatasourceType,
@@ -30,7 +30,7 @@ import { ControlConfig } from '@superset-ui/chart-controls';
 import AutoSizer from 'react-virtualized-auto-sizer';
 import { FixedSizeList as List } from 'react-window';
 
-import { debounce, isArray } from 'lodash';
+import { isArray } from 'lodash';
 import { matchSorter, rankings } from 'match-sorter';
 import Alert from 'src/components/Alert';
 import { SaveDatasetModal } from 'src/SqlLab/components/SaveDatasetModal';
@@ -39,12 +39,16 @@ import { Input } from 'src/components/Input';
 import { FAST_DEBOUNCE } from 'src/constants';
 import { ExploreActions } from 'src/explore/actions/exploreActions';
 import Control from 'src/explore/components/Control';
+import { useDebounceValue } from 'src/hooks/useDebounceValue';
 import DatasourcePanelItem, {
   ITEM_HEIGHT,
   DataSourcePanelColumn,
   DEFAULT_MAX_COLUMNS_LENGTH,
   DEFAULT_MAX_METRICS_LENGTH,
 } from './DatasourcePanelItem';
+import { DndItemType } from '../DndItemType';
+import { DndItemValue } from './types';
+import { DropzoneContext } from '../ExploreContainer';
 
 interface DatasourceControl extends ControlConfig {
   datasource?: IDatasource;
@@ -122,6 +126,9 @@ const StyledInfoboxWrapper = styled.div`
 
 const BORDER_WIDTH = 2;
 
+const sortCertifiedFirst = (slice: DataSourcePanelColumn[]) =>
+  slice.sort((a, b) => (b?.is_certified ?? 0) - (a?.is_certified ?? 0));
+
 export default function DataSourcePanel({
   datasource,
   formData,
@@ -129,11 +136,26 @@ export default function DataSourcePanel({
   actions,
   width,
 }: Props) {
+  const [dropzones] = useContext(DropzoneContext);
   const { columns: _columns, metrics } = datasource;
+
+  const allowedColumns = useMemo(() => {
+    const validators = Object.values(dropzones);
+    if (!isArray(_columns)) return [];
+    return _columns.filter(column =>
+      validators.some(validator =>
+        validator({
+          value: column as DndItemValue,
+          type: DndItemType.Column,
+        }),
+      ),
+    );
+  }, [dropzones, _columns]);
+
   // display temporal column first
   const columns = useMemo(
     () =>
-      [...(isArray(_columns) ? _columns : [])].sort((col1, col2) => {
+      [...allowedColumns].sort((col1, col2) => {
         if (col1?.is_dttm && !col2?.is_dttm) {
           return -1;
         }
@@ -142,106 +164,102 @@ export default function DataSourcePanel({
         }
         return 0;
       }),
-    [_columns],
+    [allowedColumns],
   );
 
+  const allowedMetrics = useMemo(() => {
+    const validators = Object.values(dropzones);
+    return metrics.filter(metric =>
+      validators.some(validator =>
+        validator({ value: metric, type: DndItemType.Metric }),
+      ),
+    );
+  }, [dropzones, metrics]);
+
+  const hiddenColumnCount = _columns.length - allowedColumns.length;
+  const hiddenMetricCount = metrics.length - allowedMetrics.length;
   const [showSaveDatasetModal, setShowSaveDatasetModal] = useState(false);
   const [inputValue, setInputValue] = useState('');
-  const [lists, setList] = useState({
-    columns,
-    metrics,
-  });
   const [showAllMetrics, setShowAllMetrics] = useState(false);
   const [showAllColumns, setShowAllColumns] = useState(false);
   const [collapseMetrics, setCollapseMetrics] = useState(false);
   const [collapseColumns, setCollapseColumns] = useState(false);
+  const searchKeyword = useDebounceValue(inputValue, FAST_DEBOUNCE);
 
-  const search = useMemo(
-    () =>
-      debounce((value: string) => {
-        if (value === '') {
-          setList({ columns, metrics });
-          return;
-        }
-        setList({
-          columns: matchSorter(columns, value, {
-            keys: [
-              {
-                key: 'verbose_name',
-                threshold: rankings.CONTAINS,
-              },
-              {
-                key: 'column_name',
-                threshold: rankings.CONTAINS,
-              },
-              {
-                key: item =>
-                  [item?.description ?? '', item?.expression ?? ''].map(
-                    x => x?.replace(/[_\n\s]+/g, ' ') || '',
-                  ),
-                threshold: rankings.CONTAINS,
-                maxRanking: rankings.CONTAINS,
-              },
-            ],
-            keepDiacritics: true,
-          }),
-          metrics: matchSorter(metrics, value, {
-            keys: [
-              {
-                key: 'verbose_name',
-                threshold: rankings.CONTAINS,
-              },
-              {
-                key: 'metric_name',
-                threshold: rankings.CONTAINS,
-              },
-              {
-                key: item =>
-                  [item?.description ?? '', item?.expression ?? ''].map(
-                    x => x?.replace(/[_\n\s]+/g, ' ') || '',
-                  ),
-                threshold: rankings.CONTAINS,
-                maxRanking: rankings.CONTAINS,
-              },
-            ],
-            keepDiacritics: true,
-            baseSort: (a, b) =>
-              Number(b?.item?.is_certified ?? 0) -
-                Number(a?.item?.is_certified ?? 0) ||
-              String(a?.rankedValue ?? '').localeCompare(b?.rankedValue ?? ''),
-          }),
-        });
-      }, FAST_DEBOUNCE),
-    [columns, metrics],
-  );
-
-  useEffect(() => {
-    setList({
-      columns,
-      metrics,
+  const filteredColumns = useMemo(() => {
+    if (!searchKeyword) {
+      return columns ?? [];
+    }
+    return matchSorter(columns, searchKeyword, {
+      keys: [
+        {
+          key: 'verbose_name',
+          threshold: rankings.CONTAINS,
+        },
+        {
+          key: 'column_name',
+          threshold: rankings.CONTAINS,
+        },
+        {
+          key: item =>
+            [item?.description ?? '', item?.expression ?? ''].map(
+              x => x?.replace(/[_\n\s]+/g, ' ') || '',
+            ),
+          threshold: rankings.CONTAINS,
+          maxRanking: rankings.CONTAINS,
+        },
+      ],
+      keepDiacritics: true,
     });
-    setInputValue('');
-  }, [columns, datasource, metrics]);
+  }, [columns, searchKeyword]);
 
-  const sortCertifiedFirst = (slice: DataSourcePanelColumn[]) =>
-    slice.sort((a, b) => (b?.is_certified ?? 0) - (a?.is_certified ?? 0));
+  const filteredMetrics = useMemo(() => {
+    if (!searchKeyword) {
+      return allowedMetrics ?? [];
+    }
+    return matchSorter(allowedMetrics, searchKeyword, {
+      keys: [
+        {
+          key: 'verbose_name',
+          threshold: rankings.CONTAINS,
+        },
+        {
+          key: 'metric_name',
+          threshold: rankings.CONTAINS,
+        },
+        {
+          key: item =>
+            [item?.description ?? '', item?.expression ?? ''].map(
+              x => x?.replace(/[_\n\s]+/g, ' ') || '',
+            ),
+          threshold: rankings.CONTAINS,
+          maxRanking: rankings.CONTAINS,
+        },
+      ],
+      keepDiacritics: true,
+      baseSort: (a, b) =>
+        Number(b?.item?.is_certified ?? 0) -
+          Number(a?.item?.is_certified ?? 0) ||
+        String(a?.rankedValue ?? '').localeCompare(b?.rankedValue ?? ''),
+    });
+  }, [allowedMetrics, searchKeyword]);
 
   const metricSlice = useMemo(
     () =>
       showAllMetrics
-        ? lists?.metrics
-        : lists?.metrics?.slice?.(0, DEFAULT_MAX_METRICS_LENGTH),
-    [lists?.metrics, showAllMetrics],
+        ? filteredMetrics
+        : filteredMetrics?.slice?.(0, DEFAULT_MAX_METRICS_LENGTH),
+    [filteredMetrics, showAllMetrics],
   );
 
   const columnSlice = useMemo(
     () =>
       showAllColumns
-        ? sortCertifiedFirst(lists?.columns)
+        ? sortCertifiedFirst(filteredColumns)
         : sortCertifiedFirst(
-            lists?.columns?.slice?.(0, DEFAULT_MAX_COLUMNS_LENGTH),
+            filteredColumns?.slice?.(0, DEFAULT_MAX_COLUMNS_LENGTH),
           ),
-    [lists.columns, showAllColumns],
+    [filteredColumns, showAllColumns],
   );
 
   const showInfoboxCheck = () => {
@@ -268,13 +286,12 @@ export default function DataSourcePanel({
           allowClear
           onChange={evt => {
             setInputValue(evt.target.value);
-            search(evt.target.value);
           }}
           value={inputValue}
           className="form-control input-md"
           placeholder={t('Search Metrics & Columns')}
         />
-        <div className="field-selections">
+        <div className="field-selections" data-test="fieldSelections">
           {datasourceIsSaveable && showInfoboxCheck() && (
             <StyledInfoboxWrapper>
               <Alert
@@ -321,8 +338,8 @@ export default function DataSourcePanel({
                   metricSlice,
                   columnSlice,
                   width,
-                  totalMetrics: lists?.metrics.length,
-                  totalColumns: lists?.columns.length,
+                  totalMetrics: filteredMetrics.length,
+                  totalColumns: filteredColumns.length,
                   showAllMetrics,
                   onShowAllMetricsChange: setShowAllMetrics,
                   showAllColumns,
@@ -331,6 +348,8 @@ export default function DataSourcePanel({
                   onCollapseMetricsChange: setCollapseMetrics,
                   collapseColumns,
                   onCollapseColumnsChange: setCollapseColumns,
+                  hiddenMetricCount,
+                  hiddenColumnCount,
                 }}
                 overscanCount={5}
               >
@@ -345,10 +364,9 @@ export default function DataSourcePanel({
     [
       columnSlice,
       inputValue,
-      lists.columns.length,
-      lists?.metrics?.length,
+      filteredColumns.length,
+      filteredMetrics.length,
       metricSlice,
-      search,
       showAllColumns,
       showAllMetrics,
       collapseMetrics,
diff --git 
a/superset-frontend/src/explore/components/ExploreContainer/ExploreContainer.test.tsx
 
b/superset-frontend/src/explore/components/ExploreContainer/ExploreContainer.test.tsx
index 50922256ea..fcfc351f4d 100644
--- 
a/superset-frontend/src/explore/components/ExploreContainer/ExploreContainer.test.tsx
+++ 
b/superset-frontend/src/explore/components/ExploreContainer/ExploreContainer.test.tsx
@@ -20,7 +20,7 @@ import React from 'react';
 import { fireEvent, render } from 'spec/helpers/testing-library';
 import { OptionControlLabel } from 
'src/explore/components/controls/OptionControls';
 
-import ExploreContainer, { DraggingContext } from '.';
+import ExploreContainer, { DraggingContext, DropzoneContext } from '.';
 import OptionWrapper from '../controls/DndColumnSelectControl/OptionWrapper';
 
 const MockChildren = () => {
@@ -32,6 +32,24 @@ const MockChildren = () => {
   );
 };
 
+const MockChildren2 = () => {
+  const [zones, dispatch] = React.useContext(DropzoneContext);
+  return (
+    <>
+      <div data-test="mock-children">{Object.keys(zones).join(':')}</div>
+      <button
+        type="button"
+        onClick={() => dispatch({ key: 'test_item_1', canDrop: () => true })}
+      >
+        Add
+      </button>
+      <button type="button" onClick={() => dispatch({ key: 'test_item_1' })}>
+        Remove
+      </button>
+    </>
+  );
+};
+
 test('should render children', () => {
   const { getByTestId, getByText } = render(
     <ExploreContainer>
@@ -43,7 +61,7 @@ test('should render children', () => {
   expect(getByText('not dragging')).toBeInTheDocument();
 });
 
-test('should update the style on dragging state', () => {
+test('should propagate dragging state', () => {
   const defaultProps = {
     label: <span>Test label</span>,
     tooltipTitle: 'This is a tooltip title',
@@ -83,3 +101,23 @@ test('should update the style on dragging state', () => {
   fireEvent.dragStart(getByText('Label 2'));
   expect(container.getElementsByClassName('dragging')).toHaveLength(0);
 });
+
+test('should manage the dropValidators', () => {
+  const { queryByText, getByText } = render(
+    <ExploreContainer>
+      <MockChildren2 />
+    </ExploreContainer>,
+    {
+      useRedux: true,
+      useDnd: true,
+    },
+  );
+
+  expect(queryByText('test_item_1')).not.toBeInTheDocument();
+  const addDropValidatorButton = getByText('Add');
+  fireEvent.click(addDropValidatorButton);
+  expect(getByText('test_item_1')).toBeInTheDocument();
+  const removeDropValidatorButton = getByText('Remove');
+  fireEvent.click(removeDropValidatorButton);
+  expect(queryByText('test_item_1')).not.toBeInTheDocument();
+});
diff --git 
a/superset-frontend/src/explore/components/ExploreContainer/index.tsx 
b/superset-frontend/src/explore/components/ExploreContainer/index.tsx
index 3c64373948..6f4bb7a370 100644
--- a/superset-frontend/src/explore/components/ExploreContainer/index.tsx
+++ b/superset-frontend/src/explore/components/ExploreContainer/index.tsx
@@ -16,17 +16,41 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import React, { useEffect } from 'react';
+import React, { useEffect, Dispatch, useReducer } from 'react';
 import { styled } from '@superset-ui/core';
 import { useDragDropManager } from 'react-dnd';
+import { DatasourcePanelDndItem } from '../DatasourcePanel/types';
+
+type CanDropValidator = (item: DatasourcePanelDndItem) => boolean;
+type DropzoneSet = Record<string, CanDropValidator>;
+type Action = { key: string; canDrop?: CanDropValidator };
 
 export const DraggingContext = React.createContext(false);
+export const DropzoneContext = React.createContext<
+  [DropzoneSet, Dispatch<Action>]
+>([{}, () => {}]);
 const StyledDiv = styled.div`
   display: flex;
   flex-direction: column;
   height: 100%;
   min-height: 0;
 `;
+
+const reducer = (state: DropzoneSet = {}, action: Action) => {
+  if (action.canDrop) {
+    return {
+      ...state,
+      [action.key]: action.canDrop,
+    };
+  }
+  if (action.key) {
+    const newState = { ...state };
+    delete newState[action.key];
+    return newState;
+  }
+  return state;
+};
+
 const ExploreContainer: React.FC<{}> = ({ children }) => {
   const dragDropManager = useDragDropManager();
   const [dragging, setDragging] = React.useState(
@@ -50,10 +74,14 @@ const ExploreContainer: React.FC<{}> = ({ children }) => {
     };
   }, [dragDropManager]);
 
+  const dropzoneValue = useReducer(reducer, {});
+
   return (
-    <DraggingContext.Provider value={dragging}>
-      <StyledDiv>{children}</StyledDiv>
-    </DraggingContext.Provider>
+    <DropzoneContext.Provider value={dropzoneValue}>
+      <DraggingContext.Provider value={dragging}>
+        <StyledDiv>{children}</StyledDiv>
+      </DraggingContext.Provider>
+    </DropzoneContext.Provider>
   );
 };
 
diff --git 
a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndSelectLabel.test.tsx
 
b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndSelectLabel.test.tsx
index dcf0e4d1ee..689c76d6c8 100644
--- 
a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndSelectLabel.test.tsx
+++ 
b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndSelectLabel.test.tsx
@@ -23,6 +23,7 @@ import { DndItemType } from 
'src/explore/components/DndItemType';
 import DndSelectLabel, {
   DndSelectLabelProps,
 } from 'src/explore/components/controls/DndColumnSelectControl/DndSelectLabel';
+import ExploreContainer, { DropzoneContext } from '../../ExploreContainer';
 
 const defaultProps: DndSelectLabelProps = {
   name: 'Column',
@@ -33,6 +34,23 @@ const defaultProps: DndSelectLabelProps = {
   ghostButtonText: 'Drop columns here or click',
   onClickGhostButton: jest.fn(),
 };
+const MockChildren = () => {
+  const [zones] = React.useContext(DropzoneContext);
+  return (
+    <>
+      {Object.keys(zones).map(key => (
+        <div key={key} data-test={`mock-result-${key}`}>
+          {String(
+            zones[key]({
+              value: { column_name: 'test' },
+              type: DndItemType.Column,
+            }),
+          )}
+        </div>
+      ))}
+    </>
+  );
+};
 
 test('renders with default props', () => {
   render(<DndSelectLabel {...defaultProps} />, { useDnd: true });
@@ -62,3 +80,25 @@ test('Handles ghost button click', () => {
   userEvent.click(screen.getByText('Drop columns here or click'));
   expect(defaultProps.onClickGhostButton).toHaveBeenCalled();
 });
+
+test('updates dropValidator on changes', () => {
+  const { getByTestId, rerender } = render(
+    <ExploreContainer>
+      <DndSelectLabel {...defaultProps} />
+      <MockChildren />
+    </ExploreContainer>,
+    { useDnd: true },
+  );
+  expect(getByTestId(`mock-result-${defaultProps.name}`)).toHaveTextContent(
+    'false',
+  );
+  rerender(
+    <ExploreContainer>
+      <DndSelectLabel {...defaultProps} canDrop={() => true} />
+      <MockChildren />
+    </ExploreContainer>,
+  );
+  expect(getByTestId(`mock-result-${defaultProps.name}`)).toHaveTextContent(
+    'true',
+  );
+});
diff --git 
a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndSelectLabel.tsx
 
b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndSelectLabel.tsx
index f4da2d1729..51ad92f879 100644
--- 
a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndSelectLabel.tsx
+++ 
b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndSelectLabel.tsx
@@ -16,7 +16,13 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import React, { ReactNode, useContext, useMemo } from 'react';
+import React, {
+  ReactNode,
+  useCallback,
+  useContext,
+  useEffect,
+  useMemo,
+} from 'react';
 import { useDrop } from 'react-dnd';
 import { t, useTheme } from '@superset-ui/core';
 import ControlHeader from 'src/explore/components/ControlHeader';
@@ -31,7 +37,7 @@ import {
 } from 'src/explore/components/DatasourcePanel/types';
 import Icons from 'src/components/Icons';
 import { DndItemType } from '../../DndItemType';
-import { DraggingContext } from '../../ExploreContainer';
+import { DraggingContext, DropzoneContext } from '../../ExploreContainer';
 
 export type DndSelectLabelProps = {
   name: string;
@@ -55,6 +61,14 @@ export default function DndSelectLabel({
   ...props
 }: DndSelectLabelProps) {
   const theme = useTheme();
+  const canDropProp = props.canDrop;
+  const canDropValueProp = props.canDropValue;
+
+  const dropValidator = useCallback(
+    (item: DatasourcePanelDndItem) =>
+      canDropProp(item) && (canDropValueProp?.(item.value) ?? true),
+    [canDropProp, canDropValueProp],
+  );
 
   const [{ isOver, canDrop }, datasourcePanelDrop] = useDrop({
     accept: isLoading ? [] : accept,
@@ -64,8 +78,7 @@ export default function DndSelectLabel({
       props.onDropValue?.(item.value);
     },
 
-    canDrop: (item: DatasourcePanelDndItem) =>
-      props.canDrop(item) && (props.canDropValue?.(item.value) ?? true),
+    canDrop: dropValidator,
 
     collect: monitor => ({
       isOver: monitor.isOver(),
@@ -73,6 +86,16 @@ export default function DndSelectLabel({
       type: monitor.getItemType(),
     }),
   });
+
+  const dispatch = useContext(DropzoneContext)[1];
+
+  useEffect(() => {
+    dispatch({ key: props.name, canDrop: dropValidator });
+    return () => {
+      dispatch({ key: props.name });
+    };
+  }, [dispatch, props.name, dropValidator]);
+
   const isDragging = useContext(DraggingContext);
 
   const values = useMemo(() => valuesRenderer(), [valuesRenderer]);

Reply via email to