Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@
"algoliasearch": "catalog:",
"axios": "catalog:",
"chart.js": "^4.5.0",
"client-zip": "catalog:",
"dompurify": "^3.2.5",
"dotenv": "catalog:",
"es-toolkit": "^1.39.9",
Expand Down
17 changes: 17 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ catalog:
'@webgpu/types': ^0.1.66
algoliasearch: ^5.21.0
axios: ^1.8.2
client-zip: ^2.5.0
cross-env: ^10.1.0
dotenv: ^16.4.5
eslint: ^9.34.0
Expand Down
4 changes: 4 additions & 0 deletions src/locales/en/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -2184,6 +2184,10 @@
"deleteSelected": "Delete",
"downloadStarted": "Downloading {count} files...",
"downloadsStarted": "Started downloading {count} file(s)",
"preparingZip": "Preparing ZIP with {count} file(s)...",
"zipDownloadStarted": "Started downloading {count} file(s) as ZIP",
"zipDownloadFailed": "Failed to download assets as ZIP",
"partialZipSuccess": "ZIP created with {succeeded} file(s). {failed} file(s) failed to download",
"assetsDeletedSuccessfully": "{count} asset(s) deleted successfully",
"failedToDeleteAssets": "Failed to delete selected assets",
"partialDeleteSuccess": "{succeeded} deleted successfully, {failed} failed"
Expand Down
144 changes: 118 additions & 26 deletions src/platform/assets/composables/useMediaAssetActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useToast } from 'primevue/usetoast'
import { inject } from 'vue'

import ConfirmationDialogContent from '@/components/dialog/content/ConfirmationDialogContent.vue'
import { downloadFile } from '@/base/common/downloadUtil'
import { downloadBlob, downloadFile } from '@/base/common/downloadUtil'
import { useCopyToClipboard } from '@/composables/useCopyToClipboard'
import { t } from '@/i18n'
import { isCloud } from '@/platform/distribution/types'
Expand Down Expand Up @@ -93,41 +93,133 @@ export function useMediaAssetActions() {
}

/**
* Download multiple assets at once
* Download multiple assets at once as a zip file
* @param assets Array of assets to download
*/
const downloadMultipleAssets = (assets: AssetItem[]) => {
const downloadMultipleAssets = async (assets: AssetItem[]) => {
if (!assets || assets.length === 0) return

// Show loading toast
const loadingToast = {
severity: 'info' as const,
summary: t('g.loading'),
detail: t('mediaAsset.selection.preparingZip', { count: assets.length }),
life: 0 // Keep until manually removed
}
toast.add(loadingToast)
Comment on lines +102 to +109
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

PrimeVue useToast remove method documentation

💡 Result:

Short answer: useToast returns the Toast service (PrimeVueToast) which exposes add(...), removeGroup(group) and removeAllGroups() — there is no documented single-item remove(id/message) on the current PrimeVue Toast service API. [1][2][3]

Sources:

  • PrimeVue Toast docs (useToast / ToastService). [1] (primevue.org)
  • ToastService type definitions showing available methods (add, removeGroup, removeAllGroups). [2] (app.unpkg.com)
  • useToast implementation (injects PrimeVueToast symbol). [3] (app.unpkg.com)

If you want an API that removes a specific toast, tell me which PrimeVue version you use and I’ll check that version’s source/commit history.

Citations:


🏁 Script executed:

# Find and read the file to see the actual implementation
cat -n src/platform/assets/composables/useMediaAssetActions.ts | sed -n '95,120p'

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 1103


🏁 Script executed:

# Search for other toast.remove() usage in the codebase to see patterns
rg -n 'toast\.remove' --type ts --type tsx -B 2 -A 2

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 97


🏁 Script executed:

# Search for toast.remove() calls using correct type filtering
rg -n 'toast\.remove' --type ts -B 2 -A 2

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 955


🏁 Script executed:

# Read more of the file to see where the toast is removed
cat -n src/platform/assets/composables/useMediaAssetActions.ts | sed -n '100,180p'

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 3323


Toast removal mechanism will fail — toast.remove() doesn't accept message objects.

PrimeVue's toast.remove() method does not exist in the ToastService API. The only available removal methods are removeGroup(group) and removeAllGroups(). The current code at lines 190 and 216 attempts to call toast.remove(loadingToast), which will not work and the loading toast will persist indefinitely.

Use a message group to reliably remove the loading toast:

-    // Show loading toast
-    const loadingToast = {
-      severity: 'info' as const,
-      summary: t('g.loading'),
-      detail: t('mediaAsset.selection.preparingZip', { count: assets.length }),
-      life: 0 // Keep until manually removed
-    }
-    toast.add(loadingToast)
+    // Show loading toast with group for removal
+    const loadingGroup = 'zip-download-loading'
+    toast.add({
+      severity: 'info',
+      summary: t('g.loading'),
+      detail: t('mediaAsset.selection.preparingZip', { count: assets.length }),
+      life: 0, // Keep until manually removed
+      group: loadingGroup
+    })

Then update removal calls (lines 190 and 216):

-      toast.remove(loadingToast)
+      toast.removeGroup(loadingGroup)

and

-      toast.remove(loadingToast)
+      toast.removeGroup(loadingGroup)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Show loading toast
const loadingToast = {
severity: 'info' as const,
summary: t('g.loading'),
detail: t('mediaAsset.selection.preparingZip', { count: assets.length }),
life: 0 // Keep until manually removed
}
toast.add(loadingToast)
// Show loading toast with group for removal
const loadingGroup = 'zip-download-loading'
toast.add({
severity: 'info',
summary: t('g.loading'),
detail: t('mediaAsset.selection.preparingZip', { count: assets.length }),
life: 0, // Keep until manually removed
group: loadingGroup
})
🤖 Prompt for AI Agents
In src/platform/assets/composables/useMediaAssetActions.ts around lines 102-109
(and update calls at ~190 and ~216), the loading toast is created as a message
object but later removed with toast.remove(message) which is invalid; change the
loading toast to include a message group (e.g., group: 'loadingZip') when adding
it, then replace the failing removal calls with toast.removeGroup('loadingZip')
so the loading toast can be reliably cleared; keep life: 0 if you still want it
persistent until removal.


try {
assets.forEach((asset) => {
const filename = asset.name
let downloadUrl: string

// In cloud, use preview_url directly (from GCS or other cloud storage)
// In OSS/localhost, use the /view endpoint
if (isCloud && asset.preview_url) {
downloadUrl = asset.preview_url
} else {
downloadUrl = getAssetUrl(asset)
}
downloadFile(downloadUrl, filename)
})
const { downloadZip } = await import('client-zip')

// Track filename usage to handle duplicates
const nameCount = new Map<string, number>()

// Fetch all assets and prepare files for zip (handle partial failures)
const results = await Promise.allSettled(
assets.map(async (asset) => {
try {
let filename = asset.name
let downloadUrl: string

// In cloud, use preview_url directly (from GCS or other cloud storage)
// In OSS/localhost, use the /view endpoint
if (isCloud && asset.preview_url) {
downloadUrl = asset.preview_url
} else {
downloadUrl = getAssetUrl(asset)
}

toast.add({
severity: 'success',
summary: t('g.success'),
detail: t('mediaAsset.selection.downloadsStarted', {
count: assets.length
}),
life: 2000
})
const response = await fetch(downloadUrl)
if (!response.ok) {
console.warn(
`Failed to fetch ${filename}: ${response.status} ${response.statusText}`
)
return null
}

// Handle duplicate filenames by adding a number suffix
if (nameCount.has(filename)) {
const count = nameCount.get(filename)! + 1
nameCount.set(filename, count)
const parts = filename.split('.')
const ext = parts.length > 1 ? parts.pop() : ''
filename = ext
? `${parts.join('.')}_${count}.${ext}`
: `${filename}_${count}`
} else {
nameCount.set(filename, 1)
}

return {
name: filename,
input: response
}
} catch (error) {
console.warn(`Error fetching ${asset.name}:`, error)
return null
}
})
)

// Filter out failed downloads
const files = results
.map((result) => (result.status === 'fulfilled' ? result.value : null))
.filter(
(file): file is { name: string; input: Response } => file !== null
)

// Check if any files were successfully fetched
if (files.length === 0) {
throw new Error('No assets could be downloaded')
}

// Generate zip and get blob
const zipBlob = await downloadZip(files).blob()

// Create zip filename with timestamp to avoid collisions
const timestamp = new Date()
.toISOString()
.replace(/[:.]/g, '-')
.slice(0, -5)
const zipFilename = `comfyui-assets-${timestamp}.zip`

// Download using existing utility
downloadBlob(zipFilename, zipBlob)

// Remove loading toast
toast.remove(loadingToast)

// Show appropriate success message based on results
const failedCount = assets.length - files.length
if (failedCount > 0) {
toast.add({
severity: 'warn',
summary: t('g.warning'),
detail: t('mediaAsset.selection.partialZipSuccess', {
succeeded: files.length,
failed: failedCount
}),
life: 4000
})
} else {
toast.add({
severity: 'success',
summary: t('g.success'),
detail: t('mediaAsset.selection.zipDownloadStarted', {
count: assets.length
}),
life: 2000
})
}
} catch (error) {
console.error('Failed to download assets:', error)
// Remove loading toast on error
toast.remove(loadingToast)

console.error('Failed to download assets as zip:', error)
toast.add({
severity: 'error',
summary: t('g.error'),
detail: t('g.failedToDownloadImage'),
detail: t('mediaAsset.selection.zipDownloadFailed'),
life: 3000
})
}
Expand Down
Loading