Files
ComfyUI_frontend/src/platform/assets/components/MediaAssetMoreMenu.vue
Jin Yi a4d979e4c9 [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>
2025-11-18 00:04:45 +00:00

209 lines
5.4 KiB
Vue

<template>
<div class="flex flex-col">
<!-- TODO: 3D assets currently excluded from inspection.
When 3D loader nodes are implemented, update detectNodeTypeFromFilename
to return appropriate node type for .gltf, .glb files and remove this exclusion -->
<IconTextButton
v-if="asset?.kind !== '3D'"
type="transparent"
label="Inspect asset"
@click="handleInspect"
>
<template #icon>
<i class="icon-[lucide--zoom-in] size-4" />
</template>
</IconTextButton>
<IconTextButton
v-if="showAddToWorkflow"
type="transparent"
label="Add to current workflow"
@click="handleAddToWorkflow"
>
<template #icon>
<i class="icon-[comfy--node] size-4" />
</template>
</IconTextButton>
<IconTextButton type="transparent" label="Download" @click="handleDownload">
<template #icon>
<i class="icon-[lucide--download] size-4" />
</template>
</IconTextButton>
<MediaAssetButtonDivider v-if="showAddToWorkflow || showWorkflowActions" />
<IconTextButton
v-if="showWorkflowActions"
type="transparent"
label="Open as workflow in new tab"
@click="handleOpenWorkflow"
>
<template #icon>
<i class="icon-[comfy--workflow] size-4" />
</template>
</IconTextButton>
<IconTextButton
v-if="showWorkflowActions"
type="transparent"
label="Export workflow"
@click="handleExportWorkflow"
>
<template #icon>
<i class="icon-[lucide--file-output] size-4" />
</template>
</IconTextButton>
<MediaAssetButtonDivider v-if="showWorkflowActions && showCopyJobId" />
<IconTextButton
v-if="showCopyJobId"
type="transparent"
label="Copy job ID"
@click="handleCopyJobId"
>
<template #icon>
<i class="icon-[lucide--copy] size-4" />
</template>
</IconTextButton>
<MediaAssetButtonDivider v-if="showCopyJobId && shouldShowDeleteButton" />
<IconTextButton
v-if="shouldShowDeleteButton"
type="transparent"
label="Delete"
@click="handleDelete"
>
<template #icon>
<i class="icon-[lucide--trash-2] size-4" />
</template>
</IconTextButton>
</div>
</template>
<script setup lang="ts">
import { computed, inject } from 'vue'
import IconTextButton from '@/components/button/IconTextButton.vue'
import { isCloud } from '@/platform/distribution/types'
import { supportsWorkflowMetadata } from '@/platform/workflow/utils/workflowExtractionUtil'
import { detectNodeTypeFromFilename } from '@/utils/loaderNodeUtil'
import { useMediaAssetActions } from '../composables/useMediaAssetActions'
import { MediaAssetKey } from '../schemas/mediaAssetSchema'
import MediaAssetButtonDivider from './MediaAssetButtonDivider.vue'
const { close, showDeleteButton } = defineProps<{
close: () => void
showDeleteButton?: boolean
}>()
const emit = defineEmits<{
inspect: []
'asset-deleted': []
}>()
const { asset, context } = inject(MediaAssetKey)!
const actions = useMediaAssetActions()
const assetType = computed(() => {
return asset.value?.tags?.[0] || context.value?.type || 'output'
})
// Show "Add to current workflow" for all media files (images, videos, audio)
// This works for any file type that has a corresponding loader node
const showAddToWorkflow = computed(() => {
// Output assets can always be added
if (assetType.value === 'output') return true
// Input assets: check if file type is supported by loader nodes
// Use the same utility as the actual addWorkflow function for consistency
if (assetType.value === 'input' && asset.value?.name) {
const { nodeType } = detectNodeTypeFromFilename(asset.value.name)
return nodeType !== null
}
return false
})
// Show "Open/Export workflow" only for files with workflow metadata
// This is more restrictive - only PNG, WEBP, FLAC support embedded workflows
const showWorkflowActions = computed(() => {
// Output assets always have workflow metadata
if (assetType.value === 'output') return true
// Input assets: only formats that support workflow metadata
if (assetType.value === 'input' && asset.value?.name) {
return supportsWorkflowMetadata(asset.value.name)
}
return false
})
// Only show Copy Job ID for output assets (not for imported/input assets)
const showCopyJobId = computed(() => {
return assetType.value !== 'input'
})
const shouldShowDeleteButton = computed(() => {
const propAllows = showDeleteButton ?? true
const typeAllows =
assetType.value === 'output' || (assetType.value === 'input' && isCloud)
return propAllows && typeAllows
})
const handleInspect = () => {
emit('inspect')
close()
}
const handleAddToWorkflow = () => {
if (asset.value) {
actions.addWorkflow()
}
close()
}
const handleDownload = () => {
if (asset.value) {
actions.downloadAsset()
}
close()
}
const handleOpenWorkflow = () => {
if (asset.value) {
actions.openWorkflow()
}
close()
}
const handleExportWorkflow = () => {
if (asset.value) {
actions.exportWorkflow()
}
close()
}
const handleCopyJobId = async () => {
if (asset.value) {
await actions.copyJobId()
}
close()
}
const handleDelete = async () => {
if (!asset.value) return
close() // Close the menu first
const success = await actions.confirmDelete(asset.value)
if (success) {
emit('asset-deleted')
}
}
</script>