refactor ActionItem

This commit is contained in:
Alex Tran
2025-12-05 16:37:10 +00:00
parent 63e38f347e
commit 1e238e7a48
2 changed files with 152 additions and 89 deletions

View File

@@ -1,6 +1,12 @@
import { goto } from '$app/navigation';
import { AppRoute } from '$lib/constants';
import { handleError } from '$lib/utils/handle-error';
import { getFormatter } from '$lib/utils/i18n';
import { import {
createWorkflow,
deleteWorkflow,
PluginTriggerType, PluginTriggerType,
updateWorkflow as updateWorkflowApi, updateWorkflow,
type PluginActionResponseDto, type PluginActionResponseDto,
type PluginContextType, type PluginContextType,
type PluginFilterResponseDto, type PluginFilterResponseDto,
@@ -10,6 +16,9 @@ import {
type WorkflowResponseDto, type WorkflowResponseDto,
type WorkflowUpdateDto, type WorkflowUpdateDto,
} from '@immich/sdk'; } from '@immich/sdk';
import { modalManager, toastManager, type ActionItem } from '@immich/ui';
import { mdiCodeJson, mdiDelete, mdiPause, mdiPencil, mdiPlay } from '@mdi/js';
import type { MessageFormatter } from 'svelte-i18n';
export interface WorkflowPayload { export interface WorkflowPayload {
name: string; name: string;
@@ -295,5 +304,108 @@ export const handleUpdateWorkflow = async (
triggerType, triggerType,
}; };
return updateWorkflowApi({ id: workflowId, workflowUpdateDto: updateDto }); return updateWorkflow({ id: workflowId, workflowUpdateDto: updateDto });
};
export const getWorkflowActions = ($t: MessageFormatter, workflow: WorkflowResponseDto) => {
const ToggleEnabled: ActionItem = {
title: workflow.enabled ? $t('disable') : $t('enable'),
icon: workflow.enabled ? mdiPause : mdiPlay,
color: workflow.enabled ? 'danger' : 'primary',
onAction: async () => {
await handleToggleWorkflowEnabled(workflow);
},
};
const Edit: ActionItem = {
title: $t('edit'),
icon: mdiPencil,
onAction: () => handleNavigateToWorkflow(workflow),
};
const Delete: ActionItem = {
title: $t('delete'),
icon: mdiDelete,
color: 'danger',
onAction: async () => {
await handleDeleteWorkflow(workflow);
},
};
return { ToggleEnabled, Edit, Delete };
};
export const getWorkflowShowSchemaAction = (
$t: MessageFormatter,
isExpanded: boolean,
onToggle: () => void,
): ActionItem => ({
title: isExpanded ? $t('hide_schema') : $t('show_schema'),
icon: mdiCodeJson,
onAction: onToggle,
});
export const handleCreateWorkflow = async (): Promise<WorkflowResponseDto | undefined> => {
const $t = await getFormatter();
try {
const workflow = await createWorkflow({
workflowCreateDto: {
name: $t('untitled_workflow'),
triggerType: PluginTriggerType.AssetCreate,
filters: [],
actions: [],
enabled: false,
},
});
await goto(`${AppRoute.WORKFLOWS}/${workflow.id}`);
return workflow;
} catch (error) {
handleError(error, $t('errors.unable_to_create'));
}
};
export const handleToggleWorkflowEnabled = async (
workflow: WorkflowResponseDto,
): Promise<WorkflowResponseDto | undefined> => {
const $t = await getFormatter();
try {
const updated = await updateWorkflow({
id: workflow.id,
workflowUpdateDto: { enabled: !workflow.enabled },
});
toastManager.success($t('workflow_updated'));
return updated;
} catch (error) {
handleError(error, $t('errors.unable_to_update_workflow'));
}
};
export const handleDeleteWorkflow = async (workflow: WorkflowResponseDto): Promise<boolean> => {
const $t = await getFormatter();
const confirmed = await modalManager.showDialog({
prompt: $t('workflow_delete_prompt'),
confirmColor: 'danger',
});
if (!confirmed) {
return false;
}
try {
await deleteWorkflow({ id: workflow.id });
toastManager.success($t('workflow_deleted'));
return true;
} catch (error) {
handleError(error, $t('errors.unable_to_delete_workflow'));
return false;
}
};
export const handleNavigateToWorkflow = async (workflow: WorkflowResponseDto): Promise<void> => {
await goto(`${AppRoute.WORKFLOWS}/${workflow.id}`);
}; };

View File

@@ -1,19 +1,16 @@
<script lang="ts"> <script lang="ts">
import { goto } from '$app/navigation';
import emptyWorkflows from '$lib/assets/empty-workflows.svg'; import emptyWorkflows from '$lib/assets/empty-workflows.svg';
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte'; import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte'; import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
import { AppRoute } from '$lib/constants';
import type { WorkflowPayload } from '$lib/services/workflow.service';
import { handleError } from '$lib/utils/handle-error';
import { import {
createWorkflow, getWorkflowActions,
deleteWorkflow, getWorkflowShowSchemaAction,
PluginTriggerType, handleCreateWorkflow,
updateWorkflow, handleDeleteWorkflow,
type PluginFilterResponseDto, handleToggleWorkflowEnabled,
type WorkflowResponseDto, type WorkflowPayload,
} from '@immich/sdk'; } from '$lib/services/workflow.service';
import type { PluginFilterResponseDto, WorkflowResponseDto } from '@immich/sdk';
import { import {
Button, Button,
Card, Card,
@@ -27,12 +24,10 @@
IconButton, IconButton,
MenuItemType, MenuItemType,
menuManager, menuManager,
modalManager,
Text, Text,
toastManager,
VStack, VStack,
} from '@immich/ui'; } from '@immich/ui';
import { mdiClose, mdiCodeJson, mdiDelete, mdiDotsVertical, mdiPause, mdiPencil, mdiPlay, mdiPlus } from '@mdi/js'; import { mdiClose, mdiDotsVertical, mdiPlus } from '@mdi/js';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import { SvelteMap, SvelteSet } from 'svelte/reactivity'; import { SvelteMap, SvelteSet } from 'svelte/reactivity';
import type { PageData } from './$types'; import type { PageData } from './$types';
@@ -44,6 +39,7 @@
let { data }: Props = $props(); let { data }: Props = $props();
let workflows = $state<WorkflowResponseDto[]>(data.workflows); let workflows = $state<WorkflowResponseDto[]>(data.workflows);
const expandedWorkflows = new SvelteSet<string>(); const expandedWorkflows = new SvelteSet<string>();
const pluginFilterLookup = new SvelteMap<string, PluginFilterResponseDto>(); const pluginFilterLookup = new SvelteMap<string, PluginFilterResponseDto>();
@@ -95,56 +91,20 @@
const getJson = (workflow: WorkflowResponseDto) => JSON.stringify(constructPayload(workflow), null, 2); const getJson = (workflow: WorkflowResponseDto) => JSON.stringify(constructPayload(workflow), null, 2);
const handleToggleEnabled = async (workflow: WorkflowResponseDto) => { const onToggleEnabled = async (workflow: WorkflowResponseDto) => {
try { const updated = await handleToggleWorkflowEnabled(workflow);
const updated = await updateWorkflow({ if (updated) {
id: workflow.id,
workflowUpdateDto: { enabled: !workflow.enabled },
});
workflows = workflows.map((w) => (w.id === updated.id ? updated : w)); workflows = workflows.map((w) => (w.id === updated.id ? updated : w));
toastManager.success($t('workflow_updated'));
} catch (error) {
handleError(error, $t('errors.unable_to_update_workflow'));
} }
}; };
const handleDeleteWorkflow = async (workflow: WorkflowResponseDto) => { const onDeleteWorkflow = async (workflow: WorkflowResponseDto) => {
try { const deleted = await handleDeleteWorkflow(workflow);
const confirmed = await modalManager.showDialog({ if (deleted) {
prompt: $t('workflow_delete_prompt'),
confirmColor: 'danger',
});
if (!confirmed) {
return;
}
await deleteWorkflow({ id: workflow.id });
workflows = workflows.filter((w) => w.id !== workflow.id); workflows = workflows.filter((w) => w.id !== workflow.id);
toastManager.success($t('workflow_deleted'));
} catch (error) {
handleError(error, $t('errors.unable_to_delete_workflow'));
} }
}; };
const handleEditWorkflow = async (workflow: WorkflowResponseDto) => {
await goto(`${AppRoute.WORKFLOWS}/${workflow.id}`);
};
const handleCreateWorkflow = async () => {
const workflow = await createWorkflow({
workflowCreateDto: {
name: 'New workflow',
triggerType: PluginTriggerType.AssetCreate,
filters: [],
actions: [],
enabled: false,
},
});
await goto(`${AppRoute.WORKFLOWS}/${workflow.id}`);
};
const getFilterLabel = (filterId: string) => { const getFilterLabel = (filterId: string) => {
const meta = pluginFilterLookup.get(filterId); const meta = pluginFilterLookup.get(filterId);
return meta?.title ?? $t('filter'); return meta?.title ?? $t('filter');
@@ -168,6 +128,27 @@
dateStyle: 'medium', dateStyle: 'medium',
timeStyle: 'short', timeStyle: 'short',
}).format(new Date(createdAt)); }).format(new Date(createdAt));
const showWorkflowMenu = (event: MouseEvent, workflow: WorkflowResponseDto) => {
const { ToggleEnabled, Edit, Delete } = getWorkflowActions($t, workflow);
void menuManager.show({
target: event.currentTarget as HTMLElement,
position: 'top-left',
items: [
{
...ToggleEnabled,
onAction: () => void onToggleEnabled(workflow),
},
Edit,
getWorkflowShowSchemaAction($t, expandedWorkflows.has(workflow.id), () => toggleShowingSchema(workflow.id)),
MenuItemType.Divider,
{
...Delete,
onAction: () => void onDeleteWorkflow(workflow),
},
],
});
};
</script> </script>
{#snippet chipItem(title: string)} {#snippet chipItem(title: string)}
@@ -232,37 +213,7 @@
color="secondary" color="secondary"
icon={mdiDotsVertical} icon={mdiDotsVertical}
aria-label={$t('menu')} aria-label={$t('menu')}
onclick={(event: MouseEvent) => { onclick={(event: MouseEvent) => showWorkflowMenu(event, workflow)}
void menuManager.show({
target: event.currentTarget as HTMLElement,
position: 'top-left',
items: [
{
title: workflow.enabled ? $t('disable') : $t('enable'),
color: workflow.enabled ? 'danger' : 'primary',
icon: workflow.enabled ? mdiPause : mdiPlay,
onAction: () => void handleToggleEnabled(workflow),
},
{
title: $t('edit'),
icon: mdiPencil,
onAction: () => void handleEditWorkflow(workflow),
},
{
title: expandedWorkflows.has(workflow.id) ? $t('hide_schema') : $t('show_schema'),
icon: mdiCodeJson,
onAction: () => toggleShowingSchema(workflow.id),
},
MenuItemType.Divider,
{
title: $t('delete'),
icon: mdiDelete,
color: 'danger',
onAction: () => void handleDeleteWorkflow(workflow),
},
],
});
}}
/> />
</div> </div>
</CardHeader> </CardHeader>