aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--apps/web/components/dashboard/tags/AllTagsView.tsx77
-rw-r--r--apps/web/components/dashboard/tags/TagPill.tsx96
-rw-r--r--apps/web/lib/drag-and-drop.ts55
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 {