feat: workflow asset tag trigger/filter/action (#29043)

* feat: workflow asset tag trigger and filter

* feat(web): show tag names in workflow editor

* fix(web): tag picker in schema config editor

* fix: invalid plugin manifest

* chore: update workflow method wrapper type

* chore: update tag filter method declaration

* feat: workflow action to add tags to assets
This commit is contained in:
Ben Beckford 2026-08-12 16:26:03 -07:00 committed by GitHub
parent b82d480552
commit a939561e70
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 175 additions and 8 deletions

View file

@ -1,6 +1,7 @@
<script lang="ts">
import SchemaAlbumPicker from '$lib/components/SchemaAlbumPicker.svelte';
import Self from '$lib/components/SchemaConfiguration.svelte';
import SchemaTagPicker from '$lib/components/SchemaTagPicker.svelte';
import type { JSONSchemaProperty, SchemaConfig } from '$lib/types';
import {
CodeBlock,
@ -80,6 +81,8 @@
</div>
{:else if schema.uiHint?.type === 'AlbumId'}
<SchemaAlbumPicker {label} {description} array={schema.array} bind:albumIds={getUiHintValue, setUiHintValue} />
{:else if schema.uiHint?.type === 'TagId'}
<SchemaTagPicker bind:tagIds={getUiHintValue, setUiHintValue} />
{:else if schema.enum && schema.array}
<Field {label} {description}>
<MultiSelect options={schema.enum} bind:values={getEnum, setValue} />

View file

@ -0,0 +1,32 @@
<script lang="ts">
import Combobox from '$lib/components/shared-components/Combobox.svelte';
import TagPill from '$lib/components/shared-components/TagPill.svelte';
import { getAllTags, type TagResponseDto } from '@immich/sdk';
import { onMount } from 'svelte';
import { t } from 'svelte-i18n';
let { tagIds = $bindable([]) }: { tagIds: string[] } = $props();
let tags: TagResponseDto[] = $state([]);
onMount(async () => {
tags = await getAllTags();
});
</script>
<Combobox
onSelect={(option) => {
if (option && !tagIds.includes(option.value)) {
tagIds.push(option.value);
}
}}
label={$t('tags')}
defaultFirstOption
options={tags.map((tag) => ({ label: tag.value, value: tag.id }))}
placeholder={$t('search_tags')}
/>
<section class="flex flex-wrap gap-1 pt-2">
{#each tagIds as id, index (id)}
<TagPill label={tags.find((t) => t.id === id)?.name ?? id} onRemove={() => tagIds.splice(index, 1)} />
{/each}
</section>

View file

@ -100,7 +100,7 @@ export type JSONSchemaProperty = {
properties?: Record<string, JSONSchemaProperty>;
required?: string[];
uiHint?: {
type?: 'AlbumId' | 'AssetId' | 'PersonId';
type?: 'AlbumId' | 'AssetId' | 'PersonId' | 'TagId';
order?: number;
};
};

View file

@ -13,6 +13,9 @@ export const getTriggerName = ($t: MessageFormatter, type: WorkflowTrigger) => {
case WorkflowTrigger.AssetMetadataExtraction: {
return $t('trigger_asset_metadata_extraction');
}
case WorkflowTrigger.AssetTagged: {
return $t('trigger_asset_tagged');
}
default: {
return type;
}
@ -30,6 +33,12 @@ export const getTriggerDescription = ($t: MessageFormatter, type: WorkflowTrigge
case WorkflowTrigger.AssetMetadataExtraction: {
return $t('trigger_asset_metadata_extraction_description');
}
case WorkflowTrigger.AssetTagged: {
return $t('trigger_asset_tagged_description');
}
default: {
return type;
}
}
};

View file

@ -1,6 +1,6 @@
<script module lang="ts">
import { authManager } from '$lib/managers/auth-manager.svelte';
import { getAlbumInfo } from '@immich/sdk';
import { getAlbumInfo, getTagById } from '@immich/sdk';
// eslint-disable-next-line svelte/prefer-svelte-reactivity
const albumNameCache = new Map<string, Promise<string>>();
@ -239,6 +239,16 @@
{@render badge($t('album'), `"${truncate(albumName)}"`)}
{/await}
{/each}
{:else if getUiHint(key) === 'TagId'}
{#each toIds(value) as tagId (tagId)}
{#await getTagById({ id: tagId })}
{@render badge($t('tag'), '…')}
{:then tag}
{@render badge($t('tag'), `"${truncate(tag.name)}"`)}
{:catch}
{@render badge($t('tag'), `"${truncate(tagId)}"`)}
{/await}
{/each}
{:else}
{@render badge(key, formatConfigValue(value))}
{/if}