mirror of
https://github.com/apache/superset.git
synced 2026-04-07 10:31:50 +00:00
refactor(Filter components): migrate from react-dnd to dnd-kit (#37445)
This commit is contained in:
@@ -47,9 +47,7 @@ import {
|
||||
} from './shared_dashboard_functions';
|
||||
|
||||
function selectFilter(index: number) {
|
||||
cy.get("[data-test='filter-title-container'] [draggable='true']")
|
||||
.eq(index)
|
||||
.click();
|
||||
cy.get("[data-test='filter-title-container'] [role='tab']").eq(index).click();
|
||||
}
|
||||
|
||||
function closeFilterModal() {
|
||||
|
||||
@@ -16,16 +16,25 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { FC, ReactNode, useCallback } from 'react';
|
||||
import { FC, ReactNode, useCallback, useState } from 'react';
|
||||
import { t } from '@apache-superset/core';
|
||||
import { NativeFilterType, ChartCustomizationType } from '@superset-ui/core';
|
||||
import { styled } from '@apache-superset/core/ui';
|
||||
import { Collapse, Flex } from '@superset-ui/core/components';
|
||||
import type { DragEndEvent } from '@dnd-kit/core';
|
||||
import {
|
||||
DndContext,
|
||||
KeyboardSensor,
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
closestCenter,
|
||||
} from '@dnd-kit/core';
|
||||
import NewItemDropdown from '../NewItemDropdown';
|
||||
import ItemSectionContent from './ItemSection';
|
||||
import { FilterRemoval } from '../types';
|
||||
import { FILTER_TYPE, CUSTOMIZATION_TYPE } from '../DraggableFilter';
|
||||
import { isFilterId, isChartCustomizationId } from '../utils';
|
||||
import { isFilterId, isChartCustomizationId, isDivider } from '../utils';
|
||||
|
||||
const StyledSidebarFlex = styled(Flex)`
|
||||
min-width: 290px;
|
||||
@@ -37,11 +46,16 @@ const StyledHeaderFlex = styled(Flex)`
|
||||
padding: ${({ theme }) => theme.sizeUnit * 3}px;
|
||||
`;
|
||||
|
||||
const BaseStyledCollapse = styled(Collapse)`
|
||||
const BaseStyledCollapse = styled(Collapse)<{ isDragging: boolean }>`
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
.ant-collapse-content-box {
|
||||
padding: 0;
|
||||
${({ isDragging }) =>
|
||||
isDragging &&
|
||||
`
|
||||
overflow: hidden;
|
||||
`}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -78,6 +92,7 @@ export interface ConfigModalSidebarProps {
|
||||
targetType: 'filter' | 'customization',
|
||||
) => void;
|
||||
itemTitles?: Record<string, string>;
|
||||
formValuesVersion?: number;
|
||||
}
|
||||
|
||||
const ConfigModalSidebar: FC<ConfigModalSidebarProps> = ({
|
||||
@@ -101,12 +116,113 @@ const ConfigModalSidebar: FC<ConfigModalSidebarProps> = ({
|
||||
onCollapseChange,
|
||||
onCrossListDrop,
|
||||
itemTitles,
|
||||
formValuesVersion,
|
||||
}) => {
|
||||
const getTitle = useCallback(
|
||||
(id: string) => itemTitles?.[id] ?? getItemTitle(id),
|
||||
[itemTitles, getItemTitle],
|
||||
);
|
||||
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, {
|
||||
activationConstraint: { distance: 10 },
|
||||
}),
|
||||
useSensor(KeyboardSensor),
|
||||
);
|
||||
|
||||
const handleDragStart = useCallback(() => {
|
||||
setIsDragging(true);
|
||||
}, []);
|
||||
|
||||
const handleDragEnd = useCallback(
|
||||
(event: DragEndEvent) => {
|
||||
setIsDragging(false);
|
||||
const { active, over } = event;
|
||||
|
||||
if (!over || active.id === over.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const activeFilterIndex = filterOrderedIds.findIndex(
|
||||
id => id === active.id,
|
||||
);
|
||||
const activeCustomizationIndex = customizationOrderedIds.findIndex(
|
||||
id => id === active.id,
|
||||
);
|
||||
const overFilterIndex = filterOrderedIds.findIndex(id => id === over.id);
|
||||
const overCustomizationIndex = customizationOrderedIds.findIndex(
|
||||
id => id === over.id,
|
||||
);
|
||||
|
||||
const activeData = active.data.current;
|
||||
|
||||
if (
|
||||
activeFilterIndex === -1 &&
|
||||
activeCustomizationIndex === -1 &&
|
||||
activeData
|
||||
) {
|
||||
if (
|
||||
activeData.isDivider &&
|
||||
activeData.dragType &&
|
||||
onCrossListDrop &&
|
||||
(overFilterIndex !== -1 || overCustomizationIndex !== -1)
|
||||
) {
|
||||
const sourceType: 'filter' | 'customization' =
|
||||
activeData.dragType === FILTER_TYPE ? 'filter' : 'customization';
|
||||
const targetType: 'filter' | 'customization' =
|
||||
overFilterIndex !== -1 ? 'filter' : 'customization';
|
||||
const targetIndex =
|
||||
overFilterIndex !== -1 ? overFilterIndex : overCustomizationIndex;
|
||||
onCrossListDrop(
|
||||
activeData.filterIds[0],
|
||||
targetIndex,
|
||||
sourceType,
|
||||
targetType,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
onCrossListDrop &&
|
||||
typeof active.id === 'string' &&
|
||||
isDivider(active.id) &&
|
||||
((activeFilterIndex !== -1 && overCustomizationIndex !== -1) ||
|
||||
(activeCustomizationIndex !== -1 && overFilterIndex !== -1))
|
||||
) {
|
||||
const sourceType: 'filter' | 'customization' =
|
||||
activeFilterIndex !== -1 ? 'filter' : 'customization';
|
||||
const targetType: 'filter' | 'customization' =
|
||||
sourceType === 'filter' ? 'customization' : 'filter';
|
||||
const targetIndex =
|
||||
targetType === 'filter' ? overFilterIndex : overCustomizationIndex;
|
||||
|
||||
if (targetIndex !== -1) {
|
||||
onCrossListDrop(active.id, targetIndex, sourceType, targetType);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeFilterIndex !== -1 && overFilterIndex !== -1) {
|
||||
const itemId = filterOrderedIds[activeFilterIndex];
|
||||
onRearrange(activeFilterIndex, overFilterIndex, itemId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeCustomizationIndex !== -1 && overCustomizationIndex !== -1) {
|
||||
const itemId = customizationOrderedIds[activeCustomizationIndex];
|
||||
onRearrange(activeCustomizationIndex, overCustomizationIndex, itemId);
|
||||
}
|
||||
},
|
||||
[filterOrderedIds, customizationOrderedIds, onRearrange, onCrossListDrop],
|
||||
);
|
||||
|
||||
const handleDragCancel = useCallback(() => {
|
||||
setIsDragging(false);
|
||||
}, []);
|
||||
|
||||
const handleFilterCrossListDrop = (
|
||||
sourceId: string,
|
||||
targetIndex: number,
|
||||
@@ -139,60 +255,70 @@ const ConfigModalSidebar: FC<ConfigModalSidebarProps> = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledSidebarFlex vertical>
|
||||
<StyledHeaderFlex align="center">
|
||||
<NewItemDropdown
|
||||
onAddFilter={onAddFilter}
|
||||
onAddCustomization={onAddCustomization}
|
||||
/>
|
||||
</StyledHeaderFlex>
|
||||
<StyledCollapse
|
||||
activeKey={activeCollapseKeys}
|
||||
onChange={keys => onCollapseChange(keys as string[])}
|
||||
ghost
|
||||
>
|
||||
<StyledCollapse.Panel key="filters" header={filtersHeader}>
|
||||
<ItemSectionContent
|
||||
currentItemId={currentItemId}
|
||||
items={filterOrderedIds}
|
||||
removedItems={filterRemovedItems}
|
||||
erroredItems={filterErroredItems}
|
||||
getItemTitle={getTitle}
|
||||
onChange={onChange}
|
||||
onRearrange={onRearrange}
|
||||
onRemove={onRemove}
|
||||
restoreItem={restoreItem}
|
||||
dataTestId="filter-title-container"
|
||||
deleteAltText={t('Remove filter')}
|
||||
dragType={FILTER_TYPE}
|
||||
isCurrentSection={isFilterId(currentItemId)}
|
||||
onCrossListDrop={handleFilterCrossListDrop}
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleDragEnd}
|
||||
onDragCancel={handleDragCancel}
|
||||
>
|
||||
<StyledSidebarFlex vertical>
|
||||
<StyledHeaderFlex align="center">
|
||||
<NewItemDropdown
|
||||
onAddFilter={onAddFilter}
|
||||
onAddCustomization={onAddCustomization}
|
||||
/>
|
||||
</StyledCollapse.Panel>
|
||||
|
||||
<StyledCollapse.Panel
|
||||
key="chartCustomizations"
|
||||
header={customizationsHeader}
|
||||
</StyledHeaderFlex>
|
||||
<StyledCollapse
|
||||
key={formValuesVersion}
|
||||
activeKey={activeCollapseKeys}
|
||||
onChange={keys => onCollapseChange(keys as string[])}
|
||||
ghost
|
||||
isDragging={isDragging}
|
||||
>
|
||||
<ItemSectionContent
|
||||
currentItemId={currentItemId}
|
||||
items={customizationOrderedIds}
|
||||
removedItems={customizationRemovedItems}
|
||||
erroredItems={customizationErroredItems}
|
||||
getItemTitle={getTitle}
|
||||
onChange={onChange}
|
||||
onRearrange={onRearrange}
|
||||
onRemove={onRemove}
|
||||
restoreItem={restoreItem}
|
||||
dataTestId="customization-title-container"
|
||||
deleteAltText={t('Remove customization')}
|
||||
dragType={CUSTOMIZATION_TYPE}
|
||||
isCurrentSection={isChartCustomizationId(currentItemId)}
|
||||
onCrossListDrop={handleCustomizationCrossListDrop}
|
||||
/>
|
||||
</StyledCollapse.Panel>
|
||||
</StyledCollapse>
|
||||
</StyledSidebarFlex>
|
||||
<StyledCollapse.Panel key="filters" header={filtersHeader}>
|
||||
<ItemSectionContent
|
||||
currentItemId={currentItemId}
|
||||
items={filterOrderedIds}
|
||||
removedItems={filterRemovedItems}
|
||||
erroredItems={filterErroredItems}
|
||||
getItemTitle={getTitle}
|
||||
onChange={onChange}
|
||||
onRearrange={onRearrange}
|
||||
onRemove={onRemove}
|
||||
restoreItem={restoreItem}
|
||||
dataTestId="filter-title-container"
|
||||
deleteAltText={t('Remove filter')}
|
||||
dragType={FILTER_TYPE}
|
||||
isCurrentSection={isFilterId(currentItemId)}
|
||||
onCrossListDrop={handleFilterCrossListDrop}
|
||||
/>
|
||||
</StyledCollapse.Panel>
|
||||
|
||||
<StyledCollapse.Panel
|
||||
key="chartCustomizations"
|
||||
header={customizationsHeader}
|
||||
>
|
||||
<ItemSectionContent
|
||||
currentItemId={currentItemId}
|
||||
items={customizationOrderedIds}
|
||||
removedItems={customizationRemovedItems}
|
||||
erroredItems={customizationErroredItems}
|
||||
getItemTitle={getTitle}
|
||||
onChange={onChange}
|
||||
onRearrange={onRearrange}
|
||||
onRemove={onRemove}
|
||||
restoreItem={restoreItem}
|
||||
dataTestId="customization-title-container"
|
||||
deleteAltText={t('Remove customization')}
|
||||
dragType={CUSTOMIZATION_TYPE}
|
||||
isCurrentSection={isChartCustomizationId(currentItemId)}
|
||||
onCrossListDrop={handleCustomizationCrossListDrop}
|
||||
/>
|
||||
</StyledCollapse.Panel>
|
||||
</StyledCollapse>
|
||||
</StyledSidebarFlex>
|
||||
</DndContext>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -17,213 +17,230 @@
|
||||
* under the License.
|
||||
*/
|
||||
import { render } from 'spec/helpers/testing-library';
|
||||
import { DndProvider } from 'react-dnd';
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
import {
|
||||
DndContext,
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
closestCenter,
|
||||
} from '@dnd-kit/core';
|
||||
import {
|
||||
verticalListSortingStrategy,
|
||||
SortableContext,
|
||||
} from '@dnd-kit/sortable';
|
||||
import {
|
||||
DraggableFilter,
|
||||
FILTER_TYPE,
|
||||
CUSTOMIZATION_TYPE,
|
||||
} from './DraggableFilter';
|
||||
|
||||
const renderWithDnd = (component: React.ReactElement) =>
|
||||
render(<DndProvider backend={HTML5Backend}>{component}</DndProvider>);
|
||||
const DndWrapper: React.FC<{
|
||||
children: React.ReactElement;
|
||||
items: string[];
|
||||
}> = ({ children, items }) => {
|
||||
const sensor = useSensor(PointerSensor, {
|
||||
activationConstraint: { distance: 10 },
|
||||
});
|
||||
|
||||
return (
|
||||
<DndContext sensors={[sensor]} collisionDetection={closestCenter}>
|
||||
<SortableContext items={items} strategy={verticalListSortingStrategy}>
|
||||
{children}
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
);
|
||||
};
|
||||
|
||||
const renderWithDnd = (component: React.ReactElement, items: string[] = []) =>
|
||||
render(<DndWrapper items={items}>{component}</DndWrapper>);
|
||||
|
||||
test('identifies divider items correctly', () => {
|
||||
const onRearrange = jest.fn();
|
||||
const filterIds = ['NATIVE_FILTER_DIVIDER-abc123'];
|
||||
|
||||
const { container } = renderWithDnd(
|
||||
<DraggableFilter
|
||||
id={filterIds[0]}
|
||||
index={0}
|
||||
filterIds={filterIds}
|
||||
onRearrange={onRearrange}
|
||||
dragType={FILTER_TYPE}
|
||||
>
|
||||
<div>Divider Content</div>
|
||||
</DraggableFilter>,
|
||||
filterIds,
|
||||
);
|
||||
|
||||
expect(container).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('identifies non-divider items correctly', () => {
|
||||
const onRearrange = jest.fn();
|
||||
const filterIds = ['NATIVE_FILTER-abc123'];
|
||||
|
||||
const { container } = renderWithDnd(
|
||||
<DraggableFilter
|
||||
id={filterIds[0]}
|
||||
index={0}
|
||||
filterIds={filterIds}
|
||||
onRearrange={onRearrange}
|
||||
dragType={FILTER_TYPE}
|
||||
>
|
||||
<div>Filter Content</div>
|
||||
</DraggableFilter>,
|
||||
filterIds,
|
||||
);
|
||||
|
||||
expect(container).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('calls onCrossListDrop when divider is dropped cross-list from filter to customization', () => {
|
||||
const onRearrange = jest.fn();
|
||||
const onCrossListDrop = jest.fn();
|
||||
test('renders divider item for cross-list drop target', () => {
|
||||
const filterIds = ['NATIVE_FILTER_DIVIDER-abc123'];
|
||||
|
||||
renderWithDnd(
|
||||
const { container } = renderWithDnd(
|
||||
<DraggableFilter
|
||||
id={filterIds[0]}
|
||||
index={0}
|
||||
filterIds={filterIds}
|
||||
onRearrange={onRearrange}
|
||||
onCrossListDrop={onCrossListDrop}
|
||||
dragType={CUSTOMIZATION_TYPE}
|
||||
>
|
||||
<div>Drop Target</div>
|
||||
</DraggableFilter>,
|
||||
filterIds,
|
||||
);
|
||||
|
||||
expect(onCrossListDrop).not.toHaveBeenCalled();
|
||||
expect(container).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('calls onCrossListDrop when divider is dropped cross-list from customization to filter', () => {
|
||||
const onRearrange = jest.fn();
|
||||
const onCrossListDrop = jest.fn();
|
||||
test('renders customization divider for cross-list drop target', () => {
|
||||
const filterIds = ['CHART_CUSTOMIZATION_DIVIDER-xyz789'];
|
||||
|
||||
renderWithDnd(
|
||||
const { container } = renderWithDnd(
|
||||
<DraggableFilter
|
||||
id={filterIds[0]}
|
||||
index={0}
|
||||
filterIds={filterIds}
|
||||
onRearrange={onRearrange}
|
||||
onCrossListDrop={onCrossListDrop}
|
||||
dragType={FILTER_TYPE}
|
||||
>
|
||||
<div>Drop Target</div>
|
||||
</DraggableFilter>,
|
||||
filterIds,
|
||||
);
|
||||
|
||||
expect(onCrossListDrop).not.toHaveBeenCalled();
|
||||
expect(container).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('calls onRearrange for same-list drops', () => {
|
||||
const onRearrange = jest.fn();
|
||||
test('renders filter item for same-list drops', () => {
|
||||
const filterIds = ['NATIVE_FILTER-abc123'];
|
||||
|
||||
renderWithDnd(
|
||||
const { container } = renderWithDnd(
|
||||
<DraggableFilter
|
||||
id={filterIds[0]}
|
||||
index={0}
|
||||
filterIds={filterIds}
|
||||
onRearrange={onRearrange}
|
||||
dragType={FILTER_TYPE}
|
||||
>
|
||||
<div>Filter Content</div>
|
||||
</DraggableFilter>,
|
||||
filterIds,
|
||||
);
|
||||
|
||||
expect(onRearrange).not.toHaveBeenCalled();
|
||||
expect(container).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('does not call onCrossListDrop when non-divider is dropped cross-list', () => {
|
||||
const onRearrange = jest.fn();
|
||||
const onCrossListDrop = jest.fn();
|
||||
test('renders non-divider item for cross-list drop target', () => {
|
||||
const filterIds = ['NATIVE_FILTER-abc123'];
|
||||
|
||||
renderWithDnd(
|
||||
const { container } = renderWithDnd(
|
||||
<DraggableFilter
|
||||
id={filterIds[0]}
|
||||
index={0}
|
||||
filterIds={filterIds}
|
||||
onRearrange={onRearrange}
|
||||
onCrossListDrop={onCrossListDrop}
|
||||
dragType={CUSTOMIZATION_TYPE}
|
||||
>
|
||||
<div>Drop Target</div>
|
||||
</DraggableFilter>,
|
||||
filterIds,
|
||||
);
|
||||
|
||||
expect(onCrossListDrop).not.toHaveBeenCalled();
|
||||
expect(container).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('renders children correctly', () => {
|
||||
const onRearrange = jest.fn();
|
||||
const filterIds = ['NATIVE_FILTER-abc123'];
|
||||
|
||||
const { getByText } = renderWithDnd(
|
||||
<DraggableFilter
|
||||
id={filterIds[0]}
|
||||
index={0}
|
||||
filterIds={filterIds}
|
||||
onRearrange={onRearrange}
|
||||
dragType={FILTER_TYPE}
|
||||
>
|
||||
<div>Test Content</div>
|
||||
</DraggableFilter>,
|
||||
filterIds,
|
||||
);
|
||||
|
||||
expect(getByText('Test Content')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('accepts both FILTER_TYPE and CUSTOMIZATION_TYPE drops', () => {
|
||||
const onRearrange = jest.fn();
|
||||
const filterIds = ['NATIVE_FILTER-abc123'];
|
||||
|
||||
const { container } = renderWithDnd(
|
||||
<DraggableFilter
|
||||
id={filterIds[0]}
|
||||
index={0}
|
||||
filterIds={filterIds}
|
||||
onRearrange={onRearrange}
|
||||
dragType={FILTER_TYPE}
|
||||
>
|
||||
<div>Drop Zone</div>
|
||||
</DraggableFilter>,
|
||||
filterIds,
|
||||
);
|
||||
|
||||
expect(container).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('uses FILTER_TYPE as default dragType', () => {
|
||||
const onRearrange = jest.fn();
|
||||
const filterIds = ['NATIVE_FILTER-abc123'];
|
||||
|
||||
const { container } = renderWithDnd(
|
||||
<DraggableFilter index={0} filterIds={filterIds} onRearrange={onRearrange}>
|
||||
<DraggableFilter id={filterIds[0]} index={0} filterIds={filterIds}>
|
||||
<div>Default Type</div>
|
||||
</DraggableFilter>,
|
||||
filterIds,
|
||||
);
|
||||
|
||||
expect(container).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('detects cross-list drop correctly', () => {
|
||||
const onRearrange = jest.fn();
|
||||
const onCrossListDrop = jest.fn();
|
||||
const filterIds = ['NATIVE_FILTER_DIVIDER-abc123'];
|
||||
|
||||
const { container } = renderWithDnd(
|
||||
<DraggableFilter
|
||||
id={filterIds[0]}
|
||||
index={0}
|
||||
filterIds={filterIds}
|
||||
onRearrange={onRearrange}
|
||||
onCrossListDrop={onCrossListDrop}
|
||||
dragType={CUSTOMIZATION_TYPE}
|
||||
>
|
||||
<div>Cross List Target</div>
|
||||
</DraggableFilter>,
|
||||
filterIds,
|
||||
);
|
||||
|
||||
expect(container).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('identifies chart customization divider with underscore prefix', () => {
|
||||
const onRearrange = jest.fn();
|
||||
const filterIds = ['CHART_CUSTOMIZATION_DIVIDER-abc123'];
|
||||
|
||||
const { container } = renderWithDnd(
|
||||
<DraggableFilter
|
||||
id={filterIds[0]}
|
||||
index={0}
|
||||
filterIds={filterIds}
|
||||
onRearrange={onRearrange}
|
||||
dragType={CUSTOMIZATION_TYPE}
|
||||
>
|
||||
<div>Customization Divider</div>
|
||||
</DraggableFilter>,
|
||||
filterIds,
|
||||
);
|
||||
|
||||
expect(container).toBeInTheDocument();
|
||||
|
||||
@@ -18,14 +18,9 @@
|
||||
*/
|
||||
import { t } from '@apache-superset/core';
|
||||
import { styled } from '@apache-superset/core/ui';
|
||||
import { useRef, FC } from 'react';
|
||||
import {
|
||||
DragSourceMonitor,
|
||||
DropTargetMonitor,
|
||||
useDrag,
|
||||
useDrop,
|
||||
XYCoord,
|
||||
} from 'react-dnd';
|
||||
import type { CSSProperties, FC, ReactNode } from 'react';
|
||||
import { useSortable } from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import { Icons } from '@superset-ui/core/components/Icons';
|
||||
import type { IconType } from '@superset-ui/core/components/Icons/types';
|
||||
import { isDivider } from './utils';
|
||||
@@ -57,111 +52,56 @@ const DragIcon = styled(Icons.Drag, {
|
||||
`;
|
||||
|
||||
interface FilterTabTitleProps {
|
||||
id: string;
|
||||
index: number;
|
||||
filterIds: string[];
|
||||
onRearrange: (
|
||||
dragItemIndex: number,
|
||||
targetIndex: number,
|
||||
itemId: string,
|
||||
) => void;
|
||||
onCrossListDrop?: (
|
||||
sourceId: string,
|
||||
targetIndex: number,
|
||||
sourceType: 'filter' | 'customization',
|
||||
) => void;
|
||||
dragType?: string;
|
||||
}
|
||||
|
||||
interface DragItem {
|
||||
index: number;
|
||||
filterIds: string[];
|
||||
type: string;
|
||||
isDivider: boolean;
|
||||
dragType: string;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const DraggableFilter: FC<FilterTabTitleProps> = ({
|
||||
id,
|
||||
index,
|
||||
onRearrange,
|
||||
onCrossListDrop,
|
||||
filterIds,
|
||||
dragType = FILTER_TYPE,
|
||||
children,
|
||||
}) => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const itemId = filterIds[0];
|
||||
const isDividerItem = isDivider(itemId);
|
||||
|
||||
const [{ isDragging }, drag] = useDrag({
|
||||
item: {
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({
|
||||
id,
|
||||
data: {
|
||||
filterIds,
|
||||
type: dragType,
|
||||
index,
|
||||
isDivider: isDividerItem,
|
||||
dragType,
|
||||
},
|
||||
collect: (monitor: DragSourceMonitor) => ({
|
||||
isDragging: monitor.isDragging(),
|
||||
}),
|
||||
});
|
||||
|
||||
const [, drop] = useDrop({
|
||||
accept: [FILTER_TYPE, CUSTOMIZATION_TYPE],
|
||||
drop: (item: DragItem) => {
|
||||
const isCrossListDrop = item.dragType !== dragType;
|
||||
const style: CSSProperties = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition: transition || undefined,
|
||||
};
|
||||
|
||||
if (isCrossListDrop && item.isDivider && onCrossListDrop) {
|
||||
const sourceType: 'filter' | 'customization' =
|
||||
item.dragType === FILTER_TYPE ? 'filter' : 'customization';
|
||||
onCrossListDrop(item.filterIds[0], index, sourceType);
|
||||
}
|
||||
},
|
||||
hover: (item: DragItem, monitor: DropTargetMonitor) => {
|
||||
if (!ref.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dragIndex = item.index;
|
||||
const hoverIndex = index;
|
||||
|
||||
if (dragIndex === hoverIndex && item.dragType === dragType) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hoverBoundingRect = ref.current?.getBoundingClientRect();
|
||||
const hoverMiddleY =
|
||||
(hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
|
||||
const clientOffset = monitor.getClientOffset();
|
||||
const hoverClientY = (clientOffset as XYCoord).y - hoverBoundingRect.top;
|
||||
|
||||
const isCrossListDrop = item.dragType !== dragType;
|
||||
|
||||
if (isCrossListDrop) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
|
||||
return;
|
||||
}
|
||||
if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {
|
||||
return;
|
||||
}
|
||||
|
||||
onRearrange(dragIndex, hoverIndex, item.filterIds[0]);
|
||||
item.index = hoverIndex;
|
||||
},
|
||||
});
|
||||
drag(drop(ref));
|
||||
return (
|
||||
<Container ref={ref} isDragging={isDragging}>
|
||||
<DragIcon
|
||||
isDragging={isDragging}
|
||||
alt={t('Move')}
|
||||
className="dragIcon"
|
||||
viewBox="4 4 16 16"
|
||||
/>
|
||||
<div css={{ flex: 1 }}>{children}</div>
|
||||
</Container>
|
||||
<div ref={setNodeRef} style={style}>
|
||||
<Container isDragging={isDragging} {...attributes} {...listeners}>
|
||||
<DragIcon
|
||||
isDragging={isDragging}
|
||||
alt={t('Move icon')}
|
||||
viewBox="4 4 16 16"
|
||||
/>
|
||||
<div css={{ flex: 1 }}>{children}</div>
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
import { dashboardLayout } from 'spec/fixtures/mockDashboardLayout';
|
||||
import { buildNativeFilter } from 'spec/fixtures/mockNativeFilters';
|
||||
import {
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
userEvent,
|
||||
@@ -67,22 +66,17 @@ beforeEach(() => {
|
||||
scrollMock.mockClear();
|
||||
});
|
||||
|
||||
test('drag and drop', async () => {
|
||||
test('drag and drop', () => {
|
||||
defaultRender();
|
||||
// Drag the state and country filter above the product filter
|
||||
const [countryStateFilter, productFilter] = document.querySelectorAll(
|
||||
'div[draggable=true]',
|
||||
);
|
||||
// const productFilter = await screen.findByText('NATIVE_FILTER-3');
|
||||
await waitFor(() => {
|
||||
fireEvent.dragStart(productFilter);
|
||||
fireEvent.dragEnter(countryStateFilter);
|
||||
fireEvent.dragOver(countryStateFilter);
|
||||
fireEvent.drop(countryStateFilter);
|
||||
fireEvent.dragLeave(countryStateFilter);
|
||||
fireEvent.dragEnd(productFilter);
|
||||
});
|
||||
expect(defaultProps.onRearrange).toHaveBeenCalledTimes(1);
|
||||
const dragIcons = document.querySelectorAll('[alt="Move icon"]');
|
||||
expect(dragIcons.length).toBe(3);
|
||||
|
||||
expect(screen.getByText('NATIVE_FILTER-1')).toBeInTheDocument();
|
||||
expect(screen.getByText('NATIVE_FILTER-2')).toBeInTheDocument();
|
||||
expect(screen.getByText('NATIVE_FILTER-3')).toBeInTheDocument();
|
||||
|
||||
const filterContainer = screen.getByTestId('filter-title-container');
|
||||
expect(filterContainer).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('remove filter', async () => {
|
||||
|
||||
@@ -16,11 +16,22 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { forwardRef, ReactNode } from 'react';
|
||||
import { forwardRef, useCallback, useState } from 'react';
|
||||
|
||||
import { t } from '@apache-superset/core';
|
||||
import { styled } from '@apache-superset/core/ui';
|
||||
import { Icons } from '@superset-ui/core/components/Icons';
|
||||
import type { DragEndEvent } from '@dnd-kit/core';
|
||||
import {
|
||||
DndContext,
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
closestCenter,
|
||||
} from '@dnd-kit/core';
|
||||
import {
|
||||
verticalListSortingStrategy,
|
||||
SortableContext,
|
||||
} from '@dnd-kit/sortable';
|
||||
import { FilterRemoval } from './types';
|
||||
import DraggableFilter from './DraggableFilter';
|
||||
|
||||
@@ -68,9 +79,14 @@ const StyledWarning = styled(Icons.ExclamationCircleOutlined)`
|
||||
}
|
||||
`;
|
||||
|
||||
const Container = styled.div`
|
||||
const Container = styled.div<{ isDragging: boolean }>`
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
${({ isDragging }) =>
|
||||
isDragging &&
|
||||
`
|
||||
overflow: hidden;
|
||||
`}
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
@@ -102,6 +118,39 @@ const FilterTitleContainer = forwardRef<HTMLDivElement, Props>(
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
|
||||
const sensor = useSensor(PointerSensor, {
|
||||
activationConstraint: { distance: 10 },
|
||||
});
|
||||
|
||||
const handleDragStart = useCallback(() => {
|
||||
setIsDragging(true);
|
||||
}, []);
|
||||
|
||||
const handleDragEnd = useCallback(
|
||||
(event: DragEndEvent) => {
|
||||
setIsDragging(false);
|
||||
const { active, over } = event;
|
||||
|
||||
if (!over || active.id === over.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const activeIndex = filters.findIndex(filter => filter === active.id);
|
||||
const overIndex = filters.findIndex(filter => filter === over.id);
|
||||
|
||||
if (activeIndex !== -1 && overIndex !== -1) {
|
||||
onRearrange(activeIndex, overIndex);
|
||||
}
|
||||
},
|
||||
[filters, onRearrange],
|
||||
);
|
||||
|
||||
const handleDragCancel = useCallback(() => {
|
||||
setIsDragging(false);
|
||||
}, []);
|
||||
|
||||
const renderComponent = (id: string) => {
|
||||
const isRemoved = !!removedFilters[id];
|
||||
const isErrored = erroredFilters.includes(id);
|
||||
@@ -169,29 +218,40 @@ const FilterTitleContainer = forwardRef<HTMLDivElement, Props>(
|
||||
);
|
||||
};
|
||||
|
||||
const renderFilterGroups = () => {
|
||||
const items: ReactNode[] = [];
|
||||
filters.forEach((item, index) => {
|
||||
items.push(
|
||||
<DraggableFilter
|
||||
key={index}
|
||||
onRearrange={onRearrange}
|
||||
index={index}
|
||||
filterIds={[item]}
|
||||
>
|
||||
{renderComponent(item)}
|
||||
</DraggableFilter>,
|
||||
);
|
||||
});
|
||||
return items;
|
||||
};
|
||||
|
||||
return (
|
||||
<Container data-test="filter-title-container" ref={ref}>
|
||||
{renderFilterGroups()}
|
||||
<Container
|
||||
data-test="filter-title-container"
|
||||
ref={ref}
|
||||
isDragging={isDragging}
|
||||
>
|
||||
<DndContext
|
||||
sensors={[sensor]}
|
||||
collisionDetection={closestCenter}
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleDragEnd}
|
||||
onDragCancel={handleDragCancel}
|
||||
>
|
||||
<SortableContext
|
||||
items={filters}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
{filters.map((item, index) => (
|
||||
<DraggableFilter
|
||||
key={item}
|
||||
id={item}
|
||||
index={index}
|
||||
filterIds={[item]}
|
||||
>
|
||||
{renderComponent(item)}
|
||||
</DraggableFilter>
|
||||
))}
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
</Container>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
FilterTitleContainer.displayName = 'FilterTitleContainer';
|
||||
|
||||
export default FilterTitleContainer;
|
||||
|
||||
@@ -223,6 +223,10 @@ function queryCheckbox(name: RegExp) {
|
||||
return screen.queryByRole('checkbox', { name });
|
||||
}
|
||||
|
||||
function sleep(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
test('renders a value filter type', () => {
|
||||
defaultRender();
|
||||
|
||||
@@ -524,7 +528,10 @@ test('deletes a filter including dependencies', async () => {
|
||||
);
|
||||
}, 30000);
|
||||
|
||||
test('reorders filters via drag and drop', async () => {
|
||||
const SORTABLE_ITEM_HEIGHT = 40;
|
||||
const SORTABLE_ITEM_WIDTH = 200;
|
||||
|
||||
test('reorders filters via keyboard (Space, ArrowDown, Space)', async () => {
|
||||
const nativeFilterConfig = [
|
||||
buildNativeFilter('NATIVE_FILTER-1', 'state', []),
|
||||
buildNativeFilter('NATIVE_FILTER-2', 'country', []),
|
||||
@@ -543,92 +550,109 @@ test('reorders filters via drag and drop', async () => {
|
||||
|
||||
const onSave = jest.fn();
|
||||
|
||||
defaultRender(state, {
|
||||
...props,
|
||||
createNewOnOpen: false,
|
||||
onSave,
|
||||
});
|
||||
|
||||
const filterContainer = screen.getByTestId('filter-title-container');
|
||||
const draggableFilters = within(filterContainer).getAllByRole('tab');
|
||||
|
||||
fireEvent.dragStart(draggableFilters[0]);
|
||||
fireEvent.dragOver(draggableFilters[2]);
|
||||
fireEvent.drop(draggableFilters[2]);
|
||||
fireEvent.dragEnd(draggableFilters[0]);
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: SAVE_REGEX }));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(onSave).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
filterChanges: expect.objectContaining({
|
||||
deleted: [],
|
||||
modified: [],
|
||||
reordered: expect.arrayContaining([
|
||||
'NATIVE_FILTER-2',
|
||||
'NATIVE_FILTER-3',
|
||||
'NATIVE_FILTER-1',
|
||||
]),
|
||||
}),
|
||||
}),
|
||||
),
|
||||
const originalOffsetHeight = Object.getOwnPropertyDescriptor(
|
||||
HTMLElement.prototype,
|
||||
'offsetHeight',
|
||||
);
|
||||
const originalOffsetWidth = Object.getOwnPropertyDescriptor(
|
||||
HTMLElement.prototype,
|
||||
'offsetWidth',
|
||||
);
|
||||
});
|
||||
|
||||
test('rearranges three filters and deletes one of them', async () => {
|
||||
const nativeFilterConfig = [
|
||||
buildNativeFilter('NATIVE_FILTER-1', 'state', []),
|
||||
buildNativeFilter('NATIVE_FILTER-2', 'country', []),
|
||||
buildNativeFilter('NATIVE_FILTER-3', 'product', []),
|
||||
];
|
||||
|
||||
const state = {
|
||||
...defaultState(),
|
||||
dashboardInfo: {
|
||||
metadata: {
|
||||
native_filter_configuration: nativeFilterConfig,
|
||||
},
|
||||
Object.defineProperty(HTMLElement.prototype, 'offsetHeight', {
|
||||
configurable: true,
|
||||
get() {
|
||||
return SORTABLE_ITEM_HEIGHT;
|
||||
},
|
||||
});
|
||||
Object.defineProperty(HTMLElement.prototype, 'offsetWidth', {
|
||||
configurable: true,
|
||||
get() {
|
||||
return SORTABLE_ITEM_WIDTH;
|
||||
},
|
||||
dashboardLayout,
|
||||
};
|
||||
|
||||
const onSave = jest.fn();
|
||||
|
||||
defaultRender(state, {
|
||||
...props,
|
||||
createNewOnOpen: false,
|
||||
onSave,
|
||||
});
|
||||
|
||||
const filterContainer = screen.getByTestId('filter-title-container');
|
||||
const draggableFilters = within(filterContainer).getAllByRole('tab');
|
||||
const deleteIcon = draggableFilters[1].querySelector('[data-icon="delete"]');
|
||||
fireEvent.click(deleteIcon!);
|
||||
try {
|
||||
defaultRender(state, {
|
||||
...props,
|
||||
createNewOnOpen: false,
|
||||
onSave,
|
||||
});
|
||||
|
||||
fireEvent.dragStart(draggableFilters[0]);
|
||||
fireEvent.dragOver(draggableFilters[2]);
|
||||
fireEvent.drop(draggableFilters[2]);
|
||||
fireEvent.dragEnd(draggableFilters[0]);
|
||||
const filterContainer = screen.getByTestId('filter-title-container');
|
||||
const sortableElements = filterContainer.querySelectorAll(
|
||||
'[aria-roledescription="sortable"]',
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: SAVE_REGEX }));
|
||||
sortableElements.forEach((el, index) => {
|
||||
const sortableNode = el.parentElement;
|
||||
if (sortableNode) {
|
||||
jest.spyOn(sortableNode, 'getBoundingClientRect').mockImplementation(
|
||||
() =>
|
||||
({
|
||||
bottom: (index + 1) * SORTABLE_ITEM_HEIGHT,
|
||||
height: SORTABLE_ITEM_HEIGHT,
|
||||
left: 0,
|
||||
right: SORTABLE_ITEM_WIDTH,
|
||||
top: index * SORTABLE_ITEM_HEIGHT,
|
||||
width: SORTABLE_ITEM_WIDTH,
|
||||
x: 0,
|
||||
y: index * SORTABLE_ITEM_HEIGHT,
|
||||
toJSON: () => ({}),
|
||||
}) as DOMRect,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
await waitFor(() =>
|
||||
expect(onSave).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
filterChanges: expect.objectContaining({
|
||||
modified: [],
|
||||
deleted: ['NATIVE_FILTER-2'],
|
||||
reordered: expect.arrayContaining([
|
||||
'NATIVE_FILTER-2',
|
||||
'NATIVE_FILTER-3',
|
||||
'NATIVE_FILTER-1',
|
||||
]),
|
||||
}),
|
||||
}),
|
||||
),
|
||||
);
|
||||
});
|
||||
const firstSortable = sortableElements[0] as HTMLElement;
|
||||
firstSortable.focus();
|
||||
|
||||
fireEvent.keyDown(firstSortable, { code: 'Space' });
|
||||
await sleep(1);
|
||||
fireEvent.keyDown(document.activeElement ?? firstSortable, {
|
||||
code: 'ArrowDown',
|
||||
});
|
||||
await sleep(1);
|
||||
fireEvent.keyDown(document.activeElement ?? firstSortable, {
|
||||
code: 'Space',
|
||||
});
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: SAVE_REGEX }));
|
||||
|
||||
await waitFor(
|
||||
() =>
|
||||
expect(onSave).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
filterChanges: expect.objectContaining({
|
||||
deleted: [],
|
||||
modified: [],
|
||||
reordered: [
|
||||
'NATIVE_FILTER-2',
|
||||
'NATIVE_FILTER-1',
|
||||
'NATIVE_FILTER-3',
|
||||
],
|
||||
}),
|
||||
}),
|
||||
),
|
||||
{ timeout: 5000 },
|
||||
);
|
||||
} finally {
|
||||
if (originalOffsetHeight) {
|
||||
Object.defineProperty(
|
||||
HTMLElement.prototype,
|
||||
'offsetHeight',
|
||||
originalOffsetHeight,
|
||||
);
|
||||
}
|
||||
if (originalOffsetWidth) {
|
||||
Object.defineProperty(
|
||||
HTMLElement.prototype,
|
||||
'offsetWidth',
|
||||
originalOffsetWidth,
|
||||
);
|
||||
}
|
||||
}
|
||||
}, 30000);
|
||||
|
||||
test('updates sidebar title when filter name changes', async () => {
|
||||
const nativeFilterConfig = [
|
||||
|
||||
@@ -460,19 +460,20 @@ function FiltersConfigModal({
|
||||
return titles;
|
||||
}, [filterIds, chartCustomizationIds, modalSaveLogic, formValuesVersion]);
|
||||
|
||||
const debouncedErrorHandling = useMemo(
|
||||
const debouncedHandleErroredItems = useMemo(
|
||||
() =>
|
||||
debounce(() => {
|
||||
setSaveAlertVisible(false);
|
||||
modalSaveLogic.handleErroredItems();
|
||||
setFormValuesVersion(prev => prev + 1);
|
||||
}, Constants.SLOW_DEBOUNCE),
|
||||
[modalSaveLogic],
|
||||
[modalSaveLogic, setSaveAlertVisible],
|
||||
);
|
||||
|
||||
const handleValuesChange = useCallback(() => {
|
||||
setFormValuesVersion(prev => prev + 1);
|
||||
debouncedErrorHandling();
|
||||
}, [debouncedErrorHandling]);
|
||||
const handleValuesChange = useMemo(
|
||||
() => debouncedHandleErroredItems,
|
||||
[debouncedHandleErroredItems],
|
||||
);
|
||||
|
||||
const handleActiveFilterPanelChange = useCallback(
|
||||
(key: string | string[]) => setActiveFilterPanelKey(key),
|
||||
|
||||
@@ -16,11 +16,16 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { forwardRef, ReactNode } from 'react';
|
||||
import { forwardRef, useState } from 'react';
|
||||
|
||||
import { t } from '@apache-superset/core';
|
||||
import { styled } from '@apache-superset/core/ui';
|
||||
import { Icons } from '@superset-ui/core/components/Icons';
|
||||
import { useDndMonitor } from '@dnd-kit/core';
|
||||
import {
|
||||
verticalListSortingStrategy,
|
||||
SortableContext,
|
||||
} from '@dnd-kit/sortable';
|
||||
import { FilterRemoval } from './types';
|
||||
import DraggableFilter from './DraggableFilter';
|
||||
|
||||
@@ -58,9 +63,14 @@ const StyledWarning = styled(Icons.ExclamationCircleOutlined)`
|
||||
}
|
||||
`;
|
||||
|
||||
const Container = styled.div`
|
||||
const Container = styled.div<{ isDragging: boolean }>`
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
${({ isDragging }) =>
|
||||
isDragging &&
|
||||
`
|
||||
overflow: hidden;
|
||||
`}
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
@@ -90,7 +100,6 @@ const ItemTitleContainer = forwardRef<HTMLDivElement, Props>(
|
||||
onChange,
|
||||
onRemove,
|
||||
restoreItem,
|
||||
onRearrange,
|
||||
currentItemId,
|
||||
removedItems,
|
||||
items,
|
||||
@@ -98,10 +107,23 @@ const ItemTitleContainer = forwardRef<HTMLDivElement, Props>(
|
||||
dataTestId = 'item-title-container',
|
||||
deleteAltText = 'RemoveItem',
|
||||
dragType,
|
||||
onCrossListDrop,
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
|
||||
useDndMonitor({
|
||||
onDragStart: () => {
|
||||
setIsDragging(true);
|
||||
},
|
||||
onDragEnd: () => {
|
||||
setIsDragging(false);
|
||||
},
|
||||
onDragCancel: () => {
|
||||
setIsDragging(false);
|
||||
},
|
||||
});
|
||||
|
||||
const renderComponent = (id: string) => {
|
||||
const isRemoved = !!removedItems[id];
|
||||
const isErrored = erroredItems.includes(id);
|
||||
@@ -164,31 +186,26 @@ const ItemTitleContainer = forwardRef<HTMLDivElement, Props>(
|
||||
);
|
||||
};
|
||||
|
||||
const renderItemGroups = () => {
|
||||
const itemNodes: ReactNode[] = [];
|
||||
items.forEach((item, index) => {
|
||||
itemNodes.push(
|
||||
<DraggableFilter
|
||||
key={item}
|
||||
onRearrange={onRearrange}
|
||||
onCrossListDrop={onCrossListDrop}
|
||||
index={index}
|
||||
filterIds={[item]}
|
||||
dragType={dragType}
|
||||
>
|
||||
{renderComponent(item)}
|
||||
</DraggableFilter>,
|
||||
);
|
||||
});
|
||||
return itemNodes;
|
||||
};
|
||||
|
||||
return (
|
||||
<Container data-test={dataTestId} ref={ref}>
|
||||
{renderItemGroups()}
|
||||
<Container data-test={dataTestId} ref={ref} isDragging={isDragging}>
|
||||
<SortableContext items={items} strategy={verticalListSortingStrategy}>
|
||||
{items.map((item, index) => (
|
||||
<DraggableFilter
|
||||
key={item}
|
||||
id={item}
|
||||
index={index}
|
||||
filterIds={[item]}
|
||||
dragType={dragType}
|
||||
>
|
||||
{renderComponent(item)}
|
||||
</DraggableFilter>
|
||||
))}
|
||||
</SortableContext>
|
||||
</Container>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
ItemTitleContainer.displayName = 'ItemTitleContainer';
|
||||
|
||||
export default ItemTitleContainer;
|
||||
|
||||
Reference in New Issue
Block a user