Skip to main content
Glama

Karakeep MCP server

by karakeep-app
BulkBookmarksAction.tsx9.55 kB
"use client"; import React, { useEffect, useState } from "react"; import { usePathname } from "next/navigation"; import { ActionButton, ActionButtonWithTooltip, } from "@/components/ui/action-button"; import ActionConfirmingDialog from "@/components/ui/action-confirming-dialog"; import { useToast } from "@/components/ui/use-toast"; import useBulkActionsStore from "@/lib/bulkActions"; import { useTranslation } from "@/lib/i18n/client"; import { CheckCheck, FileDown, Hash, Link, List, Pencil, RotateCw, Trash2, X, } from "lucide-react"; import { useDeleteBookmark, useRecrawlBookmark, useUpdateBookmark, } from "@karakeep/shared-react/hooks/bookmarks"; import { limitConcurrency } from "@karakeep/shared/concurrency"; import { BookmarkTypes } from "@karakeep/shared/types/bookmarks"; import BulkManageListsModal from "./bookmarks/BulkManageListsModal"; import BulkTagModal from "./bookmarks/BulkTagModal"; import { ArchivedActionIcon, FavouritedActionIcon } from "./bookmarks/icons"; const MAX_CONCURRENT_BULK_ACTIONS = 50; export default function BulkBookmarksAction() { const { t } = useTranslation(); const { selectedBookmarks, isBulkEditEnabled } = useBulkActionsStore(); const setIsBulkEditEnabled = useBulkActionsStore( (state) => state.setIsBulkEditEnabled, ); const selectAllBookmarks = useBulkActionsStore((state) => state.selectAll); const unSelectAllBookmarks = useBulkActionsStore( (state) => state.unSelectAll, ); const isEverythingSelected = useBulkActionsStore( (state) => state.isEverythingSelected, ); const { toast } = useToast(); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); const [manageListsModal, setManageListsModalOpen] = useState(false); const [bulkTagModal, setBulkTagModalOpen] = useState(false); const pathname = usePathname(); const [currentPathname, setCurrentPathname] = useState(""); // Reset bulk edit state when the route changes useEffect(() => { if (pathname !== currentPathname) { setCurrentPathname(pathname); setIsBulkEditEnabled(false); } }, [pathname, currentPathname]); const onError = () => { toast({ variant: "destructive", title: "Something went wrong", description: "There was a problem with your request.", }); }; const deleteBookmarkMutator = useDeleteBookmark({ onSuccess: () => { setIsBulkEditEnabled(false); }, onError, }); const updateBookmarkMutator = useUpdateBookmark({ onSuccess: () => { setIsBulkEditEnabled(false); }, onError, }); const recrawlBookmarkMutator = useRecrawlBookmark({ onSuccess: () => { setIsBulkEditEnabled(false); }, onError, }); interface UpdateBookmarkProps { favourited?: boolean; archived?: boolean; } const recrawlBookmarks = async (archiveFullPage: boolean) => { const links = selectedBookmarks.filter( (item) => item.content.type === BookmarkTypes.LINK, ); await Promise.all( limitConcurrency( links.map( (item) => () => recrawlBookmarkMutator.mutateAsync({ bookmarkId: item.id, archiveFullPage, }), ), MAX_CONCURRENT_BULK_ACTIONS, ), ); toast({ description: `${links.length} bookmarks will be ${archiveFullPage ? "re-crawled and archived!" : "refreshed!"}`, }); }; function isClipboardAvailable() { if (typeof window === "undefined") { return false; } return window && window.navigator && window.navigator.clipboard; } const copyLinks = async () => { if (!isClipboardAvailable()) { toast({ description: `Copying is only available over https`, }); return; } const copyString = selectedBookmarks .map((item) => { return item.content.type === BookmarkTypes.LINK && item.content.url; }) .filter(Boolean) .join("\n"); await navigator.clipboard.writeText(copyString); toast({ description: `Added ${selectedBookmarks.length} bookmark links into the clipboard!`, }); }; const updateBookmarks = async ({ favourited, archived, }: UpdateBookmarkProps) => { await Promise.all( limitConcurrency( selectedBookmarks.map( (item) => () => updateBookmarkMutator.mutateAsync({ bookmarkId: item.id, favourited, archived, }), ), MAX_CONCURRENT_BULK_ACTIONS, ), ); toast({ description: `${selectedBookmarks.length} bookmarks have been updated!`, }); }; const deleteBookmarks = async () => { await Promise.all( limitConcurrency( selectedBookmarks.map( (item) => () => deleteBookmarkMutator.mutateAsync({ bookmarkId: item.id }), ), MAX_CONCURRENT_BULK_ACTIONS, ), ); toast({ description: `${selectedBookmarks.length} bookmarks have been deleted!`, }); setIsDeleteDialogOpen(false); }; const alreadyFavourited = selectedBookmarks.length && selectedBookmarks.every((item) => item.favourited === true); const alreadyArchived = selectedBookmarks.length && selectedBookmarks.every((item) => item.archived === true); const actionList = [ { name: isClipboardAvailable() ? t("actions.copy_link") : "Copying is only available over https", icon: <Link size={18} />, action: () => copyLinks(), isPending: false, hidden: !isBulkEditEnabled, }, { name: t("actions.add_to_list"), icon: <List size={18} />, action: () => setManageListsModalOpen(true), isPending: false, hidden: !isBulkEditEnabled, }, { name: t("actions.edit_tags"), icon: <Hash size={18} />, action: () => setBulkTagModalOpen(true), isPending: false, hidden: !isBulkEditEnabled, }, { name: alreadyFavourited ? t("actions.unfavorite") : t("actions.favorite"), icon: <FavouritedActionIcon favourited={!!alreadyFavourited} size={18} />, action: () => updateBookmarks({ favourited: !alreadyFavourited }), isPending: updateBookmarkMutator.isPending, hidden: !isBulkEditEnabled, }, { name: alreadyArchived ? t("actions.unarchive") : t("actions.archive"), icon: <ArchivedActionIcon size={18} archived={!!alreadyArchived} />, action: () => updateBookmarks({ archived: !alreadyArchived }), isPending: updateBookmarkMutator.isPending, hidden: !isBulkEditEnabled, }, { name: t("actions.download_full_page_archive"), icon: <FileDown size={18} />, action: () => recrawlBookmarks(true), isPending: recrawlBookmarkMutator.isPending, hidden: !isBulkEditEnabled, }, { name: t("actions.refresh"), icon: <RotateCw size={18} />, action: () => recrawlBookmarks(false), isPending: recrawlBookmarkMutator.isPending, hidden: !isBulkEditEnabled, }, { name: t("actions.delete"), icon: <Trash2 size={18} color="red" />, action: () => setIsDeleteDialogOpen(true), hidden: !isBulkEditEnabled, }, { name: isEverythingSelected() ? t("actions.unselect_all") : t("actions.select_all"), icon: ( <p className="flex items-center gap-2"> ( <CheckCheck size={18} /> {selectedBookmarks.length} ) </p> ), action: () => isEverythingSelected() ? unSelectAllBookmarks() : selectAllBookmarks(), alwaysEnable: true, hidden: !isBulkEditEnabled, }, { name: t("actions.close_bulk_edit"), icon: <X size={18} />, action: () => setIsBulkEditEnabled(false), alwaysEnable: true, hidden: !isBulkEditEnabled, }, { name: t("actions.bulk_edit"), icon: <Pencil size={18} />, action: () => setIsBulkEditEnabled(true), alwaysEnable: true, hidden: isBulkEditEnabled, }, ]; return ( <div> <ActionConfirmingDialog open={isDeleteDialogOpen} setOpen={setIsDeleteDialogOpen} title={"Delete Bookmarks"} description={<p>Are you sure you want to delete these bookmarks?</p>} actionButton={() => ( <ActionButton type="button" variant="destructive" loading={deleteBookmarkMutator.isPending} onClick={() => deleteBookmarks()} > {t("actions.delete")} </ActionButton> )} /> <BulkManageListsModal bookmarkIds={selectedBookmarks.map((b) => b.id)} open={manageListsModal} setOpen={setManageListsModalOpen} /> <BulkTagModal bookmarkIds={selectedBookmarks.map((b) => b.id)} open={bulkTagModal} setOpen={setBulkTagModalOpen} /> <div className="flex items-center"> {actionList.map( ({ name, icon: Icon, action, isPending, hidden, alwaysEnable }) => ( <ActionButtonWithTooltip className={hidden ? "hidden" : "block"} tooltip={name} disabled={!selectedBookmarks.length && !alwaysEnable} delayDuration={100} loading={!!isPending} variant="ghost" key={name} onClick={action} > {Icon} </ActionButtonWithTooltip> ), )} </div> </div> ); }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/karakeep-app/karakeep'

If you have feedback or need assistance with the MCP directory API, please join our Discord server