From 7a76216e5c971a300e9db32c93509b0376f6f47e Mon Sep 17 00:00:00 2001 From: Mohamed Bassem Date: Tue, 30 Dec 2025 13:30:35 +0200 Subject: feat: Add bulk remove from list (#2279) * feat: Add bulk remove from list action in list context - Add "Remove from List" button in bulk actions menu - Only visible when in a manual list context with editor/owner role - Includes confirmation dialog before removal - Uses same concurrency pattern as bulk add (50 concurrent operations) - Displays success count in toast notification - Add translation key "actions.remove" for consistency This complements the existing bulk add to list functionality and allows users to efficiently remove multiple bookmarks from a list at once. * fmt * fix list context * add remove from list --------- Co-authored-by: Claude --- .../components/dashboard/BulkBookmarksAction.tsx | 75 +++++++++++++++++++++- 1 file changed, 74 insertions(+), 1 deletion(-) (limited to 'apps/web/components/dashboard/BulkBookmarksAction.tsx') diff --git a/apps/web/components/dashboard/BulkBookmarksAction.tsx b/apps/web/components/dashboard/BulkBookmarksAction.tsx index 9e248c03..bad76ff9 100644 --- a/apps/web/components/dashboard/BulkBookmarksAction.tsx +++ b/apps/web/components/dashboard/BulkBookmarksAction.tsx @@ -16,6 +16,7 @@ import { Hash, Link, List, + ListMinus, Pencil, RotateCw, Trash2, @@ -27,6 +28,7 @@ import { useRecrawlBookmark, useUpdateBookmark, } from "@karakeep/shared-react/hooks/bookmarks"; +import { useRemoveBookmarkFromList } from "@karakeep/shared-react/hooks/lists"; import { limitConcurrency } from "@karakeep/shared/concurrency"; import { BookmarkTypes } from "@karakeep/shared/types/bookmarks"; @@ -38,7 +40,11 @@ const MAX_CONCURRENT_BULK_ACTIONS = 50; export default function BulkBookmarksAction() { const { t } = useTranslation(); - const { selectedBookmarks, isBulkEditEnabled } = useBulkActionsStore(); + const { + selectedBookmarks, + isBulkEditEnabled, + listContext: withinListContext, + } = useBulkActionsStore(); const setIsBulkEditEnabled = useBulkActionsStore( (state) => state.setIsBulkEditEnabled, ); @@ -50,6 +56,8 @@ export default function BulkBookmarksAction() { (state) => state.isEverythingSelected, ); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); + const [isRemoveFromListDialogOpen, setIsRemoveFromListDialogOpen] = + useState(false); const [manageListsModal, setManageListsModalOpen] = useState(false); const [bulkTagModal, setBulkTagModalOpen] = useState(false); const pathname = usePathname(); @@ -92,6 +100,13 @@ export default function BulkBookmarksAction() { onError, }); + const removeBookmarkFromListMutator = useRemoveBookmarkFromList({ + onSuccess: () => { + setIsBulkEditEnabled(false); + }, + onError, + }); + interface UpdateBookmarkProps { favourited?: boolean; archived?: boolean; @@ -184,6 +199,31 @@ export default function BulkBookmarksAction() { setIsDeleteDialogOpen(false); }; + const removeBookmarksFromList = async () => { + if (!withinListContext) return; + + const results = await Promise.allSettled( + limitConcurrency( + selectedBookmarks.map( + (item) => () => + removeBookmarkFromListMutator.mutateAsync({ + bookmarkId: item.id, + listId: withinListContext.id, + }), + ), + MAX_CONCURRENT_BULK_ACTIONS, + ), + ); + + const successes = results.filter((r) => r.status === "fulfilled").length; + if (successes > 0) { + toast({ + description: `${successes} bookmarks have been removed from the list!`, + }); + } + setIsRemoveFromListDialogOpen(false); + }; + const alreadyFavourited = selectedBookmarks.length && selectedBookmarks.every((item) => item.favourited === true); @@ -202,6 +242,18 @@ export default function BulkBookmarksAction() { isPending: false, hidden: !isBulkEditEnabled, }, + { + name: t("actions.remove_from_list"), + icon: , + action: () => setIsRemoveFromListDialogOpen(true), + isPending: removeBookmarkFromListMutator.isPending, + hidden: + !isBulkEditEnabled || + !withinListContext || + withinListContext.type !== "manual" || + (withinListContext.userRole !== "editor" && + withinListContext.userRole !== "owner"), + }, { name: t("actions.add_to_list"), icon: , @@ -298,6 +350,27 @@ export default function BulkBookmarksAction() { )} /> + + Are you sure you want to remove {selectedBookmarks.length} bookmarks + from this list? +

+ } + actionButton={() => ( + removeBookmarksFromList()} + > + {t("actions.remove")} + + )} + /> b.id)} open={manageListsModal} -- cgit v1.2.3-70-g09d2