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:
@@ -2139,6 +2139,8 @@
|
|||||||
"trigger": "Trigger",
|
"trigger": "Trigger",
|
||||||
"trigger_asset_metadata_extraction": "Asset Metadata Extraction",
|
"trigger_asset_metadata_extraction": "Asset Metadata Extraction",
|
||||||
"trigger_asset_metadata_extraction_description": "Triggered when the EXIF metadata of an asset is extracted",
|
"trigger_asset_metadata_extraction_description": "Triggered when the EXIF metadata of an asset is extracted",
|
||||||
|
"trigger_asset_tagged": "Asset Tagged",
|
||||||
|
"trigger_asset_tagged_description": "Triggered when a tag is added to an asset",
|
||||||
"trigger_asset_uploaded": "Asset Upload",
|
"trigger_asset_uploaded": "Asset Upload",
|
||||||
"trigger_asset_uploaded_description": "Triggered when a new asset is uploaded",
|
"trigger_asset_uploaded_description": "Triggered when a new asset is uploaded",
|
||||||
"trigger_person_recognized": "Person Recognized",
|
"trigger_person_recognized": "Person Recognized",
|
||||||
|
|||||||
@@ -28066,7 +28066,8 @@
|
|||||||
"description": "Plugin trigger type",
|
"description": "Plugin trigger type",
|
||||||
"enum": [
|
"enum": [
|
||||||
"AssetCreate",
|
"AssetCreate",
|
||||||
"AssetMetadataExtraction"
|
"AssetMetadataExtraction",
|
||||||
|
"AssetTagged"
|
||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -308,6 +308,56 @@
|
|||||||
},
|
},
|
||||||
"uiHints": ["Filter"]
|
"uiHints": ["Filter"]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "assetAddTags",
|
||||||
|
"title": "Add Tags",
|
||||||
|
"description": "Add tags to an asset",
|
||||||
|
"types": ["AssetV1"],
|
||||||
|
"hostFunctions": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"tags": {
|
||||||
|
"title": "Tags",
|
||||||
|
"description": "Tags to add to the asset",
|
||||||
|
"type": "string",
|
||||||
|
"array": true,
|
||||||
|
"uiHint": {
|
||||||
|
"type": "TagId"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["tags"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "assetTagFilter",
|
||||||
|
"title": "Filter by tags",
|
||||||
|
"description": "Filter assets by which tags they have",
|
||||||
|
"types": ["AssetV1"],
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"tags": {
|
||||||
|
"title": "Tags",
|
||||||
|
"description": "Which tags the asset must have",
|
||||||
|
"type": "string",
|
||||||
|
"array": true,
|
||||||
|
"uiHint": {
|
||||||
|
"type": "TagId"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"matching": {
|
||||||
|
"title": "Matching",
|
||||||
|
"description": "Whether assets must have all, any, or none of the listed tags",
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["all", "any", "none"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["tags", "matching"]
|
||||||
|
},
|
||||||
|
"uiHints": ["Filter"]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "assetExifFilter",
|
"name": "assetExifFilter",
|
||||||
"title": "Filter by EXIF metadata",
|
"title": "Filter by EXIF metadata",
|
||||||
|
|||||||
@@ -39,6 +39,11 @@ const matchValueResult = (value: string, config: MatchValueConfig) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const methods = wrapper<Manifest>({
|
const methods = wrapper<Manifest>({
|
||||||
|
assetAddTags: ({ config, data, functions }) => {
|
||||||
|
functions.bulkTagAssets({ assetIds: [data.asset.id], tagIds: config.tags });
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
|
||||||
assetAddToAlbums: ({ config, data, functions }) => {
|
assetAddToAlbums: ({ config, data, functions }) => {
|
||||||
const assetId = data.asset.id;
|
const assetId = data.asset.id;
|
||||||
|
|
||||||
@@ -176,6 +181,24 @@ const methods = wrapper<Manifest>({
|
|||||||
return { workflow: { continue: hasTimeZone === needsTimeZone } };
|
return { workflow: { continue: hasTimeZone === needsTimeZone } };
|
||||||
},
|
},
|
||||||
|
|
||||||
|
assetTagFilter: ({ config, data }) => {
|
||||||
|
const assetTags = data.asset.tags.map((tag) => tag.id);
|
||||||
|
|
||||||
|
for (const tag of config.tags) {
|
||||||
|
if (assetTags.includes(tag)) {
|
||||||
|
if (config.matching === 'any') {
|
||||||
|
break;
|
||||||
|
} else if (config.matching === 'none') {
|
||||||
|
return { workflow: { continue: false } };
|
||||||
|
}
|
||||||
|
} else if (config.matching === 'all') {
|
||||||
|
return { workflow: { continue: false } };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { workflow: { continue: true } };
|
||||||
|
},
|
||||||
|
|
||||||
assetTypeFilter: ({ config, data }) => {
|
assetTypeFilter: ({ config, data }) => {
|
||||||
return { workflow: { continue: config.allowedTypes.includes(data.asset.type) } };
|
return { workflow: { continue: config.allowedTypes.includes(data.asset.type) } };
|
||||||
},
|
},
|
||||||
@@ -208,6 +231,7 @@ const methods = wrapper<Manifest>({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
assetAddTags,
|
||||||
assetAddToAlbums,
|
assetAddToAlbums,
|
||||||
assetArchive,
|
assetArchive,
|
||||||
assetFavorite,
|
assetFavorite,
|
||||||
@@ -217,6 +241,7 @@ const {
|
|||||||
assetDateFilter,
|
assetDateFilter,
|
||||||
assetLock,
|
assetLock,
|
||||||
assetMissingTimeZoneFilter,
|
assetMissingTimeZoneFilter,
|
||||||
|
assetTagFilter,
|
||||||
assetTypeFilter,
|
assetTypeFilter,
|
||||||
assetVisibility,
|
assetVisibility,
|
||||||
webhook,
|
webhook,
|
||||||
@@ -226,6 +251,7 @@ const {
|
|||||||
} = methods;
|
} = methods;
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
assetAddTags,
|
||||||
assetAddToAlbums,
|
assetAddToAlbums,
|
||||||
assetArchive,
|
assetArchive,
|
||||||
assetFavorite,
|
assetFavorite,
|
||||||
@@ -235,6 +261,7 @@ export {
|
|||||||
assetDateFilter,
|
assetDateFilter,
|
||||||
assetLock,
|
assetLock,
|
||||||
assetMissingTimeZoneFilter,
|
assetMissingTimeZoneFilter,
|
||||||
|
assetTagFilter,
|
||||||
assetTypeFilter,
|
assetTypeFilter,
|
||||||
assetVisibility,
|
assetVisibility,
|
||||||
webhook,
|
webhook,
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import {
|
|||||||
type BulkIdResponseDto,
|
type BulkIdResponseDto,
|
||||||
type BulkIdsDto,
|
type BulkIdsDto,
|
||||||
type CreateAlbumDto,
|
type CreateAlbumDto,
|
||||||
|
type TagBulkAssetsDto,
|
||||||
|
type TagBulkAssetsResponseDto,
|
||||||
} from '@immich/sdk';
|
} from '@immich/sdk';
|
||||||
|
|
||||||
declare module 'extism:host' {
|
declare module 'extism:host' {
|
||||||
@@ -47,6 +49,7 @@ export const availableFunctions = [
|
|||||||
'addAssetsToAlbum',
|
'addAssetsToAlbum',
|
||||||
'addAssetsToAlbums',
|
'addAssetsToAlbums',
|
||||||
'httpRequest',
|
'httpRequest',
|
||||||
|
'bulkTagAssets',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export const hostFunctions = (authToken: string) => {
|
export const hostFunctions = (authToken: string) => {
|
||||||
@@ -96,5 +99,11 @@ export const hostFunctions = (authToken: string) => {
|
|||||||
authToken,
|
authToken,
|
||||||
[url, options],
|
[url, options],
|
||||||
),
|
),
|
||||||
|
bulkTagAssets: (dto: TagBulkAssetsDto) =>
|
||||||
|
call<[TagBulkAssetsDto], TagBulkAssetsResponseDto>(
|
||||||
|
'bulkTagAssets',
|
||||||
|
authToken,
|
||||||
|
[dto],
|
||||||
|
),
|
||||||
} satisfies Record<(typeof availableFunctions)[number], unknown>;
|
} satisfies Record<(typeof availableFunctions)[number], unknown>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
import type { AssetTypeEnum, AssetVisibility, WorkflowType } from '@immich/sdk';
|
import type {
|
||||||
|
AssetTypeEnum,
|
||||||
|
AssetVisibility,
|
||||||
|
TagResponseDto,
|
||||||
|
WorkflowType,
|
||||||
|
} from '@immich/sdk';
|
||||||
|
|
||||||
type DeepPartial<T> = T extends Date
|
type DeepPartial<T> = T extends Date
|
||||||
? T
|
? T
|
||||||
@@ -18,6 +23,7 @@ export type WorkflowEventData<T extends WorkflowType> = WorkflowEventMap[T];
|
|||||||
export enum WorkflowTrigger {
|
export enum WorkflowTrigger {
|
||||||
AssetCreate = 'AssetCreate',
|
AssetCreate = 'AssetCreate',
|
||||||
AssetMetadataExtraction = 'AssetMetadataExtraction',
|
AssetMetadataExtraction = 'AssetMetadataExtraction',
|
||||||
|
AssetTagged = 'AssetTagged',
|
||||||
// PersonRecognized = 'PersonRecognized',
|
// PersonRecognized = 'PersonRecognized',
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,6 +94,7 @@ export type AssetV1 = {
|
|||||||
duplicateId: string | null;
|
duplicateId: string | null;
|
||||||
visibility: AssetVisibility;
|
visibility: AssetVisibility;
|
||||||
isEdited: boolean;
|
isEdited: boolean;
|
||||||
|
tags: TagResponseDto[];
|
||||||
exifInfo: {
|
exifInfo: {
|
||||||
make: string | null;
|
make: string | null;
|
||||||
model: string | null;
|
model: string | null;
|
||||||
|
|||||||
@@ -7423,7 +7423,8 @@ export enum WorkflowType {
|
|||||||
}
|
}
|
||||||
export enum WorkflowTrigger {
|
export enum WorkflowTrigger {
|
||||||
AssetCreate = "AssetCreate",
|
AssetCreate = "AssetCreate",
|
||||||
AssetMetadataExtraction = "AssetMetadataExtraction"
|
AssetMetadataExtraction = "AssetMetadataExtraction",
|
||||||
|
AssetTagged = "AssetTagged"
|
||||||
}
|
}
|
||||||
export enum QueueJobStatus {
|
export enum QueueJobStatus {
|
||||||
Active = "active",
|
Active = "active",
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ type EventMap = {
|
|||||||
|
|
||||||
// asset events
|
// asset events
|
||||||
AssetCreate: [{ asset: Pick<Asset, 'id' | 'ownerId'>; file?: UploadFile }];
|
AssetCreate: [{ asset: Pick<Asset, 'id' | 'ownerId'>; file?: UploadFile }];
|
||||||
AssetTag: [{ assetId: string }];
|
AssetTag: [{ assetId: string; userId: string }];
|
||||||
AssetUntag: [{ assetId: string }];
|
AssetUntag: [{ assetId: string }];
|
||||||
AssetHide: [{ assetId: string; userId: string }];
|
AssetHide: [{ assetId: string; userId: string }];
|
||||||
AssetShow: [{ assetId: string; userId: string }];
|
AssetShow: [{ assetId: string; userId: string }];
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { WorkflowSearchDto } from 'src/dtos/workflow.dto';
|
|||||||
import { DB } from 'src/schema';
|
import { DB } from 'src/schema';
|
||||||
import { WorkflowStepTable } from 'src/schema/tables/workflow-step.table';
|
import { WorkflowStepTable } from 'src/schema/tables/workflow-step.table';
|
||||||
import { WorkflowTable } from 'src/schema/tables/workflow.table';
|
import { WorkflowTable } from 'src/schema/tables/workflow.table';
|
||||||
|
import { withTags } from 'src/utils/database';
|
||||||
|
|
||||||
export type WorkflowStepUpsert = Omit<Insertable<WorkflowStepTable>, 'workflowId' | 'order'>;
|
export type WorkflowStepUpsert = Omit<Insertable<WorkflowStepTable>, 'workflowId' | 'order'>;
|
||||||
|
|
||||||
@@ -143,6 +144,7 @@ export class WorkflowRepository {
|
|||||||
.leftJoin('asset_exif', 'asset_exif.assetId', 'asset.id')
|
.leftJoin('asset_exif', 'asset_exif.assetId', 'asset.id')
|
||||||
.select((eb) => [
|
.select((eb) => [
|
||||||
...columns.workflowAssetV1,
|
...columns.workflowAssetV1,
|
||||||
|
withTags,
|
||||||
jsonObjectFrom(
|
jsonObjectFrom(
|
||||||
eb
|
eb
|
||||||
.selectFrom('asset_exif')
|
.selectFrom('asset_exif')
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ export class TagService extends BaseService {
|
|||||||
const results = await this.tagRepository.upsertAssetIds(items);
|
const results = await this.tagRepository.upsertAssetIds(items);
|
||||||
for (const assetId of new Set(results.map((item) => item.assetId))) {
|
for (const assetId of new Set(results.map((item) => item.assetId))) {
|
||||||
await this.updateTags(assetId);
|
await this.updateTags(assetId);
|
||||||
await this.eventRepository.emit('AssetTag', { assetId });
|
await this.eventRepository.emit('AssetTag', { assetId, userId: auth.user.id });
|
||||||
}
|
}
|
||||||
|
|
||||||
return { count: results.length };
|
return { count: results.length };
|
||||||
@@ -114,7 +114,7 @@ export class TagService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await this.updateTags(assetId);
|
await this.updateTags(assetId);
|
||||||
await this.eventRepository.emit('AssetTag', { assetId });
|
await this.eventRepository.emit('AssetTag', { assetId, userId: auth.user.id });
|
||||||
}
|
}
|
||||||
|
|
||||||
return results;
|
return results;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { AlbumsAddAssetsDto, CreateAlbumDto, GetAlbumsDto } from 'src/dtos/album
|
|||||||
import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
|
import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
|
||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import { PluginManifestDto } from 'src/dtos/plugin-manifest.dto';
|
import { PluginManifestDto } from 'src/dtos/plugin-manifest.dto';
|
||||||
|
import { TagBulkAssetsDto } from 'src/dtos/tag.dto';
|
||||||
import {
|
import {
|
||||||
BootstrapEventPriority,
|
BootstrapEventPriority,
|
||||||
DatabaseLock,
|
DatabaseLock,
|
||||||
@@ -27,6 +28,7 @@ import { ArgOf } from 'src/repositories/event.repository';
|
|||||||
import { AlbumService } from 'src/services/album.service';
|
import { AlbumService } from 'src/services/album.service';
|
||||||
import { AssetService } from 'src/services/asset.service';
|
import { AssetService } from 'src/services/asset.service';
|
||||||
import { BaseService } from 'src/services/base.service';
|
import { BaseService } from 'src/services/base.service';
|
||||||
|
import { TagService } from 'src/services/tag.service';
|
||||||
import { JobOf } from 'src/types';
|
import { JobOf } from 'src/types';
|
||||||
|
|
||||||
const dummy = () => {
|
const dummy = () => {
|
||||||
@@ -69,6 +71,7 @@ export class WorkflowExecutionService extends BaseService {
|
|||||||
this.jwtSecret = this.cryptoRepository.randomBytesAsText(32);
|
this.jwtSecret = this.cryptoRepository.randomBytesAsText(32);
|
||||||
|
|
||||||
const albumService = BaseService.create(AlbumService, this);
|
const albumService = BaseService.create(AlbumService, this);
|
||||||
|
const tagService = BaseService.create(TagService, this);
|
||||||
|
|
||||||
const searchAlbums = this.wrap<[dto: GetAlbumsDto]>((authDto, ctx, args) => albumService.getAll(authDto, ...args));
|
const searchAlbums = this.wrap<[dto: GetAlbumsDto]>((authDto, ctx, args) => albumService.getAll(authDto, ...args));
|
||||||
const createAlbum = this.wrap<[dto: CreateAlbumDto]>((authDto, ctx, args) => albumService.create(authDto, ...args));
|
const createAlbum = this.wrap<[dto: CreateAlbumDto]>((authDto, ctx, args) => albumService.create(authDto, ...args));
|
||||||
@@ -106,6 +109,9 @@ export class WorkflowExecutionService extends BaseService {
|
|||||||
|
|
||||||
throw new Error('Hostname did not match any listed in methods[].allowedHosts in the plugin manifest');
|
throw new Error('Hostname did not match any listed in methods[].allowedHosts in the plugin manifest');
|
||||||
});
|
});
|
||||||
|
const bulkTagAssets = this.wrap<[dto: TagBulkAssetsDto]>((authDto, ctx, args) =>
|
||||||
|
tagService.bulkTagAssets(authDto, ...args),
|
||||||
|
);
|
||||||
|
|
||||||
const functions = {
|
const functions = {
|
||||||
searchAlbums,
|
searchAlbums,
|
||||||
@@ -113,6 +119,7 @@ export class WorkflowExecutionService extends BaseService {
|
|||||||
addAssetsToAlbum,
|
addAssetsToAlbum,
|
||||||
addAssetsToAlbums,
|
addAssetsToAlbums,
|
||||||
httpRequest,
|
httpRequest,
|
||||||
|
bulkTagAssets,
|
||||||
};
|
};
|
||||||
|
|
||||||
const stubs: typeof functions = {
|
const stubs: typeof functions = {
|
||||||
@@ -121,6 +128,7 @@ export class WorkflowExecutionService extends BaseService {
|
|||||||
addAssetsToAlbum: dummy,
|
addAssetsToAlbum: dummy,
|
||||||
addAssetsToAlbums: dummy,
|
addAssetsToAlbums: dummy,
|
||||||
httpRequest: dummy,
|
httpRequest: dummy,
|
||||||
|
bulkTagAssets: dummy,
|
||||||
};
|
};
|
||||||
|
|
||||||
const plugins = await this.pluginRepository.getForLoad();
|
const plugins = await this.pluginRepository.getForLoad();
|
||||||
@@ -309,6 +317,11 @@ export class WorkflowExecutionService extends BaseService {
|
|||||||
return this.onAssetTrigger({ userId, assetId, trigger: WorkflowTrigger.AssetMetadataExtraction });
|
return this.onAssetTrigger({ userId, assetId, trigger: WorkflowTrigger.AssetMetadataExtraction });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OnEvent({ name: 'AssetTag' })
|
||||||
|
onAssetTagged({ assetId, userId }: ArgOf<'AssetTag'>) {
|
||||||
|
return this.onAssetTrigger({ userId, assetId, trigger: WorkflowTrigger.AssetTagged });
|
||||||
|
}
|
||||||
|
|
||||||
private async onAssetTrigger({ userId, assetId, trigger }: AssetTrigger) {
|
private async onAssetTrigger({ userId, assetId, trigger }: AssetTrigger) {
|
||||||
const items = await this.workflowRepository.search({ userId, trigger });
|
const items = await this.workflowRepository.search({ userId, trigger });
|
||||||
await this.jobRepository.queueAll(
|
await this.jobRepository.queueAll(
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export const triggerMap: Record<WorkflowTrigger, WorkflowType[]> = {
|
|||||||
[WorkflowTrigger.AssetCreate]: [WorkflowType.AssetV1],
|
[WorkflowTrigger.AssetCreate]: [WorkflowType.AssetV1],
|
||||||
// [WorkflowTrigger.PersonRecognized]: [WorkflowType.AssetPersonV1],
|
// [WorkflowTrigger.PersonRecognized]: [WorkflowType.AssetPersonV1],
|
||||||
[WorkflowTrigger.AssetMetadataExtraction]: [WorkflowType.AssetV1],
|
[WorkflowTrigger.AssetMetadataExtraction]: [WorkflowType.AssetV1],
|
||||||
|
[WorkflowTrigger.AssetTagged]: [WorkflowType.AssetV1],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getWorkflowTriggers = () =>
|
export const getWorkflowTriggers = () =>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import SchemaAlbumPicker from '$lib/components/SchemaAlbumPicker.svelte';
|
import SchemaAlbumPicker from '$lib/components/SchemaAlbumPicker.svelte';
|
||||||
import Self from '$lib/components/SchemaConfiguration.svelte';
|
import Self from '$lib/components/SchemaConfiguration.svelte';
|
||||||
|
import SchemaTagPicker from '$lib/components/SchemaTagPicker.svelte';
|
||||||
import type { JSONSchemaProperty, SchemaConfig } from '$lib/types';
|
import type { JSONSchemaProperty, SchemaConfig } from '$lib/types';
|
||||||
import {
|
import {
|
||||||
CodeBlock,
|
CodeBlock,
|
||||||
@@ -80,6 +81,8 @@
|
|||||||
</div>
|
</div>
|
||||||
{:else if schema.uiHint?.type === 'AlbumId'}
|
{:else if schema.uiHint?.type === 'AlbumId'}
|
||||||
<SchemaAlbumPicker {label} {description} array={schema.array} bind:albumIds={getUiHintValue, setUiHintValue} />
|
<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}
|
{:else if schema.enum && schema.array}
|
||||||
<Field {label} {description}>
|
<Field {label} {description}>
|
||||||
<MultiSelect options={schema.enum} bind:values={getEnum, setValue} />
|
<MultiSelect options={schema.enum} bind:values={getEnum, setValue} />
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -100,7 +100,7 @@ export type JSONSchemaProperty = {
|
|||||||
properties?: Record<string, JSONSchemaProperty>;
|
properties?: Record<string, JSONSchemaProperty>;
|
||||||
required?: string[];
|
required?: string[];
|
||||||
uiHint?: {
|
uiHint?: {
|
||||||
type?: 'AlbumId' | 'AssetId' | 'PersonId';
|
type?: 'AlbumId' | 'AssetId' | 'PersonId' | 'TagId';
|
||||||
order?: number;
|
order?: number;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ export const getTriggerName = ($t: MessageFormatter, type: WorkflowTrigger) => {
|
|||||||
case WorkflowTrigger.AssetMetadataExtraction: {
|
case WorkflowTrigger.AssetMetadataExtraction: {
|
||||||
return $t('trigger_asset_metadata_extraction');
|
return $t('trigger_asset_metadata_extraction');
|
||||||
}
|
}
|
||||||
|
case WorkflowTrigger.AssetTagged: {
|
||||||
|
return $t('trigger_asset_tagged');
|
||||||
|
}
|
||||||
default: {
|
default: {
|
||||||
return type;
|
return type;
|
||||||
}
|
}
|
||||||
@@ -30,6 +33,12 @@ export const getTriggerDescription = ($t: MessageFormatter, type: WorkflowTrigge
|
|||||||
case WorkflowTrigger.AssetMetadataExtraction: {
|
case WorkflowTrigger.AssetMetadataExtraction: {
|
||||||
return $t('trigger_asset_metadata_extraction_description');
|
return $t('trigger_asset_metadata_extraction_description');
|
||||||
}
|
}
|
||||||
|
case WorkflowTrigger.AssetTagged: {
|
||||||
|
return $t('trigger_asset_tagged_description');
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script module lang="ts">
|
<script module lang="ts">
|
||||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
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
|
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
||||||
const albumNameCache = new Map<string, Promise<string>>();
|
const albumNameCache = new Map<string, Promise<string>>();
|
||||||
@@ -239,6 +239,16 @@
|
|||||||
{@render badge($t('album'), `"${truncate(albumName)}"`)}
|
{@render badge($t('album'), `"${truncate(albumName)}"`)}
|
||||||
{/await}
|
{/await}
|
||||||
{/each}
|
{/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}
|
{:else}
|
||||||
{@render badge(key, formatConfigValue(value))}
|
{@render badge(key, formatConfigValue(value))}
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
Reference in New Issue
Block a user