mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-28 02:34:10 +00:00
[feat] Implement media asset workflow actions with shared utilities (#6696)
## Summary Implements 4 missing media asset workflow features and creates shared utilities to eliminate code duplication. ## Implemented Features ### 1. Copy Job ID ✅ - Properly extracts promptId using `getOutputAssetMetadata` - Uses `useCopyToClipboard` composable ### 2. Add to Current Workflow ✅ - Adds LoadImage/LoadVideo/LoadAudio nodes to canvas - Supports all media file types (JPEG, PNG, MP4, etc.) - Auto-detects appropriate node type using `detectNodeTypeFromFilename` utility ### 3. Open Workflow in New Tab ✅ - Extracts workflow from asset metadata or embedded PNG - Opens workflow in new tab ### 4. Export Workflow ✅ - Exports workflow as JSON file - Supports optional filename prompt ## Code Refactoring ### Created Shared Utilities: 1. **`assetTypeUtil.ts`** - `getAssetType()` function eliminates 6 instances of `asset.tags?.[0] || 'output'` 2. **`assetUrlUtil.ts`** - `getAssetUrl()` function consolidates 3 URL construction patterns 3. **`workflowActionsService.ts`** - Shared service for workflow export/open operations 4. **`workflowExtractionUtil.ts`** - Extract workflows from jobs/assets 5. **`loaderNodeUtil.ts`** - Detect loader node types from filenames ### Improvements to Existing Code: - Refactored to use `formatUtil.getMediaTypeFromFilename()` - Extracted `deleteAssetApi()` helper to reduce deletion logic duplication (~40 lines) - Moved `isResultItemType` type guard to shared `typeGuardUtil.ts` - Added 9 i18n strings for proper localization - Added `@comfyorg/shared-frontend-utils` dependency ## Input Assets Support Improved input assets to support workflow features where applicable: - ✅ All media files (JPEG/PNG/MP4, etc.) → "Add to current workflow" enabled - ✅ PNG/WEBP/FLAC with embedded metadata → "Open/Export workflow" enabled ## Impact - **~150+ lines** of duplicate code eliminated - **5 new utility files** created to improve code reusability - **11 files** changed, **483 insertions**, **234 deletions** ## Testing ✅ TypeScript typecheck passed ✅ ESLint passed ✅ Knip passed 🤖 Generated with [Claude Code](https://claude.com/claude-code) ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-6696-feat-Implement-media-asset-workflow-actions-with-shared-utilities-2ab6d73d365081fb8ae9d71ce6e38589) by [Unito](https://www.unito.io) --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: GitHub Action <action@github.com>
This commit is contained in:
123
src/platform/workflow/core/services/workflowActionsService.ts
Normal file
123
src/platform/workflow/core/services/workflowActionsService.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* Shared workflow actions service
|
||||
* Provides reusable workflow operations that can be used by both
|
||||
* job menu and media asset actions
|
||||
*/
|
||||
|
||||
import { downloadBlob } from '@/scripts/utils'
|
||||
import type { ComfyWorkflowJSON } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import { useWorkflowService } from '@/platform/workflow/core/services/workflowService'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { appendJsonExt } from '@/utils/formatUtil'
|
||||
import { t } from '@/i18n'
|
||||
|
||||
/**
|
||||
* Provides shared workflow actions
|
||||
* These operations are used by multiple contexts (jobs, assets)
|
||||
* to avoid code duplication while maintaining flexibility
|
||||
*/
|
||||
export function useWorkflowActionsService() {
|
||||
const workflowStore = useWorkflowStore()
|
||||
const workflowService = useWorkflowService()
|
||||
const settingStore = useSettingStore()
|
||||
const dialogService = useDialogService()
|
||||
|
||||
/**
|
||||
* Export workflow as JSON file with optional filename prompt
|
||||
*
|
||||
* @param workflow The workflow data to export
|
||||
* @param defaultFilename Default filename to use
|
||||
* @returns Result of the export operation
|
||||
*
|
||||
* @example
|
||||
* const result = await exportWorkflowAction(workflow, 'MyWorkflow.json')
|
||||
* if (result.success) {
|
||||
* toast.add({ severity: 'success', detail: 'Exported!' })
|
||||
* }
|
||||
*/
|
||||
const exportWorkflowAction = async (
|
||||
workflow: ComfyWorkflowJSON | null,
|
||||
defaultFilename: string
|
||||
): Promise<{
|
||||
success: boolean
|
||||
error?: string
|
||||
}> => {
|
||||
if (!workflow) {
|
||||
return { success: false, error: 'No workflow data available' }
|
||||
}
|
||||
|
||||
try {
|
||||
let filename = defaultFilename
|
||||
|
||||
// Optionally prompt for custom filename
|
||||
if (settingStore.get('Comfy.PromptFilename')) {
|
||||
const input = await dialogService.prompt({
|
||||
title: t('workflowService.exportWorkflow'),
|
||||
message: t('workflowService.enterFilename') + ':',
|
||||
defaultValue: filename
|
||||
})
|
||||
// User cancelled the prompt
|
||||
if (!input) return { success: false }
|
||||
filename = appendJsonExt(input)
|
||||
}
|
||||
|
||||
// Convert workflow to formatted JSON
|
||||
const json = JSON.stringify(workflow, null, 2)
|
||||
const blob = new Blob([json], { type: 'application/json' })
|
||||
downloadBlob(filename, blob)
|
||||
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error:
|
||||
error instanceof Error ? error.message : 'Failed to export workflow'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open workflow in new tab
|
||||
* Creates a temporary workflow and opens it via the workflow service
|
||||
*
|
||||
* @param workflow The workflow data to open
|
||||
* @param filename Filename for the temporary workflow
|
||||
* @returns Result of the open operation
|
||||
*
|
||||
* @example
|
||||
* const result = await openWorkflowAction(workflow, 'Job 123.json')
|
||||
* if (!result.success) {
|
||||
* toast.add({ severity: 'error', detail: result.error })
|
||||
* }
|
||||
*/
|
||||
const openWorkflowAction = async (
|
||||
workflow: ComfyWorkflowJSON | null,
|
||||
filename: string
|
||||
): Promise<{
|
||||
success: boolean
|
||||
error?: string
|
||||
}> => {
|
||||
if (!workflow) {
|
||||
return { success: false, error: 'No workflow data available' }
|
||||
}
|
||||
|
||||
try {
|
||||
const temp = workflowStore.createTemporary(filename, workflow)
|
||||
await workflowService.openWorkflow(temp)
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error:
|
||||
error instanceof Error ? error.message : 'Failed to open workflow'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
exportWorkflowAction,
|
||||
openWorkflowAction
|
||||
}
|
||||
}
|
||||
93
src/platform/workflow/utils/workflowExtractionUtil.ts
Normal file
93
src/platform/workflow/utils/workflowExtractionUtil.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* Utilities for extracting workflows from different sources
|
||||
* Supports both job-based and asset-based workflow extraction
|
||||
*/
|
||||
|
||||
import type { ComfyWorkflowJSON } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
|
||||
import { getOutputAssetMetadata } from '@/platform/assets/schemas/assetMetadataSchema'
|
||||
import { getAssetUrl } from '@/platform/assets/utils/assetUrlUtil'
|
||||
import { getWorkflowDataFromFile } from '@/scripts/metadata/parser'
|
||||
|
||||
/**
|
||||
* Extract workflow from AssetItem (async - may need file fetch)
|
||||
* Tries metadata first (for output assets), then falls back to extracting from file
|
||||
* This supports both output assets (with embedded metadata) and input assets (PNG with workflow)
|
||||
*
|
||||
* @param asset The asset item to extract workflow from
|
||||
* @returns WorkflowSource with workflow and generated filename
|
||||
*
|
||||
* @example
|
||||
* const asset = { name: 'output.png', user_metadata: { workflow: {...} } }
|
||||
* const { workflow, filename } = await extractWorkflowFromAsset(asset)
|
||||
*/
|
||||
export async function extractWorkflowFromAsset(asset: AssetItem): Promise<{
|
||||
workflow: ComfyWorkflowJSON | null
|
||||
filename: string
|
||||
}> {
|
||||
const baseFilename = asset.name.replace(/\.[^/.]+$/, '.json')
|
||||
|
||||
// Strategy 1: Try metadata first (for output assets)
|
||||
const metadata = getOutputAssetMetadata(asset.user_metadata)
|
||||
if (metadata?.workflow) {
|
||||
return {
|
||||
workflow: metadata.workflow as ComfyWorkflowJSON,
|
||||
filename: baseFilename
|
||||
}
|
||||
}
|
||||
|
||||
// Strategy 2: Try extracting from file (for input assets with embedded workflow)
|
||||
// This supports PNG, WEBP, FLAC, and other formats with metadata
|
||||
try {
|
||||
const fileUrl = getAssetUrl(asset)
|
||||
const response = await fetch(fileUrl)
|
||||
if (!response.ok) {
|
||||
return { workflow: null, filename: baseFilename }
|
||||
}
|
||||
|
||||
const blob = await response.blob()
|
||||
const file = new File([blob], asset.name, { type: blob.type })
|
||||
|
||||
const workflowData = await getWorkflowDataFromFile(file)
|
||||
if (workflowData?.workflow) {
|
||||
// Handle both string and object workflow data
|
||||
const workflow =
|
||||
typeof workflowData.workflow === 'string'
|
||||
? JSON.parse(workflowData.workflow)
|
||||
: workflowData.workflow
|
||||
|
||||
return {
|
||||
workflow: workflow as ComfyWorkflowJSON,
|
||||
filename: baseFilename
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to extract workflow from asset:', error)
|
||||
}
|
||||
|
||||
return {
|
||||
workflow: null,
|
||||
filename: baseFilename
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a file format supports embedded workflow metadata
|
||||
* Useful for UI to show/hide workflow-related options
|
||||
*
|
||||
* @param filename The filename to check
|
||||
* @returns true if the format can contain workflow metadata
|
||||
*
|
||||
* @example
|
||||
* supportsWorkflowMetadata('image.png') // true
|
||||
* supportsWorkflowMetadata('image.jpg') // false
|
||||
*/
|
||||
export function supportsWorkflowMetadata(filename: string): boolean {
|
||||
const lower = filename.toLowerCase()
|
||||
return (
|
||||
lower.endsWith('.png') ||
|
||||
lower.endsWith('.webp') ||
|
||||
lower.endsWith('.flac') ||
|
||||
lower.endsWith('.json')
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user