aboutsummaryrefslogtreecommitdiffstats
path: root/apps/web/components
diff options
context:
space:
mode:
authorMohamed Bassem <me@mbassem.com>2025-11-28 15:39:57 +0000
committerGitHub <noreply@github.com>2025-11-28 15:39:57 +0000
commit2619f4cfefdf9264a7f4c3a8741493323fdde901 (patch)
tree570264ff512525bfff7f67933bbea7b7c288b60e /apps/web/components
parent9ed338fe81a7f225fdffb0f09d0fa8fc6bcf815e (diff)
downloadkarakeep-2619f4cfefdf9264a7f4c3a8741493323fdde901.tar.zst
fix: separate shared lists in the sidebar (#2180)
* fix: separate shared lists in the sidebar * fix sub * i18n
Diffstat (limited to 'apps/web/components')
-rw-r--r--apps/web/components/dashboard/lists/AllListsView.tsx74
-rw-r--r--apps/web/components/dashboard/lists/CollapsibleBookmarkLists.tsx56
-rw-r--r--apps/web/components/dashboard/sidebar/AllLists.tsx137
3 files changed, 236 insertions, 31 deletions
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 (
<ul>
<EditListModal>
@@ -84,21 +107,54 @@ export default function AllListsView({
icon="🗄️"
path={`/dashboard/archive`}
/>
+
+ {/* Owned Lists */}
<CollapsibleBookmarkLists
- initialData={initialData}
- render={({ item, level, open }) => (
+ listsData={lists}
+ filter={(node) => node.item.userRole === "owner"}
+ render={({ node, level, open }) => (
<ListItem
- key={item.item.id}
- name={item.item.name}
- icon={item.item.icon}
- list={item.item}
- path={`/dashboard/lists/${item.item.id}`}
- collapsible={item.children.length > 0}
+ name={node.item.name}
+ icon={node.item.icon}
+ list={node.item}
+ path={`/dashboard/lists/${node.item.id}`}
+ collapsible={node.children.length > 0}
open={open}
style={{ marginLeft: `${level * 1}rem` }}
/>
)}
/>
+
+ {/* Shared Lists */}
+ {hasSharedLists && (
+ <Collapsible open={sharedListsOpen} onOpenChange={setSharedListsOpen}>
+ <ListItem
+ collapsible={true}
+ name={t("lists.shared_lists")}
+ icon="👥"
+ path="#"
+ open={sharedListsOpen}
+ />
+ <CollapsibleContent>
+ <CollapsibleBookmarkLists
+ listsData={lists}
+ filter={(node) => node.item.userRole !== "owner"}
+ indentOffset={1}
+ render={({ node, level, open }) => (
+ <ListItem
+ name={node.item.name}
+ icon={node.item.icon}
+ list={node.item}
+ path={`/dashboard/lists/${node.item.id}`}
+ collapsible={node.children.length > 0}
+ open={open}
+ style={{ marginLeft: `${level * 1}rem` }}
+ />
+ )}
+ />
+ </CollapsibleContent>
+ </Collapsible>
+ )}
</ul>
);
}
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<string, number>;
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 (
<Collapsible open={open} onOpenChange={setOpen} className={className}>
{render({
- item: node,
- level,
+ node,
+ level: level + indentOffset,
open,
- numBookmarks: listStats?.stats.get(node.item.id),
+ onOpenChange: setOpen,
+ numBookmarks: listStats?.get(node.item.id),
})}
<CollapsibleContent>
{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<string, ZBookmarkListTreeNode>;
+ 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 <FullPageSpinner />;
}
- const { root } = data;
+ const rootNodes = Object.values(data.root);
+ const filteredRoots = filter ? rootNodes.filter(filter) : rootNodes;
return (
<div>
- {Object.values(root)
+ {filteredRoots
.sort((a, b) => a.item.name.localeCompare(b.item.name))
- .map((l) => (
+ .map((node) => (
<ListItem
- key={l.item.id}
- node={l}
+ key={node.item.id}
+ node={node}
render={render}
level={0}
+ indentOffset={indentOffset}
className={className}
+ listStats={listStats?.stats}
isOpenFunc={isOpenFunc ?? (() => 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<string | null>(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 (
<ul className="max-h-full gap-y-2 overflow-auto text-sm">
<li className="flex justify-between pb-3">
@@ -60,10 +98,13 @@ export default function AllLists({
linkClassName="py-0.5"
className="px-0.5"
/>
+
+ {/* Owned Lists */}
<CollapsibleBookmarkLists
- initialData={initialData.lists}
+ listsData={lists}
+ filter={(node) => node.item.userRole === "owner"}
isOpenFunc={isNodeOpen}
- render={({ item: node, level, open, numBookmarks }) => (
+ render={({ node, level, open, numBookmarks }) => (
<SidebarItem
collapseButton={
node.children.length > 0 && (
@@ -83,8 +124,8 @@ export default function AllLists({
className="group px-0.5"
right={
<ListOptions
- onOpenChange={(open) => {
- if (open) {
+ onOpenChange={(isOpen) => {
+ if (isOpen) {
setSelectedListId(node.item.id);
} else {
setSelectedListId(null);
@@ -101,7 +142,6 @@ export default function AllLists({
: "opacity-0",
)}
/>
-
<span
className={cn(
"px-2.5 text-xs font-light text-muted-foreground opacity-100 transition-opacity duration-100 group-hover:opacity-0",
@@ -121,6 +161,89 @@ export default function AllLists({
/>
)}
/>
+
+ {/* Shared Lists */}
+ {hasSharedLists && (
+ <Collapsible open={sharedListsOpen} onOpenChange={setSharedListsOpen}>
+ <SidebarItem
+ collapseButton={
+ <CollapsibleTriggerTriangle
+ className="absolute left-0.5 top-1/2 size-2 -translate-y-1/2"
+ open={sharedListsOpen}
+ />
+ }
+ logo={<span className="text-lg">👥</span>}
+ name={t("lists.shared_lists")}
+ path="#"
+ linkClassName="py-0.5"
+ className="px-0.5"
+ />
+ <CollapsibleContent>
+ <CollapsibleBookmarkLists
+ listsData={lists}
+ filter={(node) => node.item.userRole !== "owner"}
+ isOpenFunc={isNodeOpen}
+ indentOffset={1}
+ render={({ node, level, open, numBookmarks }) => (
+ <SidebarItem
+ collapseButton={
+ node.children.length > 0 && (
+ <CollapsibleTriggerTriangle
+ className="absolute left-0.5 top-1/2 size-2 -translate-y-1/2"
+ open={open}
+ />
+ )
+ }
+ logo={
+ <span className="flex">
+ <span className="text-lg"> {node.item.icon}</span>
+ </span>
+ }
+ name={node.item.name}
+ path={`/dashboard/lists/${node.item.id}`}
+ className="group px-0.5"
+ right={
+ <ListOptions
+ onOpenChange={(isOpen) => {
+ if (isOpen) {
+ setSelectedListId(node.item.id);
+ } else {
+ setSelectedListId(null);
+ }
+ }}
+ list={node.item}
+ >
+ <Button size="none" variant="ghost" className="relative">
+ <MoreHorizontal
+ className={cn(
+ "absolute inset-0 m-auto size-4 opacity-0 transition-opacity duration-100 group-hover:opacity-100",
+ selectedListId == node.item.id
+ ? "opacity-100"
+ : "opacity-0",
+ )}
+ />
+ <span
+ className={cn(
+ "px-2.5 text-xs font-light text-muted-foreground opacity-100 transition-opacity duration-100 group-hover:opacity-0",
+ selectedListId == node.item.id ||
+ numBookmarks === undefined
+ ? "opacity-0"
+ : "opacity-100",
+ )}
+ >
+ {numBookmarks}
+ </span>
+ </Button>
+ </ListOptions>
+ }
+ linkClassName="py-0.5"
+ style={{ marginLeft: `${level * 1}rem` }}
+ />
+ )}
+ />
+ </CollapsibleContent>
+ </Collapsible>
+ )}
</ul>
);
}