From eda304bda970a71e0884a41346eb17263dc9e718 Mon Sep 17 00:00:00 2001 From: "JUST.in DO IT" Date: Fri, 5 Apr 2024 09:29:05 -0700 Subject: [PATCH] chore(explore): Hide non-droppable metric and column list (#27717) --- .../DatasourcePanel/DatasourcePanel.test.tsx | 83 +++++++- .../DatasourcePanelItem.test.tsx | 31 +++ .../DatasourcePanel/DatasourcePanelItem.tsx | 43 +++- .../components/DatasourcePanel/index.tsx | 194 ++++++++++-------- .../ExploreContainer.test.tsx | 42 +++- .../components/ExploreContainer/index.tsx | 36 +++- .../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 452ee4609c4d2..a0c7d707d5504 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(, { useRedux: true, useDnd: true }); + render( + + + + , + { 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(, { useRedux: true, useDnd: true }); + render( + + + + , + { 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(, { useRedux: true, useDnd: true }); + render( + + + + , + { 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(, { useRedux: true, useDnd: true }); + render( + + + + , + { 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( + + + + + , + { 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 76c4d58e2d172..abe3207e4d186 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( + , + { useDnd: true }, + ); + expect( + getByText(`${hiddenMetricCount} ineligible item(s) are hidden`), + ).toBeInTheDocument(); + + const startIndexOfColumnSection = mockData.metricSlice.length + 3; + rerender( + , + ); + 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 ab89019da29d1..85fd8dc3dc944 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 = ({ index, style, data }) => { const { metricSlice: _metricSlice, @@ -145,6 +160,8 @@ const DatasourcePanelItem: React.FC = ({ index, style, data }) => { onCollapseMetricsChange, collapseColumns, onCollapseColumnsChange, + hiddenMetricCount, + hiddenColumnCount, } = data; const metricSlice = collapseMetrics ? [] : _metricSlice; @@ -169,6 +186,7 @@ const DatasourcePanelItem: React.FC = ({ index, style, data }) => { ? onShowAllColumnsChange : onShowAllMetricsChange; const theme = useTheme(); + const hiddenCount = isColumnSection ? hiddenColumnCount : hiddenMetricCount; return (
= ({ index, style, data }) => { )} {index === SUBTITLE_LINE && !collapsed && ( -
- {isColumnSection - ? t(`Showing %s of %s`, columnSlice?.length, totalColumns) - : t(`Showing %s of %s`, metricSlice?.length, totalMetrics)} +
+
+ {isColumnSection + ? t(`Showing %s of %s`, columnSlice?.length, totalColumns) + : t(`Showing %s of %s`, metricSlice?.length, totalMetrics)} +
+ {hiddenCount > 0 && ( + {t(`%s ineligible item(s) are hidden`, hiddenCount)} + )}
)} {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 395b70061a11a..c82f27d01127c 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')} /> -
+
{datasourceIsSaveable && showInfoboxCheck() && ( @@ -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 50922256eaeee..fcfc351f4d3dc 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 ( + <> +
{Object.keys(zones).join(':')}
+ + + + ); +}; + test('should render children', () => { const { getByTestId, getByText } = render( @@ -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: Test label, 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( + + + , + { + 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 3c643739487e5..6f4bb7a370577 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; +type Action = { key: string; canDrop?: CanDropValidator }; export const DraggingContext = React.createContext(false); +export const DropzoneContext = React.createContext< + [DropzoneSet, Dispatch] +>([{}, () => {}]); 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 ( - - {children} - + + + {children} + + ); }; 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 dcf0e4d1eecfd..689c76d6c8881 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 => ( +
+ {String( + zones[key]({ + value: { column_name: 'test' }, + type: DndItemType.Column, + }), + )} +
+ ))} + + ); +}; test('renders with default props', () => { render(, { 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( + + + + , + { useDnd: true }, + ); + expect(getByTestId(`mock-result-${defaultProps.name}`)).toHaveTextContent( + 'false', + ); + rerender( + + true} /> + + , + ); + 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 f4da2d1729dc9..51ad92f879429 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]);