feat: bulk tag editing, bulk export, and move recipes between collections

Extends the existing bulk-select infra: recipes list gets tag add/remove
and multi-recipe Markdown export; collection pages get a select mode to
move recipes to another collection or remove them from the current one.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Arnaud
2026-07-13 11:57:07 +02:00
parent 4960dfc7c6
commit 70eb565eec
14 changed files with 533 additions and 18 deletions
@@ -20,11 +20,14 @@ export function AddToCollectionDialog({
onOpenChange,
recipeIds,
onDone,
sourceCollectionId,
}: {
open: boolean;
onOpenChange: (open: boolean) => void;
recipeIds: string[];
onDone: () => void;
/** When set, this is a "move" — after adding to the target, the recipes are removed from this collection. */
sourceCollectionId?: string;
}) {
const t = useTranslations("recipe");
const [collections, setCollections] = useState<CollectionSummary[]>([]);
@@ -42,10 +45,10 @@ export function AddToCollectionDialog({
fetch("/api/v1/collections?limit=50")
.then((res) => (res.ok ? res.json() : null))
.then((data: { data: CollectionSummary[] } | null) => {
setCollections(data?.data ?? []);
setCollections((data?.data ?? []).filter((c) => c.id !== sourceCollectionId));
})
.finally(() => setLoading(false));
}, [open]);
}, [open, sourceCollectionId]);
async function addTo(collectionId: string, collectionName: string) {
setBusyId(collectionId);
@@ -56,7 +59,20 @@ export function AddToCollectionDialog({
body: JSON.stringify({ addRecipeIds: recipeIds }),
});
if (!res.ok) throw new Error();
toast.success(t("addToCollectionSuccess", { count: recipeIds.length, name: collectionName }));
if (sourceCollectionId) {
await fetch(`/api/v1/collections/${sourceCollectionId}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ removeRecipeIds: recipeIds }),
});
}
toast.success(
sourceCollectionId
? t("moveToCollectionSuccess", { count: recipeIds.length, name: collectionName })
: t("addToCollectionSuccess", { count: recipeIds.length, name: collectionName })
);
onOpenChange(false);
onDone();
} catch {
@@ -91,7 +107,9 @@ export function AddToCollectionDialog({
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<FolderPlus className="h-5 w-5 text-primary" />
{t("addToCollectionTitle", { count: recipeIds.length })}
{sourceCollectionId
? t("moveToCollectionTitle", { count: recipeIds.length })
: t("addToCollectionTitle", { count: recipeIds.length })}
</DialogTitle>
</DialogHeader>