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 (
@@ -125,6 +125,9 @@ export default function ActionConfig({ subType, config, onChange }: ActionConfig value={config.tagName || ""} onChange={(e) => onChange({ ...config, tagName: e.target.value })} /> + {subType === "remove_tag" && ( +

Removes this tag from matched assets. If the tag doesn't exist, nothing happens.

+ )}
); } diff --git a/src/components/workflows/nodes/ActionNode.tsx b/src/components/workflows/nodes/ActionNode.tsx index fea2f50..08f937e 100644 --- a/src/components/workflows/nodes/ActionNode.tsx +++ b/src/components/workflows/nodes/ActionNode.tsx @@ -9,6 +9,7 @@ const actionIcons: Record = { unfavorite: HeartOff, archive: Archive, tag: Tag, + remove_tag: Tag, }; const actionLabels: Record = { @@ -19,6 +20,7 @@ const actionLabels: Record = { unfavorite: "Unfavorite", archive: "Archive", tag: "Add Tag", + remove_tag: "Remove Tag", }; export default function ActionNode({ data, selected }: NodeProps) { @@ -42,7 +44,7 @@ export default function ActionNode({ data, selected }: NodeProps) { {(subType === "add_to_album" || subType === "remove_from_album") && config.albumName && (

{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 = { create_album: FolderPlus, add_to_album: FolderInput, remove_from_album: FolderMinus, - favorite: Heart, unfavorite: HeartOff, archive: Archive, tag: Tag, + favorite: Heart, unfavorite: HeartOff, archive: Archive, tag: Tag, remove_tag: Tag, }; const labels: Record = { create_album: "Create Album", add_to_album: "Add to Album", remove_from_album: "Remove from Album", - favorite: "Favorite", unfavorite: "Unfavorite", archive: "Archive", tag: "Add Tag", + favorite: "Favorite", unfavorite: "Unfavorite", archive: "Archive", tag: "Add Tag", remove_tag: "Remove Tag", }; const Icon = icons[subType] || FolderPlus; const isDryRun = step?.detail?.includes("DRY RUN");