diff --git a/packages/ui/src/sortable/draggable.tsx b/packages/ui/src/sortable/draggable.tsx index cdeef1347..a56afe073 100644 --- a/packages/ui/src/sortable/draggable.tsx +++ b/packages/ui/src/sortable/draggable.tsx @@ -3,7 +3,8 @@ import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine"; import { draggable, dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"; import { isEqual } from "lodash"; import { cn } from "../../helpers"; - +import { attachClosestEdge, extractClosestEdge } from "@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge"; +import { DropIndicator } from "../drop-indicator"; type Props = { children: React.ReactNode; data: any; //@todo make this generic @@ -14,6 +15,7 @@ const Draggable = ({ children, data, className }: Props) => { const [dragging, setDragging] = useState(false); // NEW const [isDraggedOver, setIsDraggedOver] = useState(false); + const [closestEdge, setClosestEdge] = useState(null); useEffect(() => { const el = ref.current; @@ -27,19 +29,31 @@ const Draggable = ({ children, data, className }: Props) => { }), dropTargetForElements({ element: el, - onDragEnter: () => setIsDraggedOver(true), + onDragEnter: (args) => { + setIsDraggedOver(true); + setClosestEdge(extractClosestEdge(args.self.data)); + }, onDragLeave: () => setIsDraggedOver(false), - onDrop: () => setIsDraggedOver(false), + onDrop: () => { + setIsDraggedOver(false); + }, canDrop: ({ source }) => !isEqual(source.data, data), - getData: () => data, + getData: ({ input, element }) => + attachClosestEdge(data, { + input, + element, + allowedEdges: ["top", "bottom"], + }), }) ); } }, [data]); return ( -
+
+ {} {children} + {}
); }; diff --git a/packages/ui/src/sortable/sortable.stories.tsx b/packages/ui/src/sortable/sortable.stories.tsx index f5654e8d0..6d40ddc2e 100644 --- a/packages/ui/src/sortable/sortable.stories.tsx +++ b/packages/ui/src/sortable/sortable.stories.tsx @@ -13,7 +13,7 @@ type Story = StoryObj; const data = [ { id: "1", name: "John Doe" }, - { id: "2", name: "Jane Doe" }, + { id: "2", name: "Jane Doe 2" }, { id: "3", name: "Alice" }, { id: "4", name: "Bob" }, { id: "5", name: "Charlie" }, @@ -22,11 +22,11 @@ export const Default: Story = { args: { data, render: (item: any) => ( - -
{item.name}
-
+ // +
{item.name}
+ //
), - onChange: (data) => console.log(data), + onChange: (data) => console.log(data.map(({ id }) => id)), keyExtractor: (item: any) => item.id, }, }; diff --git a/packages/ui/src/sortable/sortable.tsx b/packages/ui/src/sortable/sortable.tsx index e8d2ae503..967965f2a 100644 --- a/packages/ui/src/sortable/sortable.tsx +++ b/packages/ui/src/sortable/sortable.tsx @@ -1,33 +1,55 @@ import React, { Fragment, useEffect } from "react"; import { monitorForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"; +import { Draggable } from "./draggable"; type Props = { data: T[]; render: (item: T, index: number) => React.ReactNode; onChange: (data: T[]) => void; keyExtractor: (item: T, index: number) => string; + containerClassName?: string; }; -const moveItems = (data: T[], source: T, destination: T): T[] => { +const moveItem = ( + data: T[], + source: T, + destination: T & Record, + keyExtractor: (item: T, index: number) => string +) => { const sourceIndex = data.indexOf(source); - const destinationIndex = data.indexOf(destination); + if (sourceIndex === -1) return data; - if (sourceIndex === -1 || destinationIndex === -1) return data; + const destinationIndex = data.findIndex((item, index) => keyExtractor(item, index) === keyExtractor(destination, 0)); + if (destinationIndex === -1) return data; + + const symbolKey = Reflect.ownKeys(destination).find((key) => key.toString() === "Symbol(closestEdge)"); + const position = symbolKey ? destination[symbolKey as symbol] : "bottom"; // Add 'as symbol' to cast symbolKey to symbol const newData = [...data]; - newData.splice(sourceIndex, 1); - newData.splice(destinationIndex, 0, source); + const [movedItem] = newData.splice(sourceIndex, 1); + + let adjustedDestinationIndex = destinationIndex; + if (position === "bottom") { + adjustedDestinationIndex++; + } + + // Prevent moving item out of bounds + if (adjustedDestinationIndex > newData.length) { + adjustedDestinationIndex = newData.length; + } + + newData.splice(adjustedDestinationIndex, 0, movedItem); return newData; }; -export const Sortable = ({ data, render, onChange, keyExtractor }: Props) => { +export const Sortable = ({ data, render, onChange, keyExtractor, containerClassName }: Props) => { useEffect(() => { const unsubscribe = monitorForElements({ onDrop({ source, location }) { const destination = location?.current?.dropTargets[0]; if (!destination) return; - onChange(moveItems(data, source.data as T, destination.data as T)); + onChange(moveItem(data, source.data as T, destination.data as T & { closestEdge: string }, keyExtractor)); }, }); @@ -40,7 +62,9 @@ export const Sortable = ({ data, render, onChange, keyExtractor }: Props) return ( <> {data.map((item, index) => ( - {render(item, index)} + + {render(item, index)} + ))} ); diff --git a/web/components/estimates/points/create-root.tsx b/web/components/estimates/points/create-root.tsx index 31a93aa33..e1b3f1e1a 100644 --- a/web/components/estimates/points/create-root.tsx +++ b/web/components/estimates/points/create-root.tsx @@ -86,21 +86,19 @@ export const EstimatePointCreateRoot: FC = observer((p ( - - - handleEstimatePoint("update", { ...value, value: estimatePointValue }) - } - handleEstimatePointValueRemove={() => handleEstimatePoint("remove", value)} - /> - + + handleEstimatePoint("update", { ...value, value: estimatePointValue }) + } + handleEstimatePointValueRemove={() => handleEstimatePoint("remove", value)} + /> )} onChange={(data: TEstimatePointsObject[]) => handleDragEstimatePoints(data)} keyExtractor={(item: TEstimatePointsObject) => item?.id?.toString() || item.value.toString()} diff --git a/web/components/estimates/points/edit-root.tsx b/web/components/estimates/points/edit-root.tsx index 966b661e9..f1277c515 100644 --- a/web/components/estimates/points/edit-root.tsx +++ b/web/components/estimates/points/edit-root.tsx @@ -1,4 +1,4 @@ -import { FC, useState } from "react"; +import { FC, Fragment, useState } from "react"; import { observer } from "mobx-react"; import { Plus } from "lucide-react"; import { TEstimatePointsObject } from "@plane/types"; @@ -74,26 +74,28 @@ export const EstimatePointEditRoot: FC = observer((props return (
{estimate?.type}
- ( - - {value?.id && estimate?.type && ( - - )} - - )} - onChange={(data: TEstimatePointsObject[]) => handleDragEstimatePoints(data)} - keyExtractor={(item: TEstimatePointsObject) => item?.id?.toString() || item.value.toString()} - /> +
+ ( + + {value?.id && estimate?.type ? ( + + ) : null} + + )} + onChange={(data: TEstimatePointsObject[]) => handleDragEstimatePoints(data)} + keyExtractor={(item: TEstimatePointsObject) => item?.id?.toString() || item.value.toString()} + /> +
{estimatePointCreate && estimatePointCreate.map( diff --git a/web/components/estimates/points/preview.tsx b/web/components/estimates/points/preview.tsx index 57e938992..741b401de 100644 --- a/web/components/estimates/points/preview.tsx +++ b/web/components/estimates/points/preview.tsx @@ -43,7 +43,7 @@ export const EstimatePointItemPreview: FC = observer( return (
{!estimatePointEditToggle && !estimatePointDeleteToggle && ( -
+