diff options
| author | kamtschatka <simon.schatka@gmx.at> | 2024-07-01 13:59:10 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-07-01 12:59:10 +0100 |
| commit | 9cd617055b7878048120eebd58dc2e05a9cbdfca (patch) | |
| tree | 4c80908640bb6b92e355d027ed64fce26fac2454 /apps/web | |
| parent | 9883c6bcd18c2ca1fda7d83e465a0bf9cf45510d (diff) | |
| download | karakeep-9cd617055b7878048120eebd58dc2e05a9cbdfca.tar.zst | |
refactor: drag and drop improvements (#264)
* [Feature request] Drag and Drop Items to Lists #123
reworked the drag and drop mechanism to still have change detection, but not so much that it has a huge overhead
Changed the layout a bit to allow proper drag/drop of elements from the main section to the sidebar
Added the possibility to drag/drop bookmarks onto lists
* [Feature request] Drag and Drop Items to Lists #123
Removed the changes to allow dragging&dropping bookmarks
Diffstat (limited to 'apps/web')
| -rw-r--r-- | apps/web/components/dashboard/tags/AllTagsView.tsx | 77 | ||||
| -rw-r--r-- | apps/web/components/dashboard/tags/TagPill.tsx | 96 | ||||
| -rw-r--r-- | apps/web/lib/drag-and-drop.ts | 55 |
3 files changed, 124 insertions, 104 deletions
diff --git a/apps/web/components/dashboard/tags/AllTagsView.tsx b/apps/web/components/dashboard/tags/AllTagsView.tsx index ac139e23..7b81ed72 100644 --- a/apps/web/components/dashboard/tags/AllTagsView.tsx +++ b/apps/web/components/dashboard/tags/AllTagsView.tsx @@ -13,16 +13,11 @@ import InfoTooltip from "@/components/ui/info-tooltip"; import { Separator } from "@/components/ui/separator"; import { Toggle } from "@/components/ui/toggle"; import { toast } from "@/components/ui/use-toast"; -import { useDragAndDrop } from "@/lib/drag-and-drop"; import { api } from "@/lib/trpc"; import { ArrowDownAZ, Combine } from "lucide-react"; -import Draggable from "react-draggable"; import type { ZGetTagResponse } from "@hoarder/shared/types/tags"; -import { - useDeleteUnusedTags, - useMergeTag, -} from "@hoarder/shared-react/hooks/tags"; +import { useDeleteUnusedTags } from "@hoarder/shared-react/hooks/tags"; import { TagPill } from "./TagPill"; @@ -79,17 +74,6 @@ export default function AllTagsView({ const [draggingEnabled, setDraggingEnabled] = React.useState(false); const [sortByName, setSortByName] = React.useState(false); - const { handleDragStart, handleDragEnd } = useDragAndDrop( - "data-id", - "data-id", - (dragSourceId: string, dragTargetId: string) => { - mergeTag({ - fromTagIds: [dragSourceId], - intoTagId: dragTargetId, - }); - }, - ); - function toggleSortByName(): void { setSortByName(!sortByName); } @@ -98,40 +82,9 @@ export default function AllTagsView({ setDraggingEnabled(!draggingEnabled); } - const { mutate: mergeTag } = useMergeTag({ - onSuccess: () => { - toast({ - description: "Tags have been merged!", - }); - }, - onError: (e) => { - if (e.data?.code == "BAD_REQUEST") { - if (e.data.zodError) { - toast({ - variant: "destructive", - description: Object.values(e.data.zodError.fieldErrors) - .flat() - .join("\n"), - }); - } else { - toast({ - variant: "destructive", - description: e.message, - }); - } - } else { - toast({ - variant: "destructive", - title: "Something went wrong", - }); - } - }, - }); - const { data } = api.tags.list.useQuery(undefined, { initialData: { tags: initialData }, }); - // Sort tags by usage desc const allTags = data.tags.sort(sortByName ? byNameSorter : byUsageSorter); @@ -145,21 +98,13 @@ export default function AllTagsView({ tagPill = ( <div className="flex flex-wrap gap-3"> {tags.map((t) => ( - <Draggable + <TagPill key={t.id} - axis="both" - onStart={handleDragStart} - onStop={handleDragEnd} - disabled={!draggingEnabled} - defaultClassNameDragging={ - "position-relative z-10 pointer-events-none" - } - position={{ x: 0, y: 0 }} - > - <div className="cursor-grab" data-id={t.id}> - <TagPill id={t.id} name={t.name} count={t.count} /> - </div> - </Draggable> + id={t.id} + name={t.name} + count={t.count} + isDraggable={draggingEnabled} + /> ))} </div> ); @@ -173,6 +118,7 @@ export default function AllTagsView({ <div className="flex justify-end gap-x-2"> <Toggle variant="outline" + aria-label="Toggle bold" pressed={draggingEnabled} onPressedChange={toggleDraggingEnabled} > @@ -184,6 +130,7 @@ export default function AllTagsView({ </Toggle> <Toggle variant="outline" + aria-label="Toggle bold" pressed={sortByName} onPressedChange={toggleSortByName} > @@ -196,22 +143,16 @@ export default function AllTagsView({ <p>Tags that were attached at least once by you</p> </InfoTooltip> </span> - {tagsToPill(humanTags)} - <Separator /> - <span className="flex items-center gap-2"> <p className="text-lg">AI Tags</p> <InfoTooltip size={15} className="my-auto" variant="explain"> <p>Tags that were only attached automatically (by AI)</p> </InfoTooltip> </span> - {tagsToPill(aiTags)} - <Separator /> - <span className="flex items-center gap-2"> <p className="text-lg">Unused Tags</p> <InfoTooltip size={15} className="my-auto" variant="explain"> diff --git a/apps/web/components/dashboard/tags/TagPill.tsx b/apps/web/components/dashboard/tags/TagPill.tsx index f1c99d70..88b88b52 100644 --- a/apps/web/components/dashboard/tags/TagPill.tsx +++ b/apps/web/components/dashboard/tags/TagPill.tsx @@ -1,7 +1,13 @@ +import React from "react"; import Link from "next/link"; import { Button } from "@/components/ui/button"; import { Separator } from "@/components/ui/separator"; +import { toast } from "@/components/ui/use-toast"; +import { useDragAndDrop } from "@/lib/drag-and-drop"; import { X } from "lucide-react"; +import Draggable from "react-draggable"; + +import { useMergeTag } from "@hoarder/shared-react/hooks/tags"; import DeleteTagConfirmationDialog from "./DeleteTagConfirmationDialog"; @@ -9,32 +15,84 @@ export function TagPill({ id, name, count, + isDraggable, }: { id: string; name: string; count: number; + isDraggable: boolean; }) { - return ( - <div className="group relative flex"> - <Link - className={ - "flex gap-2 rounded-md border border-border bg-background px-2 py-1 text-foreground hover:bg-foreground hover:text-background" + const { mutate: mergeTag } = useMergeTag({ + onSuccess: () => { + toast({ + description: "Tags have been merged!", + }); + }, + onError: (e) => { + if (e.data?.code == "BAD_REQUEST") { + if (e.data.zodError) { + toast({ + variant: "destructive", + description: Object.values(e.data.zodError.fieldErrors) + .flat() + .join("\n"), + }); + } else { + toast({ + variant: "destructive", + description: e.message, + }); } - href={`/dashboard/tags/${id}`} - data-id={id} - > - {name} <Separator orientation="vertical" /> {count} - </Link> + } else { + toast({ + variant: "destructive", + title: "Something went wrong", + }); + } + }, + }); + + const dragAndDropFunction = useDragAndDrop( + "data-id", + (dragTargetId: string) => { + mergeTag({ + fromTagIds: [id], + intoTagId: dragTargetId, + }); + }, + ); - <DeleteTagConfirmationDialog tag={{ name, id }}> - <Button - size="none" - variant="secondary" - className="-translate-1/2 absolute -right-1 -top-1 hidden rounded-full group-hover:block" + return ( + <Draggable + key={id} + axis="both" + onStart={dragAndDropFunction.handleDragStart} + onStop={dragAndDropFunction.handleDragEnd} + disabled={!isDraggable} + defaultClassNameDragging={"position-relative z-10 pointer-events-none"} + position={{ x: 0, y: 0 }} + > + <div className="group relative flex"> + <Link + className={ + "flex gap-2 rounded-md border border-border bg-background px-2 py-1 text-foreground hover:bg-foreground hover:text-background" + } + href={`/dashboard/tags/${id}`} + data-id={id} > - <X className="size-3" /> - </Button> - </DeleteTagConfirmationDialog> - </div> + {name} <Separator orientation="vertical" /> {count} + </Link> + + <DeleteTagConfirmationDialog tag={{ name, id }}> + <Button + size="none" + variant="secondary" + className="-translate-1/2 absolute -right-1 -top-1 hidden rounded-full group-hover:block" + > + <X className="size-3" /> + </Button> + </DeleteTagConfirmationDialog> + </div> + </Draggable> ); } diff --git a/apps/web/lib/drag-and-drop.ts b/apps/web/lib/drag-and-drop.ts index e005a6d0..ec37e810 100644 --- a/apps/web/lib/drag-and-drop.ts +++ b/apps/web/lib/drag-and-drop.ts @@ -1,31 +1,48 @@ import React from "react"; -import { DraggableData, DraggableEvent } from "react-draggable"; +import { DraggableEvent } from "react-draggable"; + +export interface DraggingState { + isDragging: boolean; + initialX: number; + initialY: number; +} export function useDragAndDrop( - dragSourceIdAttribute: string, dragTargetIdAttribute: string, - onDragOver: (dragSourceId: string, dragTargetId: string) => void, + onDragOver: (dragTargetId: string) => void, + setDraggingState?: React.Dispatch<React.SetStateAction<DraggingState>>, ) { - const [dragSourceId, setDragSourceId] = React.useState<string | null>(null); + function findTargetId(element: HTMLElement): string | null { + let currentElement: HTMLElement | null = element; + while (currentElement) { + const listId = currentElement.getAttribute(dragTargetIdAttribute); + if (listId) { + return listId; + } + currentElement = currentElement.parentElement; + } + return null; + } const handleDragStart = React.useCallback( - (_e: DraggableEvent, { node }: DraggableData) => { - const id = node.getAttribute(dragSourceIdAttribute); - setDragSourceId(id); + (e: DraggableEvent) => { + const rect = (e.target as HTMLElement).getBoundingClientRect(); + setDraggingState?.({ + isDragging: true, + initialX: rect.x, + initialY: rect.y, + }); }, - [], + [setDraggingState], ); const handleDragEnd = React.useCallback( (e: DraggableEvent) => { const { target } = e; - const dragTargetId = (target as HTMLElement).getAttribute( - dragTargetIdAttribute, - ); + const dragTargetId = findTargetId(target as HTMLElement); - if (dragSourceId && dragTargetId && dragSourceId !== dragTargetId) { - /* - As Draggable tries to setState when the + if (dragTargetId) { + /* As Draggable tries to setState when the component is unmounted, it is needed to push onCombine to the event loop queue. onCombine would be run after setState on @@ -33,12 +50,16 @@ export function useDragAndDrop( they fix it on their end. */ setTimeout(() => { - onDragOver(dragSourceId, dragTargetId); + onDragOver(dragTargetId); }, 0); } - setDragSourceId(null); + setDraggingState?.({ + isDragging: false, + initialX: 0, + initialY: 0, + }); }, - [dragSourceId, onDragOver], + [onDragOver], ); return { |
