diff options
Diffstat (limited to 'apps/web/components/dashboard/bookmarks')
6 files changed, 231 insertions, 155 deletions
diff --git a/apps/web/components/dashboard/bookmarks/BookmarkLayoutAdaptingCard.tsx b/apps/web/components/dashboard/bookmarks/BookmarkLayoutAdaptingCard.tsx index 98babb22..e8520b1a 100644 --- a/apps/web/components/dashboard/bookmarks/BookmarkLayoutAdaptingCard.tsx +++ b/apps/web/components/dashboard/bookmarks/BookmarkLayoutAdaptingCard.tsx @@ -13,6 +13,7 @@ import { } from "@/lib/userLocalSettings/bookmarksLayout"; import { cn } from "@/lib/utils"; import { Check, Image as ImageIcon, NotebookPen } from "lucide-react"; +import { useSession } from "next-auth/react"; import { useTheme } from "next-themes"; import type { ZBookmark } from "@karakeep/shared/types/bookmarks"; @@ -64,12 +65,15 @@ function MultiBookmarkSelector({ bookmark }: { bookmark: ZBookmark }) { const toggleBookmark = useBulkActionsStore((state) => state.toggleBookmark); const [isSelected, setIsSelected] = useState(false); const { theme } = useTheme(); + const { data: session } = useSession(); useEffect(() => { setIsSelected(selectedBookmarks.some((item) => item.id === bookmark.id)); }, [selectedBookmarks]); - if (!isBulkEditEnabled) return null; + // Don't show selector for non-owned bookmarks or when bulk edit is disabled + const isOwner = session?.user?.id === bookmark.userId; + if (!isBulkEditEnabled || !isOwner) return null; const getIconColor = () => { if (theme === "dark") { diff --git a/apps/web/components/dashboard/bookmarks/BookmarkOptions.tsx b/apps/web/components/dashboard/bookmarks/BookmarkOptions.tsx index 4725c77f..66de6156 100644 --- a/apps/web/components/dashboard/bookmarks/BookmarkOptions.tsx +++ b/apps/web/components/dashboard/bookmarks/BookmarkOptions.tsx @@ -22,6 +22,7 @@ import { SquarePen, Trash2, } from "lucide-react"; +import { useSession } from "next-auth/react"; import type { ZBookmark, @@ -46,9 +47,13 @@ export default function BookmarkOptions({ bookmark }: { bookmark: ZBookmark }) { const { t } = useTranslation(); const { toast } = useToast(); const linkId = bookmark.id; + const { data: session } = useSession(); const demoMode = !!useClientConfig().demoMode; + // Check if the current user owns this bookmark + const isOwner = session?.user?.id === bookmark.userId; + const [isClipboardAvailable, setIsClipboardAvailable] = useState(false); useEffect(() => { @@ -114,6 +119,142 @@ export default function BookmarkOptions({ bookmark }: { bookmark: ZBookmark }) { onError, }); + // Define action items array + const actionItems = [ + { + id: "edit", + title: t("actions.edit"), + icon: <Pencil className="mr-2 size-4" />, + visible: isOwner, + disabled: false, + onClick: () => setEditBookmarkDialogOpen(true), + }, + { + id: "open-editor", + title: t("actions.open_editor"), + icon: <SquarePen className="mr-2 size-4" />, + visible: isOwner && bookmark.content.type === BookmarkTypes.TEXT, + disabled: false, + onClick: () => setTextEditorOpen(true), + }, + { + id: "favorite", + title: bookmark.favourited + ? t("actions.unfavorite") + : t("actions.favorite"), + icon: ( + <FavouritedActionIcon + className="mr-2 size-4" + favourited={bookmark.favourited} + /> + ), + visible: isOwner, + disabled: demoMode, + onClick: () => + updateBookmarkMutator.mutate({ + bookmarkId: linkId, + favourited: !bookmark.favourited, + }), + }, + { + id: "archive", + title: bookmark.archived ? t("actions.unarchive") : t("actions.archive"), + icon: ( + <ArchivedActionIcon + className="mr-2 size-4" + archived={bookmark.archived} + /> + ), + visible: isOwner, + disabled: demoMode, + onClick: () => + updateBookmarkMutator.mutate({ + bookmarkId: linkId, + archived: !bookmark.archived, + }), + }, + { + id: "download-full-page", + title: t("actions.download_full_page_archive"), + icon: <FileDown className="mr-2 size-4" />, + visible: isOwner && bookmark.content.type === BookmarkTypes.LINK, + disabled: false, + onClick: () => { + fullPageArchiveBookmarkMutator.mutate({ + bookmarkId: bookmark.id, + archiveFullPage: true, + }); + }, + }, + { + id: "copy-link", + title: t("actions.copy_link"), + icon: <Link className="mr-2 size-4" />, + visible: bookmark.content.type === BookmarkTypes.LINK, + disabled: !isClipboardAvailable, + onClick: () => { + navigator.clipboard.writeText( + (bookmark.content as ZBookmarkedLink).url, + ); + toast({ + description: t("toasts.bookmarks.clipboard_copied"), + }); + }, + }, + { + id: "manage-lists", + title: t("actions.manage_lists"), + icon: <List className="mr-2 size-4" />, + visible: isOwner, + disabled: false, + onClick: () => setManageListsModalOpen(true), + }, + { + id: "remove-from-list", + title: t("actions.remove_from_list"), + icon: <ListX className="mr-2 size-4" />, + visible: + (isOwner || + (withinListContext && + (withinListContext.userRole === "editor" || + withinListContext.userRole === "owner"))) && + !!listId && + !!withinListContext && + withinListContext.type === "manual", + disabled: demoMode, + onClick: () => + removeFromListMutator.mutate({ + listId: listId!, + bookmarkId: bookmark.id, + }), + }, + { + id: "refresh", + title: t("actions.refresh"), + icon: <RotateCw className="mr-2 size-4" />, + visible: isOwner && bookmark.content.type === BookmarkTypes.LINK, + disabled: demoMode, + onClick: () => crawlBookmarkMutator.mutate({ bookmarkId: bookmark.id }), + }, + { + id: "delete", + title: t("actions.delete"), + icon: <Trash2 className="mr-2 size-4" />, + visible: isOwner, + disabled: demoMode, + className: "text-destructive", + onClick: () => setDeleteBookmarkDialogOpen(true), + }, + ]; + + // Filter visible items + const visibleItems = actionItems.filter((item) => item.visible); + + // If no items are visible, don't render the dropdown + if (visibleItems.length === 0) { + return null; + } + return ( <> {manageListsModal} @@ -142,127 +283,17 @@ export default function BookmarkOptions({ bookmark }: { bookmark: ZBookmark }) { </Button> </DropdownMenuTrigger> <DropdownMenuContent className="w-fit"> - <DropdownMenuItem onClick={() => setEditBookmarkDialogOpen(true)}> - <Pencil className="mr-2 size-4" /> - <span>{t("actions.edit")}</span> - </DropdownMenuItem> - {bookmark.content.type === BookmarkTypes.TEXT && ( - <DropdownMenuItem onClick={() => setTextEditorOpen(true)}> - <SquarePen className="mr-2 size-4" /> - <span>{t("actions.open_editor")}</span> - </DropdownMenuItem> - )} - <DropdownMenuItem - disabled={demoMode} - onClick={() => - updateBookmarkMutator.mutate({ - bookmarkId: linkId, - favourited: !bookmark.favourited, - }) - } - > - <FavouritedActionIcon - className="mr-2 size-4" - favourited={bookmark.favourited} - /> - <span> - {bookmark.favourited - ? t("actions.unfavorite") - : t("actions.favorite")} - </span> - </DropdownMenuItem> - <DropdownMenuItem - disabled={demoMode} - onClick={() => - updateBookmarkMutator.mutate({ - bookmarkId: linkId, - archived: !bookmark.archived, - }) - } - > - <ArchivedActionIcon - className="mr-2 size-4" - archived={bookmark.archived} - /> - <span> - {bookmark.archived - ? t("actions.unarchive") - : t("actions.archive")} - </span> - </DropdownMenuItem> - - {bookmark.content.type === BookmarkTypes.LINK && ( - <DropdownMenuItem - onClick={() => { - fullPageArchiveBookmarkMutator.mutate({ - bookmarkId: bookmark.id, - archiveFullPage: true, - }); - }} - > - <FileDown className="mr-2 size-4" /> - <span>{t("actions.download_full_page_archive")}</span> - </DropdownMenuItem> - )} - - {bookmark.content.type === BookmarkTypes.LINK && ( + {visibleItems.map((item) => ( <DropdownMenuItem - disabled={!isClipboardAvailable} - onClick={() => { - navigator.clipboard.writeText( - (bookmark.content as ZBookmarkedLink).url, - ); - toast({ - description: t("toasts.bookmarks.clipboard_copied"), - }); - }} + key={item.id} + disabled={item.disabled} + className={item.className} + onClick={item.onClick} > - <Link className="mr-2 size-4" /> - <span>{t("actions.copy_link")}</span> + {item.icon} + <span>{item.title}</span> </DropdownMenuItem> - )} - - <DropdownMenuItem onClick={() => setManageListsModalOpen(true)}> - <List className="mr-2 size-4" /> - <span>{t("actions.manage_lists")}</span> - </DropdownMenuItem> - - {listId && - withinListContext && - withinListContext.type === "manual" && ( - <DropdownMenuItem - disabled={demoMode} - onClick={() => - removeFromListMutator.mutate({ - listId, - bookmarkId: bookmark.id, - }) - } - > - <ListX className="mr-2 size-4" /> - <span>{t("actions.remove_from_list")}</span> - </DropdownMenuItem> - )} - - {bookmark.content.type === BookmarkTypes.LINK && ( - <DropdownMenuItem - disabled={demoMode} - onClick={() => - crawlBookmarkMutator.mutate({ bookmarkId: bookmark.id }) - } - > - <RotateCw className="mr-2 size-4" /> - <span>{t("actions.refresh")}</span> - </DropdownMenuItem> - )} - <DropdownMenuItem - disabled={demoMode} - className="text-destructive" - onClick={() => setDeleteBookmarkDialogOpen(true)} - > - <Trash2 className="mr-2 size-4" /> - <span>{t("actions.delete")}</span> - </DropdownMenuItem> + ))} </DropdownMenuContent> </DropdownMenu> </> diff --git a/apps/web/components/dashboard/bookmarks/BookmarkTagsEditor.tsx b/apps/web/components/dashboard/bookmarks/BookmarkTagsEditor.tsx index fa4f40de..22b5408e 100644 --- a/apps/web/components/dashboard/bookmarks/BookmarkTagsEditor.tsx +++ b/apps/web/components/dashboard/bookmarks/BookmarkTagsEditor.tsx @@ -5,7 +5,13 @@ import { useUpdateBookmarkTags } from "@karakeep/shared-react/hooks/bookmarks"; import { TagsEditor } from "./TagsEditor"; -export function BookmarkTagsEditor({ bookmark }: { bookmark: ZBookmark }) { +export function BookmarkTagsEditor({ + bookmark, + disabled, +}: { + bookmark: ZBookmark; + disabled?: boolean; +}) { const { mutate } = useUpdateBookmarkTags({ onSuccess: () => { toast({ @@ -24,6 +30,7 @@ export function BookmarkTagsEditor({ bookmark }: { bookmark: ZBookmark }) { return ( <TagsEditor tags={bookmark.tags} + disabled={disabled} onAttach={({ tagName, tagId }) => { mutate({ bookmarkId: bookmark.id, diff --git a/apps/web/components/dashboard/bookmarks/SummarizeBookmarkArea.tsx b/apps/web/components/dashboard/bookmarks/SummarizeBookmarkArea.tsx index e75886e1..b2cf118e 100644 --- a/apps/web/components/dashboard/bookmarks/SummarizeBookmarkArea.tsx +++ b/apps/web/components/dashboard/bookmarks/SummarizeBookmarkArea.tsx @@ -17,9 +17,11 @@ import { BookmarkTypes, ZBookmark } from "@karakeep/shared/types/bookmarks"; function AISummary({ bookmarkId, summary, + readOnly = false, }: { bookmarkId: string; summary: string; + readOnly?: boolean; }) { const [isExpanded, setIsExpanded] = React.useState(false); const { mutate: resummarize, isPending: isResummarizing } = @@ -60,28 +62,34 @@ function AISummary({ </MarkdownReadonly> {isExpanded && ( <span className="flex justify-end gap-2 pt-2"> - <ActionButton - variant="none" - size="none" - spinner={<LoadingSpinner className="size-4" />} - className="rounded-full bg-gray-200 p-1 text-gray-600 dark:bg-gray-700 dark:text-gray-400" - aria-label={isExpanded ? "Collapse" : "Expand"} - loading={isResummarizing} - onClick={() => resummarize({ bookmarkId })} - > - <RefreshCw size={16} /> - </ActionButton> - <ActionButton - size="none" - variant="none" - spinner={<LoadingSpinner className="size-4" />} - className="rounded-full bg-gray-200 p-1 text-gray-600 dark:bg-gray-700 dark:text-gray-400" - aria-label={isExpanded ? "Collapse" : "Expand"} - loading={isUpdatingBookmark} - onClick={() => updateBookmark({ bookmarkId, summary: null })} - > - <Trash2 size={16} /> - </ActionButton> + {!readOnly && ( + <> + <ActionButton + variant="none" + size="none" + spinner={<LoadingSpinner className="size-4" />} + className="rounded-full bg-gray-200 p-1 text-gray-600 dark:bg-gray-700 dark:text-gray-400" + aria-label={isExpanded ? "Collapse" : "Expand"} + loading={isResummarizing} + onClick={() => resummarize({ bookmarkId })} + > + <RefreshCw size={16} /> + </ActionButton> + <ActionButton + size="none" + variant="none" + spinner={<LoadingSpinner className="size-4" />} + className="rounded-full bg-gray-200 p-1 text-gray-600 dark:bg-gray-700 dark:text-gray-400" + aria-label={isExpanded ? "Collapse" : "Expand"} + loading={isUpdatingBookmark} + onClick={() => + updateBookmark({ bookmarkId, summary: null }) + } + > + <Trash2 size={16} /> + </ActionButton> + </> + )} <button className="rounded-full bg-gray-200 p-1 text-gray-600 dark:bg-gray-700 dark:text-gray-400" aria-label="Collapse" @@ -99,8 +107,10 @@ function AISummary({ export default function SummarizeBookmarkArea({ bookmark, + readOnly = false, }: { bookmark: ZBookmark; + readOnly?: boolean; }) { const { t } = useTranslation(); const { mutate, isPending } = useSummarizeBookmark({ @@ -118,8 +128,14 @@ export default function SummarizeBookmarkArea({ } if (bookmark.summary) { - return <AISummary bookmarkId={bookmark.id} summary={bookmark.summary} />; - } else if (!clientConfig.inference.isConfigured) { + return ( + <AISummary + bookmarkId={bookmark.id} + summary={bookmark.summary} + readOnly={readOnly} + /> + ); + } else if (!clientConfig.inference.isConfigured || readOnly) { return null; } else { return ( diff --git a/apps/web/components/dashboard/bookmarks/TagList.tsx b/apps/web/components/dashboard/bookmarks/TagList.tsx index 593a269b..f1c319ea 100644 --- a/apps/web/components/dashboard/bookmarks/TagList.tsx +++ b/apps/web/components/dashboard/bookmarks/TagList.tsx @@ -2,6 +2,7 @@ import Link from "next/link"; import { badgeVariants } from "@/components/ui/badge"; import { Skeleton } from "@/components/ui/skeleton"; import { cn } from "@/lib/utils"; +import { useSession } from "next-auth/react"; import type { ZBookmark } from "@karakeep/shared/types/bookmarks"; @@ -14,6 +15,9 @@ export default function TagList({ loading?: boolean; className?: string; }) { + const { data: session } = useSession(); + const isOwner = session?.user?.id === bookmark.userId; + if (loading) { return ( <div className="flex w-full flex-col justify-end space-y-2 p-2"> @@ -26,16 +30,28 @@ export default function TagList({ <> {bookmark.tags.map((t) => ( <div key={t.id} className={className}> - <Link - key={t.id} - className={cn( - badgeVariants({ variant: "secondary" }), - "text-nowrap font-light text-gray-700 hover:bg-foreground hover:text-secondary dark:text-gray-400", - )} - href={`/dashboard/tags/${t.id}`} - > - {t.name} - </Link> + {isOwner ? ( + <Link + key={t.id} + className={cn( + badgeVariants({ variant: "secondary" }), + "text-nowrap font-light text-gray-700 hover:bg-foreground hover:text-secondary dark:text-gray-400", + )} + href={`/dashboard/tags/${t.id}`} + > + {t.name} + </Link> + ) : ( + <span + key={t.id} + className={cn( + badgeVariants({ variant: "secondary" }), + "text-nowrap font-light text-gray-700 dark:text-gray-400", + )} + > + {t.name} + </span> + )} </div> ))} </> diff --git a/apps/web/components/dashboard/bookmarks/TagsEditor.tsx b/apps/web/components/dashboard/bookmarks/TagsEditor.tsx index 512fa990..bc06c647 100644 --- a/apps/web/components/dashboard/bookmarks/TagsEditor.tsx +++ b/apps/web/components/dashboard/bookmarks/TagsEditor.tsx @@ -25,13 +25,15 @@ export function TagsEditor({ tags: _tags, onAttach, onDetach, + disabled, }: { tags: ZBookmarkTags[]; onAttach: (tag: { tagName: string; tagId?: string }) => void; onDetach: (tag: { tagName: string; tagId: string }) => void; + disabled?: boolean; }) { const demoMode = !!useClientConfig().demoMode; - const isDisabled = demoMode; + const isDisabled = demoMode || disabled; const inputRef = React.useRef<HTMLInputElement>(null); const containerRef = React.useRef<HTMLDivElement>(null); const [open, setOpen] = React.useState(false); |
