diff --git a/e2e/test/scenarios/admin/datamodel/editor.cy.spec.js b/e2e/test/scenarios/admin/datamodel/editor.cy.spec.js index d701be876ecef4c773f184cb054a21dcb1792ba9..f0418cd3e493f310a1bd0aeac252a653124bd620 100644 --- a/e2e/test/scenarios/admin/datamodel/editor.cy.spec.js +++ b/e2e/test/scenarios/admin/datamodel/editor.cy.spec.js @@ -659,10 +659,25 @@ const getFieldSection = fieldName => { }; const moveField = (fieldIndex, deltaY) => { - cy.get(".Grabber").eq(fieldIndex).trigger("mousedown", 0, 0, { force: true }); - cy.get("#ColumnsList") - .trigger("mousemove", 10, deltaY) - .trigger("mouseup", 10, deltaY); + cy.get(".Grabber") + .eq(fieldIndex) + .trigger("pointerdown", 0, 0, { force: true, button: 0, isPrimary: true }) + .wait(200) + .trigger("pointermove", 5, 5, { force: true, button: 0, isPrimary: true }) + .wait(200) + //cy.get("#ColumnsList") + .trigger("pointermove", 10, deltaY, { + force: true, + button: 0, + isPrimary: true, + }) + .wait(200) + .trigger("pointerup", 10, deltaY, { + force: true, + button: 0, + isPrimary: true, + }) + .wait(200); }; const setTableOrder = order => { diff --git a/frontend/src/metabase/admin/datamodel/metadata/components/MetadataTableColumn/MetadataTableColumn.styled.tsx b/frontend/src/metabase/admin/datamodel/metadata/components/MetadataTableColumn/MetadataTableColumn.styled.tsx index 17efa813282b8f64de3fcceec6da4c2478266db6..4b287452a801618a202fb98a1020146d5527c39e 100644 --- a/frontend/src/metabase/admin/datamodel/metadata/components/MetadataTableColumn/MetadataTableColumn.styled.tsx +++ b/frontend/src/metabase/admin/datamodel/metadata/components/MetadataTableColumn/MetadataTableColumn.styled.tsx @@ -15,6 +15,7 @@ export const ColumnContainer = styled.section` &:last-child { margin-bottom: 0; } + background: ${color("white")}; `; export const ColumnInput = styled(InputBlurChange)` diff --git a/frontend/src/metabase/admin/datamodel/metadata/components/MetadataTableColumnList/MetadataTableColumnList.tsx b/frontend/src/metabase/admin/datamodel/metadata/components/MetadataTableColumnList/MetadataTableColumnList.tsx index 7df00689d7a0823ab0b31c367bac109a85374955..e0aabaf55130d510f04747355a7233e457b2ad99 100644 --- a/frontend/src/metabase/admin/datamodel/metadata/components/MetadataTableColumnList/MetadataTableColumnList.tsx +++ b/frontend/src/metabase/admin/datamodel/metadata/components/MetadataTableColumnList/MetadataTableColumnList.tsx @@ -1,5 +1,8 @@ +import type { UniqueIdentifier } from "@dnd-kit/core"; +import { useSensor, PointerSensor } from "@dnd-kit/core"; +import { useSortable } from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; import cx from "classnames"; -import type { ReactNode } from "react"; import { useCallback, useMemo } from "react"; import { connect } from "react-redux"; import { t } from "ttag"; @@ -7,12 +10,8 @@ import _ from "underscore"; import Grabber from "metabase/components/Grabber"; import TippyPopoverWithTrigger from "metabase/components/PopoverWithTrigger/TippyPopoverWithTrigger"; -import { - SortableContainer, - SortableElement, - SortableHandle, -} from "metabase/components/sortable"; import AccordionList from "metabase/core/components/AccordionList"; +import { SortableList } from "metabase/core/components/Sortable"; import Tables from "metabase/entities/tables"; import { Icon } from "metabase/ui"; import type Field from "metabase-lib/metadata/Field"; @@ -45,11 +44,6 @@ interface DispatchProps { onUpdateFieldOrder: (table: Table, fieldOrder: FieldId[]) => void; } -interface DragProps { - oldIndex: number; - newIndex: number; -} - type MetadataTableColumnListProps = OwnProps & DispatchProps; const mapDispatchToProps: DispatchProps = { @@ -57,6 +51,8 @@ const mapDispatchToProps: DispatchProps = { onUpdateFieldOrder: Tables.actions.setFieldOrder, }; +const getId = (field: Field) => field.getId(); + const MetadataTableColumnList = ({ table, idFields, @@ -67,23 +63,31 @@ const MetadataTableColumnList = ({ const { fields = [], visibility_type } = table; const isHidden = visibility_type != null; + const pointerSensor = useSensor(PointerSensor, { + activationConstraint: { distance: 0 }, + }); + const sortedFields = useMemo( () => _.sortBy(fields, field => field.position), [fields], ); - const handleSortStart = useCallback(() => { - document.body.classList.add("grabbing"); - }, []); - const handleSortEnd = useCallback( - ({ oldIndex, newIndex }: DragProps) => { - document.body.classList.remove("grabbing"); - - const fieldOrder = updateFieldOrder(sortedFields, oldIndex, newIndex); + ({ itemIds: fieldOrder }) => { onUpdateFieldOrder(table, fieldOrder); }, - [table, sortedFields, onUpdateFieldOrder], + [table, onUpdateFieldOrder], + ); + + const renderItem = ({ item, id }: { item: Field; id: string | number }) => ( + <SortableColumn + key={`sortable-${id}`} + id={id} + field={item} + idFields={idFields} + table={table} + selectedSchemaId={selectedSchemaId} + /> ); return ( @@ -108,41 +112,20 @@ const MetadataTableColumnList = ({ </SortButtonContainer> </div> </div> - <SortableColumnList - helperClass="ColumnSortHelper" - useDragHandle={true} - onSortStart={handleSortStart} - onSortEnd={handleSortEnd} - > - {sortedFields.map((field, index) => ( - <SortableColumn - key={field.getId()} - index={index} - field={field} - idFields={idFields} - selectedDatabaseId={table.db_id} - selectedSchemaId={selectedSchemaId} - selectedTableId={table.id} - dragHandle={<SortableColumnHandle />} - /> - ))} - </SortableColumnList> + <div> + <SortableList + items={sortedFields} + renderItem={renderItem} + getId={getId} + onSortEnd={handleSortEnd} + sensors={[pointerSensor]} + useDragOverlay={false} + /> + </div> </div> ); }; -interface ColumnListProps { - children?: ReactNode; -} - -const ColumnList = ({ children, ...props }: ColumnListProps) => { - return <div {...props}>{children}</div>; -}; - -const ColumnGrabber = () => { - return <Grabber style={{ width: 10 }} />; -}; - interface TableFieldOrderOption { name: string; value: TableFieldOrder; @@ -191,31 +174,56 @@ const TableFieldOrderDropdown = ({ ); }; -const SortableColumn = SortableElement(MetadataTableColumn); -const SortableColumnList = SortableContainer(ColumnList); -const SortableColumnHandle = SortableHandle(ColumnGrabber); - -const updateFieldOrder = ( - fields: Field[], - oldIndex: number, - newIndex: number, -) => { - const fieldOrder = new Array<FieldId>(fields.length); - - fields.forEach((field, prevIndex) => { - const nextIndex = - newIndex <= prevIndex && prevIndex < oldIndex - ? prevIndex + 1 // shift down - : oldIndex < prevIndex && prevIndex <= newIndex - ? prevIndex - 1 // shift up - : prevIndex === oldIndex - ? newIndex // move dragged column to new location - : prevIndex; // otherwise, leave it where it is - - fieldOrder[nextIndex] = Number(field.id); +interface SortableColumnProps { + id: UniqueIdentifier; + field: Field; + idFields: Field[]; + table: Table; + selectedSchemaId: SchemaId; +} + +const SortableColumn = ({ + id, + field, + table, + idFields, + selectedSchemaId, +}: SortableColumnProps) => { + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging, + } = useSortable({ + id, }); - return fieldOrder; + const dragHandle = ( + <Grabber style={{ width: 10 }} {...attributes} {...listeners} /> + ); + + return ( + <div + ref={setNodeRef} + style={{ + transform: CSS.Transform.toString(transform), + transition, + position: "relative", + zIndex: isDragging ? 100 : 1, + }} + > + <MetadataTableColumn + field={field} + idFields={idFields} + selectedDatabaseId={table.db_id} + selectedSchemaId={selectedSchemaId} + selectedTableId={table.id} + dragHandle={dragHandle} + /> + </div> + ); }; // eslint-disable-next-line import/no-default-export -- deprecated usage diff --git a/frontend/src/metabase/components/Grabber/Grabber.jsx b/frontend/src/metabase/components/Grabber/Grabber.jsx index 81a77867fc8bf5aa73535b06d8a28f11629b274c..7889d500b0cd7bf5b1a0291007d48f737a1b26e8 100644 --- a/frontend/src/metabase/components/Grabber/Grabber.jsx +++ b/frontend/src/metabase/components/Grabber/Grabber.jsx @@ -1,6 +1,12 @@ /* eslint-disable react/prop-types */ import cx from "classnames"; -export default function Grabber({ className = "", style }) { - return <div className={cx("Grabber cursor-grab", className)} style={style} />; +export default function Grabber({ className = "", style, ...props }) { + return ( + <div + className={cx("Grabber cursor-grab", className)} + style={style} + {...props} + /> + ); } diff --git a/frontend/src/metabase/core/components/Sortable/SortableList.tsx b/frontend/src/metabase/core/components/Sortable/SortableList.tsx index c6dadf1391d79020e27555881851740c7cf8d87e..da63e07465f2849c316fa877da5e6ecf3c10aa1e 100644 --- a/frontend/src/metabase/core/components/Sortable/SortableList.tsx +++ b/frontend/src/metabase/core/components/Sortable/SortableList.tsx @@ -15,6 +15,7 @@ type ItemId = number | string; export type DragEndEvent = { id: ItemId; newIndex: number; + itemIds: ItemId[]; }; interface RenderItemProps<T> { @@ -30,6 +31,7 @@ interface useSortableListProps<T> { onSortEnd?: ({ id, newIndex }: DragEndEvent) => void; sensors?: SensorDescriptor<any>[]; modifiers?: Modifier[]; + useDragOverlay?: boolean; } export const SortableList = <T,>({ @@ -40,6 +42,7 @@ export const SortableList = <T,>({ onSortEnd, sensors = [], modifiers = [], + useDragOverlay = true, }: useSortableListProps<T>) => { const [itemIds, setItemIds] = useState<ItemId[]>([]); const [indexedItems, setIndexedItems] = useState<Record<ItemId, T>>({}); @@ -90,6 +93,7 @@ export const SortableList = <T,>({ onSortEnd({ id: getId(activeItem), newIndex: itemIds.findIndex(id => id === getId(activeItem)), + itemIds, }); setActiveItem(null); } @@ -104,15 +108,17 @@ export const SortableList = <T,>({ modifiers={modifiers} > <SortableContext items={itemIds}>{sortableElements}</SortableContext> - <DragOverlay> - {activeItem - ? renderItem({ - item: activeItem, - id: getId(activeItem), - isDragOverlay: true, - }) - : null} - </DragOverlay> + {useDragOverlay && ( + <DragOverlay> + {activeItem + ? renderItem({ + item: activeItem, + id: getId(activeItem), + isDragOverlay: true, + }) + : null} + </DragOverlay> + )} </DndContext> ); }; diff --git a/frontend/src/metabase/visualizations/components/settings/ChartSettingOrderedItems/ChartSettingOrderedItems.tsx b/frontend/src/metabase/visualizations/components/settings/ChartSettingOrderedItems/ChartSettingOrderedItems.tsx index d12bada006b3aba684724cace2429c40ce61a03d..4adb7351b7dcac4214c828b00225859cb2383374 100644 --- a/frontend/src/metabase/visualizations/components/settings/ChartSettingOrderedItems/ChartSettingOrderedItems.tsx +++ b/frontend/src/metabase/visualizations/components/settings/ChartSettingOrderedItems/ChartSettingOrderedItems.tsx @@ -1,6 +1,7 @@ import { useSensor, PointerSensor } from "@dnd-kit/core"; import { useCallback } from "react"; +import type { DragEndEvent } from "metabase/core/components/Sortable"; import { Sortable, SortableList } from "metabase/core/components/Sortable"; import type { IconProps } from "metabase/ui"; @@ -23,13 +24,7 @@ interface SortableColumnFunctions<T> { } interface ChartSettingOrderedItemsProps<T extends SortableItem> extends SortableColumnFunctions<T> { - onSortEnd: ({ - id, - newIndex, - }: { - id: number | string; - newIndex: number; - }) => void; + onSortEnd: ({ id, newIndex }: DragEndEvent) => void; items: T[]; getId: (item: T) => string | number; }