From d9af0461efeb6d0678cd3aaf3f0904c638ed9dec Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 11 Sep 2025 18:11:12 +0300 Subject: [PATCH] chore(react/collections/table): add drop indicator --- .../src/widgets/collections/board/index.css | 19 ++-- .../src/widgets/collections/board/index.tsx | 103 ++++++++++++++---- 2 files changed, 91 insertions(+), 31 deletions(-) diff --git a/apps/client/src/widgets/collections/board/index.css b/apps/client/src/widgets/collections/board/index.css index d5ed0ba5f..6a00dec2f 100644 --- a/apps/client/src/widgets/collections/board/index.css +++ b/apps/client/src/widgets/collections/board/index.css @@ -106,7 +106,7 @@ position: relative; background-color: var(--main-background-color); border: 1px solid var(--main-border-color); - transition: transform 0.2s ease, box-shadow 0.2s ease, opacity 0.15s ease; + transition: transform 0.2s ease, box-shadow 0.2s ease, opacity 0.15s ease, margin-top 0.2s ease; opacity: 1; } @@ -144,12 +144,16 @@ } .board-view-container .board-note.dragging { - opacity: 0.8; + opacity: 0.5; transform: rotate(5deg); z-index: 1000; box-shadow: 4px 8px 16px rgba(0, 0, 0, 0.5); } +.board-view-container .board-note.shift-down { + margin-top: 45px; +} + .board-view-container .board-note.editing { box-shadow: 2px 4px 8px rgba(0, 0, 0, 0.35); border-color: var(--main-text-color); @@ -171,16 +175,17 @@ } .board-drop-indicator { - height: 3px; - background-color: var(--main-text-color); + height: 2px; + background: linear-gradient(90deg, transparent, var(--main-text-color) 20%, var(--main-text-color) 80%, transparent); border-radius: 2px; - margin: 0.25em 0; + margin: -1px 0; opacity: 0; - transition: opacity 0.2s ease; + transition: opacity 0.15s ease; + position: relative; } .board-drop-indicator.show { - opacity: 1; + opacity: 0.8; } .column-drop-indicator { diff --git a/apps/client/src/widgets/collections/board/index.tsx b/apps/client/src/widgets/collections/board/index.tsx index 6eb9e17a9..64364ed17 100644 --- a/apps/client/src/widgets/collections/board/index.tsx +++ b/apps/client/src/widgets/collections/board/index.tsx @@ -22,8 +22,9 @@ export default function BoardView({ note: parentNote, noteIds, viewConfig, saveC const [ statusAttribute ] = useNoteLabel(parentNote, "board:groupBy"); const [ byColumn, setByColumn ] = useState(); const [ columns, setColumns ] = useState(); - const [ draggedCard, setDraggedCard ] = useState<{ noteId: string, fromColumn: string } | null>(null); + const [ draggedCard, setDraggedCard ] = useState<{ noteId: string, fromColumn: string, index: number } | null>(null); const [ dropTarget, setDropTarget ] = useState(null); + const [ dropPosition, setDropPosition ] = useState<{ column: string, index: number } | null>(null); function refresh() { getBoardData(parentNote, statusAttribute ?? "status", viewConfig ?? {}).then(({ byColumn, newPersistedData }) => { @@ -82,6 +83,8 @@ export default function BoardView({ note: parentNote, noteIds, viewConfig, saveC setDraggedCard={setDraggedCard} dropTarget={dropTarget} setDropTarget={setDropTarget} + dropPosition={dropPosition} + setDropPosition={setDropPosition} onCardDrop={refresh} /> ))} @@ -101,22 +104,44 @@ function Column({ setDraggedCard, dropTarget, setDropTarget, + dropPosition, + setDropPosition, onCardDrop }: { parentNote: FNote, column: string, columnItems?: { note: FNote, branch: FBranch }[], statusAttribute: string, - draggedCard: { noteId: string, fromColumn: string } | null, - setDraggedCard: (card: { noteId: string, fromColumn: string } | null) => void, + draggedCard: { noteId: string, fromColumn: string, index: number } | null, + setDraggedCard: (card: { noteId: string, fromColumn: string, index: number } | null) => void, dropTarget: string | null, setDropTarget: (target: string | null) => void, + dropPosition: { column: string, index: number } | null, + setDropPosition: (position: { column: string, index: number } | null) => void, onCardDrop: () => void }) { const handleDragOver = useCallback((e: DragEvent) => { e.preventDefault(); setDropTarget(column); - }, [column, setDropTarget]); + + // Calculate drop position based on mouse position + const cards = Array.from(e.currentTarget.querySelectorAll('.board-note')); + const mouseY = e.clientY; + + let newIndex = cards.length; + for (let i = 0; i < cards.length; i++) { + const card = cards[i] as HTMLElement; + const rect = card.getBoundingClientRect(); + const cardMiddle = rect.top + rect.height / 2; + + if (mouseY < cardMiddle) { + newIndex = i; + break; + } + } + + setDropPosition({ column, index: newIndex }); + }, [column, setDropTarget, setDropPosition]); const handleDragLeave = useCallback((e: DragEvent) => { const relatedTarget = e.relatedTarget as HTMLElement; @@ -124,19 +149,25 @@ function Column({ if (!currentTarget.contains(relatedTarget)) { setDropTarget(null); + setDropPosition(null); } - }, [setDropTarget]); + }, [setDropTarget, setDropPosition]); const handleDrop = useCallback(async (e: DragEvent) => { e.preventDefault(); setDropTarget(null); + setDropPosition(null); - if (draggedCard && draggedCard.fromColumn !== column) { - await changeColumn(draggedCard.noteId, column, statusAttribute); - onCardDrop(); + if (draggedCard) { + // For now, just handle column changes + // TODO: Add position/order handling + if (draggedCard.fromColumn !== column) { + await changeColumn(draggedCard.noteId, column, statusAttribute); + onCardDrop(); + } } setDraggedCard(null); - }, [draggedCard, column, statusAttribute, setDraggedCard, setDropTarget, onCardDrop]); + }, [draggedCard, column, statusAttribute, setDraggedCard, setDropTarget, setDropPosition, onCardDrop]); return (
- {(columnItems ?? []).map(({ note, branch }) => ( - - ))} + {(columnItems ?? []).map(({ note, branch }, index) => { + const showIndicatorBefore = dropPosition?.column === column && + dropPosition.index === index && + draggedCard?.noteId !== note.noteId; + const shouldShift = dropPosition?.column === column && + dropPosition.index <= index && + draggedCard?.noteId !== note.noteId && + draggedCard !== null; + + return ( + <> + {showIndicatorBefore && ( +
+ )} + + + ); + })} + {dropPosition?.column === column && dropPosition.index === (columnItems?.length ?? 0) && ( +
+ )}
createNewItem(parentNote, column)}> {" "} @@ -172,22 +223,26 @@ function Column({ function Card({ note, column, + index, setDraggedCard, - isDragging + isDragging, + shouldShift }: { note: FNote, branch: FBranch, column: string, - setDraggedCard: (card: { noteId: string, fromColumn: string } | null) => void, - isDragging: boolean + index: number, + setDraggedCard: (card: { noteId: string, fromColumn: string, index: number } | null) => void, + isDragging: boolean, + shouldShift?: boolean }) { const colorClass = note.getColorClass() || ''; const handleDragStart = useCallback((e: DragEvent) => { e.dataTransfer!.effectAllowed = 'move'; e.dataTransfer!.setData('text/plain', note.noteId); - setDraggedCard({ noteId: note.noteId, fromColumn: column }); - }, [note.noteId, column, setDraggedCard]); + setDraggedCard({ noteId: note.noteId, fromColumn: column, index }); + }, [note.noteId, column, index, setDraggedCard]); const handleDragEnd = useCallback(() => { setDraggedCard(null); @@ -195,7 +250,7 @@ function Card({ return (