diff --git a/src/components/workflows/NodePalette.tsx b/src/components/workflows/NodePalette.tsx index 4ffc7c4..632db56 100644 --- a/src/components/workflows/NodePalette.tsx +++ b/src/components/workflows/NodePalette.tsx @@ -27,6 +27,7 @@ const actionItems: PaletteItem[] = [ { type: "action", subType: "unfavorite", label: "Unfavorite", icon: HeartOff, color: "text-purple-500" }, { type: "action", subType: "archive", label: "Archive", icon: Archive, color: "text-purple-500" }, { type: "action", subType: "tag", label: "Add Tag", icon: Tag, color: "text-purple-500" }, + { type: "action", subType: "remove_tag", label: "Remove Tag", icon: Tag, color: "text-purple-500" }, ]; function PaletteGroup({ title, items }: { title: string; items: PaletteItem[] }) { diff --git a/src/components/workflows/config/ActionConfig.tsx b/src/components/workflows/config/ActionConfig.tsx index 09a62cc..cc15cb0 100644 --- a/src/components/workflows/config/ActionConfig.tsx +++ b/src/components/workflows/config/ActionConfig.tsx @@ -115,7 +115,7 @@ export default function ActionConfig({ subType, config, onChange }: ActionConfig ); } - if (subType === "tag") { + if (subType === "tag" || subType === "remove_tag") { return (
Removes this tag from matched assets. If the tag doesn't exist, nothing happens.
+ )}{config.albumName}
)} - {subType === "tag" && config.tagName && ( + {(subType === "tag" || subType === "remove_tag") && config.tagName && ({config.tagName}
)} diff --git a/src/lib/workflow/actionExecutor.ts b/src/lib/workflow/actionExecutor.ts index 1fd09b1..244ecb6 100644 --- a/src/lib/workflow/actionExecutor.ts +++ b/src/lib/workflow/actionExecutor.ts @@ -180,6 +180,25 @@ export async function executeAction( } } + case "remove_tag": { + if (!config.tagName) throw new Error("Tag name is required"); + try { + // Look up the tag by value (full path for nested tags) without creating it — + // removing a tag that doesn't exist is a no-op, not an error. + const result = await db.execute( + sql`SELECT id FROM "tag" WHERE "userId" = ${user.id} AND "value" = ${config.tagName} LIMIT 1` + ); + const tagId = result.rows?.[0]?.id as string | undefined; + if (!tagId) { + return { action: "remove_tag", assetsProcessed: 0 }; + } + await immichFetch(`/tags/${tagId}/assets`, "DELETE", { ids: assetIds }, user); + return { action: "remove_tag", assetsProcessed: assetIds.length }; + } catch (e: any) { + return { action: "remove_tag", assetsProcessed: 0, error: e.message }; + } + } + default: return { action: subType, assetsProcessed: 0, error: `Unknown action: ${subType}` }; } diff --git a/src/pages/workflows/[id]/runs/[runId].tsx b/src/pages/workflows/[id]/runs/[runId].tsx index 14f6e6c..eb261c8 100644 --- a/src/pages/workflows/[id]/runs/[runId].tsx +++ b/src/pages/workflows/[id]/runs/[runId].tsx @@ -135,11 +135,11 @@ function AnnotatedActionNode({ data }: NodeProps) { const step = data.runStep as any; const icons: Record