From 2619f4cfefdf9264a7f4c3a8741493323fdde901 Mon Sep 17 00:00:00 2001 From: Mohamed Bassem Date: Fri, 28 Nov 2025 15:39:57 +0000 Subject: fix: separate shared lists in the sidebar (#2180) * fix: separate shared lists in the sidebar * fix sub * i18n --- .../components/dashboard/lists/AllListsView.tsx | 74 +++++++++-- .../dashboard/lists/CollapsibleBookmarkLists.tsx | 56 ++++++--- apps/web/components/dashboard/sidebar/AllLists.tsx | 137 +++++++++++++++++++-- 3 files changed, 236 insertions(+), 31 deletions(-) (limited to 'apps/web/components') diff --git a/apps/web/components/dashboard/lists/AllListsView.tsx b/apps/web/components/dashboard/lists/AllListsView.tsx index 6101112d..7a7c9504 100644 --- a/apps/web/components/dashboard/lists/AllListsView.tsx +++ b/apps/web/components/dashboard/lists/AllListsView.tsx @@ -1,13 +1,22 @@ "use client"; +import { useMemo, useState } from "react"; import Link from "next/link"; import { EditListModal } from "@/components/dashboard/lists/EditListModal"; import { Button } from "@/components/ui/button"; -import { CollapsibleTriggerChevron } from "@/components/ui/collapsible"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTriggerChevron, +} from "@/components/ui/collapsible"; import { useTranslation } from "@/lib/i18n/client"; import { MoreHorizontal, Plus } from "lucide-react"; import type { ZBookmarkList } from "@karakeep/shared/types/lists"; +import { + augmentBookmarkListsWithInitialData, + useBookmarkLists, +} from "@karakeep/shared-react/hooks/lists"; import { CollapsibleBookmarkLists } from "./CollapsibleBookmarkLists"; import { ListOptions } from "./ListOptions"; @@ -64,6 +73,20 @@ export default function AllListsView({ initialData: ZBookmarkList[]; }) { const { t } = useTranslation(); + + // Fetch live lists data + const { data: listsData } = useBookmarkLists(undefined, { + initialData: { lists: initialData }, + }); + const lists = augmentBookmarkListsWithInitialData(listsData, initialData); + + // Check if there are any shared lists + const hasSharedLists = useMemo(() => { + return lists.data.some((list) => list.userRole !== "owner"); + }, [lists.data]); + + const [sharedListsOpen, setSharedListsOpen] = useState(true); + return ( ); } diff --git a/apps/web/components/dashboard/lists/CollapsibleBookmarkLists.tsx b/apps/web/components/dashboard/lists/CollapsibleBookmarkLists.tsx index 2f8fca6a..2bb5f41b 100644 --- a/apps/web/components/dashboard/lists/CollapsibleBookmarkLists.tsx +++ b/apps/web/components/dashboard/lists/CollapsibleBookmarkLists.tsx @@ -9,9 +9,10 @@ import { ZBookmarkList } from "@karakeep/shared/types/lists"; import { ZBookmarkListTreeNode } from "@karakeep/shared/utils/listUtils"; type RenderFunc = (params: { - item: ZBookmarkListTreeNode; + node: ZBookmarkListTreeNode; level: number; open: boolean; + onOpenChange: (open: boolean) => void; numBookmarks?: number; }) => React.ReactNode; @@ -23,11 +24,15 @@ function ListItem({ level, className, isOpenFunc, + listStats, + indentOffset, }: { node: ZBookmarkListTreeNode; render: RenderFunc; isOpenFunc: IsOpenFunc; + listStats?: Map; level: number; + indentOffset: number; className?: string; }) { // Not the most efficient way to do this, but it works for now @@ -44,17 +49,15 @@ function ListItem({ useEffect(() => { setOpen((curr) => curr || isAnyChildOpen(node, isOpenFunc)); }, [node, isOpenFunc]); - const { data: listStats } = api.lists.stats.useQuery(undefined, { - placeholderData: keepPreviousData, - }); return ( {render({ - item: node, - level, + node, + level: level + indentOffset, open, - numBookmarks: listStats?.stats.get(node.item.id), + onOpenChange: setOpen, + numBookmarks: listStats?.get(node.item.id), })} {node.children @@ -66,6 +69,8 @@ function ListItem({ node={l} render={render} level={level + 1} + indentOffset={indentOffset} + listStats={listStats} className={className} /> ))} @@ -77,35 +82,56 @@ function ListItem({ export function CollapsibleBookmarkLists({ render, initialData, + listsData, className, isOpenFunc, + filter, + indentOffset = 0, }: { - initialData: ZBookmarkList[]; + initialData?: ZBookmarkList[]; + listsData?: { + data: ZBookmarkList[]; + root: Record; + allPaths: ZBookmarkList[][]; + getPathById: (id: string) => ZBookmarkList[] | undefined; + }; render: RenderFunc; isOpenFunc?: IsOpenFunc; className?: string; + filter?: (node: ZBookmarkListTreeNode) => boolean; + indentOffset?: number; }) { - let { data } = useBookmarkLists(undefined, { - initialData: { lists: initialData }, + // If listsData is provided, use it directly. Otherwise, fetch it. + let { data: fetchedData } = useBookmarkLists(undefined, { + initialData: initialData ? { lists: initialData } : undefined, + enabled: !listsData, // Only fetch if listsData is not provided + }); + const data = listsData || fetchedData; + + const { data: listStats } = api.lists.stats.useQuery(undefined, { + placeholderData: keepPreviousData, }); if (!data) { return ; } - const { root } = data; + const rootNodes = Object.values(data.root); + const filteredRoots = filter ? rootNodes.filter(filter) : rootNodes; return (
- {Object.values(root) + {filteredRoots .sort((a, b) => a.item.name.localeCompare(b.item.name)) - .map((l) => ( + .map((node) => ( false)} /> ))} diff --git a/apps/web/components/dashboard/sidebar/AllLists.tsx b/apps/web/components/dashboard/sidebar/AllLists.tsx index 1cc3f3a5..f8b80cba 100644 --- a/apps/web/components/dashboard/sidebar/AllLists.tsx +++ b/apps/web/components/dashboard/sidebar/AllLists.tsx @@ -1,16 +1,24 @@ "use client"; -import { useCallback, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import Link from "next/link"; import { usePathname } from "next/navigation"; import SidebarItem from "@/components/shared/sidebar/SidebarItem"; import { Button } from "@/components/ui/button"; -import { CollapsibleTriggerTriangle } from "@/components/ui/collapsible"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTriggerTriangle, +} from "@/components/ui/collapsible"; import { useTranslation } from "@/lib/i18n/client"; import { cn } from "@/lib/utils"; import { MoreHorizontal, Plus } from "lucide-react"; import type { ZBookmarkList } from "@karakeep/shared/types/lists"; +import { + augmentBookmarkListsWithInitialData, + useBookmarkLists, +} from "@karakeep/shared-react/hooks/lists"; import { ZBookmarkListTreeNode } from "@karakeep/shared/utils/listUtils"; import { CollapsibleBookmarkLists } from "../lists/CollapsibleBookmarkLists"; @@ -31,6 +39,36 @@ export default function AllLists({ const [selectedListId, setSelectedListId] = useState(null); + // Fetch live lists data + const { data: listsData } = useBookmarkLists(undefined, { + initialData: { lists: initialData.lists }, + }); + const lists = augmentBookmarkListsWithInitialData( + listsData, + initialData.lists, + ); + + // Check if any shared list is currently being viewed + const isViewingSharedList = useMemo(() => { + return lists.data.some( + (list) => list.userRole !== "owner" && pathName.includes(list.id), + ); + }, [lists.data, pathName]); + + // Check if there are any shared lists + const hasSharedLists = useMemo(() => { + return lists.data.some((list) => list.userRole !== "owner"); + }, [lists.data]); + + const [sharedListsOpen, setSharedListsOpen] = useState(isViewingSharedList); + + // Auto-open shared lists if viewing one + useEffect(() => { + if (isViewingSharedList && !sharedListsOpen) { + setSharedListsOpen(true); + } + }, [isViewingSharedList, sharedListsOpen]); + return (
  • @@ -60,10 +98,13 @@ export default function AllLists({ linkClassName="py-0.5" className="px-0.5" /> + + {/* Owned Lists */} node.item.userRole === "owner"} isOpenFunc={isNodeOpen} - render={({ item: node, level, open, numBookmarks }) => ( + render={({ node, level, open, numBookmarks }) => ( 0 && ( @@ -83,8 +124,8 @@ export default function AllLists({ className="group px-0.5" right={ { - if (open) { + onOpenChange={(isOpen) => { + if (isOpen) { setSelectedListId(node.item.id); } else { setSelectedListId(null); @@ -101,7 +142,6 @@ export default function AllLists({ : "opacity-0", )} /> - )} /> + + {/* Shared Lists */} + {hasSharedLists && ( + + + } + logo={👥} + name={t("lists.shared_lists")} + path="#" + linkClassName="py-0.5" + className="px-0.5" + /> + + node.item.userRole !== "owner"} + isOpenFunc={isNodeOpen} + indentOffset={1} + render={({ node, level, open, numBookmarks }) => ( + 0 && ( + + ) + } + logo={ + + {node.item.icon} + + } + name={node.item.name} + path={`/dashboard/lists/${node.item.id}`} + className="group px-0.5" + right={ + { + if (isOpen) { + setSelectedListId(node.item.id); + } else { + setSelectedListId(null); + } + }} + list={node.item} + > + + + } + linkClassName="py-0.5" + style={{ marginLeft: `${level * 1}rem` }} + /> + )} + /> + + + )}
); } -- cgit v1.2.3-70-g09d2