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]);