mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-06 08:00:05 +00:00
refactor
- use existing loading overlay, move to common - change deletion to use single shared flow
This commit is contained in:
@@ -5,8 +5,13 @@
|
||||
class="absolute inset-0 z-50 flex items-center justify-center bg-backdrop/50"
|
||||
>
|
||||
<div class="flex flex-col items-center">
|
||||
<div class="spinner" />
|
||||
<div class="mt-4 text-lg text-base-foreground">
|
||||
<div class="relative flex items-center justify-center">
|
||||
<div class="animate-spin spinner" />
|
||||
<div class="absolute">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="loadingMessage" class="mt-4 text-lg text-base-foreground">
|
||||
{{ loadingMessage }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -17,7 +22,7 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
loading: boolean
|
||||
loadingMessage: string
|
||||
loadingMessage?: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
@@ -25,18 +30,8 @@ defineProps<{
|
||||
.spinner {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border: 4px solid #f3f3f3;
|
||||
border-top: 4px solid #3498db;
|
||||
border: 4px solid var(--muted-foreground);
|
||||
border-top-color: var(--base-foreground);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -31,7 +31,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
import LoadingOverlay from '@/components/load3d/LoadingOverlay.vue'
|
||||
import LoadingOverlay from '@/components/common/LoadingOverlay.vue'
|
||||
import { useLoad3dDrag } from '@/composables/useLoad3dDrag'
|
||||
|
||||
const props = defineProps<{
|
||||
|
||||
@@ -120,11 +120,9 @@
|
||||
@click="handleAssetSelect(item)"
|
||||
@zoom="handleZoomClick(item)"
|
||||
@output-count-click="enterFolderView(item)"
|
||||
@asset-deleted="refreshAssets"
|
||||
@asset-deleting="handleAssetDeleting(item.id, $event)"
|
||||
@context-menu-opened="openContextMenuId = item.id"
|
||||
@bulk-download="handleBulkDownload"
|
||||
@bulk-delete="handleBulkDelete"
|
||||
@delete-assets="handleDeleteAssets"
|
||||
/>
|
||||
</template>
|
||||
</VirtualGrid>
|
||||
@@ -544,26 +542,15 @@ const setAssetsDeletingState = (assetIds: string[], isDeleting: boolean) => {
|
||||
)
|
||||
}
|
||||
|
||||
const handleDeleteSelected = async () => {
|
||||
const selectedAssets = getSelectedAssets(displayAssets.value)
|
||||
const assetIds = selectedAssets.map((a) => a.id)
|
||||
|
||||
await deleteMultipleAssets(selectedAssets, (isDeleting) =>
|
||||
setAssetsDeletingState(assetIds, isDeleting)
|
||||
)
|
||||
clearSelection()
|
||||
}
|
||||
|
||||
const handleBulkDownload = (assets: AssetItem[]) => {
|
||||
downloadMultipleAssets(assets)
|
||||
clearSelection()
|
||||
}
|
||||
|
||||
const handleAssetDeleting = (assetId: string, isDeleting: boolean) => {
|
||||
setAssetsDeletingState([assetId], isDeleting)
|
||||
}
|
||||
const handleDeleteSelected = () =>
|
||||
handleDeleteAssets(getSelectedAssets(displayAssets.value))
|
||||
|
||||
const handleBulkDelete = async (assets: AssetItem[]) => {
|
||||
const handleDeleteAssets = async (assets: AssetItem[]) => {
|
||||
const assetIds = assets.map((a) => a.id)
|
||||
|
||||
await deleteMultipleAssets(assets, (isDeleting) =>
|
||||
|
||||
@@ -46,17 +46,9 @@
|
||||
@image-loaded="handleImageLoaded"
|
||||
/>
|
||||
|
||||
<div v-if="isDeleting" class="absolute inset-0 bg-black/50 z-10">
|
||||
<div class="flex items-center justify-center h-full relative">
|
||||
<div
|
||||
class="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-white absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2"
|
||||
></div>
|
||||
<i
|
||||
class="icon-[lucide--trash-2] size-6 absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2"
|
||||
>
|
||||
</i>
|
||||
</div>
|
||||
</div>
|
||||
<LoadingOverlay :loading="isDeleting">
|
||||
<i class="icon-[lucide--trash-2] size-5" />
|
||||
</LoadingOverlay>
|
||||
|
||||
<!-- Action buttons overlay (top-left) -->
|
||||
<div
|
||||
@@ -142,10 +134,8 @@
|
||||
:selected-assets="selectedAssets"
|
||||
:is-bulk-mode="hasSelection && (selectedAssets?.length ?? 0) > 1"
|
||||
@zoom="handleZoomClick"
|
||||
@asset-deleted="emit('asset-deleted')"
|
||||
@asset-deleting="emit('asset-deleting', $event)"
|
||||
@bulk-download="emit('bulk-download', $event)"
|
||||
@bulk-delete="emit('bulk-delete', $event)"
|
||||
@delete-assets="emit('delete-assets', $event)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -154,6 +144,7 @@ import { useElementHover, whenever } from '@vueuse/core'
|
||||
import { computed, defineAsyncComponent, provide, ref, toRef } from 'vue'
|
||||
|
||||
import IconGroup from '@/components/button/IconGroup.vue'
|
||||
import LoadingOverlay from '@/components/common/LoadingOverlay.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import {
|
||||
formatDuration,
|
||||
@@ -212,11 +203,9 @@ const emit = defineEmits<{
|
||||
click: []
|
||||
zoom: [asset: AssetItem]
|
||||
'output-count-click': []
|
||||
'asset-deleted': []
|
||||
'asset-deleting': [isDeleting: boolean]
|
||||
'context-menu-opened': []
|
||||
'bulk-download': [assets: AssetItem[]]
|
||||
'bulk-delete': [assets: AssetItem[]]
|
||||
'delete-assets': [assets: AssetItem[]]
|
||||
}>()
|
||||
|
||||
const cardContainerRef = ref<HTMLElement>()
|
||||
|
||||
@@ -62,10 +62,8 @@ const {
|
||||
|
||||
const emit = defineEmits<{
|
||||
zoom: []
|
||||
'asset-deleted': []
|
||||
'asset-deleting': [boolean]
|
||||
'bulk-download': [assets: AssetItem[]]
|
||||
'bulk-delete': [assets: AssetItem[]]
|
||||
'delete-assets': [assets: AssetItem[]]
|
||||
}>()
|
||||
|
||||
const contextMenu = ref<InstanceType<typeof ContextMenu>>()
|
||||
@@ -153,7 +151,7 @@ const contextMenuItems = computed<MenuItem[]>(() => {
|
||||
items.push({
|
||||
label: t('mediaAsset.selection.deleteSelectedAll'),
|
||||
icon: 'icon-[lucide--trash-2]',
|
||||
command: () => emit('bulk-delete', selectedAssets)
|
||||
command: () => emit('delete-assets', selectedAssets)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -220,16 +218,7 @@ const contextMenuItems = computed<MenuItem[]>(() => {
|
||||
items.push({
|
||||
label: t('mediaAsset.actions.delete'),
|
||||
icon: 'icon-[lucide--trash-2]',
|
||||
command: async () => {
|
||||
if (asset) {
|
||||
const success = await actions.confirmDelete(asset, (deleting) => {
|
||||
emit('asset-deleting', deleting)
|
||||
})
|
||||
if (success) {
|
||||
emit('asset-deleted')
|
||||
}
|
||||
}
|
||||
}
|
||||
command: () => emit('delete-assets', [asset])
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -137,74 +137,27 @@ export function useMediaAssetActions() {
|
||||
/**
|
||||
* Show confirmation dialog and delete asset if confirmed
|
||||
* @param asset The asset to delete
|
||||
* @param onDeleting Optional callback called with true when deletion starts and false when complete
|
||||
* @returns true if the asset was deleted, false otherwise
|
||||
*/
|
||||
const confirmDelete = async (
|
||||
asset: AssetItem,
|
||||
onDeleting?: (deleting: boolean) => void
|
||||
): Promise<boolean> => {
|
||||
const assetType = getAssetType(asset)
|
||||
|
||||
return new Promise((resolve) => {
|
||||
dialogStore.showDialog({
|
||||
key: 'delete-asset-confirmation',
|
||||
title: t('mediaAsset.deleteAssetTitle'),
|
||||
component: ConfirmationDialogContent,
|
||||
props: {
|
||||
message: t('mediaAsset.deleteAssetDescription'),
|
||||
type: 'delete',
|
||||
itemList: [asset.name],
|
||||
onConfirm: async () => {
|
||||
onDeleting?.(true)
|
||||
try {
|
||||
const success = await deleteAsset(asset, assetType)
|
||||
resolve(success)
|
||||
} finally {
|
||||
onDeleting?.(false)
|
||||
}
|
||||
},
|
||||
onCancel: () => {
|
||||
resolve(false)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
return deleteMultipleAssets([asset], onDeleting)
|
||||
}
|
||||
|
||||
const deleteAsset = async (asset: AssetItem, assetType: string) => {
|
||||
const assetsStore = useAssetsStore()
|
||||
|
||||
try {
|
||||
// Perform the deletion
|
||||
await deleteAssetApi(asset, assetType)
|
||||
|
||||
// Update the appropriate store based on asset type
|
||||
if (assetType === 'output') {
|
||||
await assetsStore.updateHistory()
|
||||
} else {
|
||||
await assetsStore.updateInputs()
|
||||
}
|
||||
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: t('g.success'),
|
||||
detail: t('mediaAsset.assetDeletedSuccessfully'),
|
||||
life: 2000
|
||||
})
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Failed to delete asset:', error)
|
||||
const errorMessage = error instanceof Error ? error.message : ''
|
||||
const isCloudWarning = errorMessage.includes('Cloud')
|
||||
|
||||
toast.add({
|
||||
severity: isCloudWarning ? 'warn' : 'error',
|
||||
summary: isCloudWarning ? t('g.warning') : t('g.error'),
|
||||
detail: errorMessage || t('mediaAsset.failedToDeleteAsset'),
|
||||
life: 3000
|
||||
})
|
||||
return false
|
||||
}
|
||||
/**
|
||||
* Delete a single asset with confirmation
|
||||
* @param asset The asset to delete
|
||||
* @param onDeleting Optional callback called with true when deletion starts and false when complete
|
||||
* @returns true if the asset was deleted, false otherwise
|
||||
*/
|
||||
const deleteAsset = async (
|
||||
asset: AssetItem,
|
||||
onDeleting?: (deleting: boolean) => void
|
||||
): Promise<boolean> => {
|
||||
return deleteMultipleAssets([asset], onDeleting)
|
||||
}
|
||||
|
||||
const copyJobId = async (asset?: AssetItem) => {
|
||||
@@ -374,27 +327,33 @@ export function useMediaAssetActions() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete multiple assets with confirmation dialog
|
||||
* Delete one or more assets with confirmation dialog
|
||||
* @param assets Array of assets to delete
|
||||
* @param onDeleting Optional callback called with true when deletion starts (after confirmation) and false when complete
|
||||
* @returns true if all assets were deleted successfully, false otherwise
|
||||
*/
|
||||
const deleteMultipleAssets = async (
|
||||
assets: AssetItem[],
|
||||
onDeleting?: (deleting: boolean) => void
|
||||
) => {
|
||||
if (!assets || assets.length === 0) return
|
||||
): Promise<boolean> => {
|
||||
if (!assets || assets.length === 0) return false
|
||||
|
||||
const assetsStore = useAssetsStore()
|
||||
const isSingleAsset = assets.length === 1
|
||||
|
||||
return new Promise<void>((resolve) => {
|
||||
return new Promise<boolean>((resolve) => {
|
||||
dialogStore.showDialog({
|
||||
key: 'delete-multiple-assets-confirmation',
|
||||
title: t('mediaAsset.deleteSelectedTitle'),
|
||||
key: 'delete-assets-confirmation',
|
||||
title: isSingleAsset
|
||||
? t('mediaAsset.deleteAssetTitle')
|
||||
: t('mediaAsset.deleteSelectedTitle'),
|
||||
component: ConfirmationDialogContent,
|
||||
props: {
|
||||
message: t('mediaAsset.deleteSelectedDescription', {
|
||||
count: assets.length
|
||||
}),
|
||||
message: isSingleAsset
|
||||
? t('mediaAsset.deleteAssetDescription')
|
||||
: t('mediaAsset.deleteSelectedDescription', {
|
||||
count: assets.length
|
||||
}),
|
||||
type: 'delete',
|
||||
itemList: assets.map((asset) => asset.name),
|
||||
onConfirm: async () => {
|
||||
@@ -407,6 +366,8 @@ export function useMediaAssetActions() {
|
||||
)
|
||||
)
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000))
|
||||
|
||||
// Count successes and failures
|
||||
const succeeded = results.filter(
|
||||
(r) => r.status === 'fulfilled'
|
||||
@@ -442,17 +403,29 @@ export function useMediaAssetActions() {
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: t('g.success'),
|
||||
detail: t('mediaAsset.selection.assetsDeletedSuccessfully', {
|
||||
count: succeeded
|
||||
}),
|
||||
detail: isSingleAsset
|
||||
? t('mediaAsset.assetDeletedSuccessfully')
|
||||
: t('mediaAsset.selection.assetsDeletedSuccessfully', {
|
||||
count: succeeded
|
||||
}),
|
||||
life: 2000
|
||||
})
|
||||
} else if (succeeded === 0) {
|
||||
// All failed
|
||||
const errorMessage =
|
||||
failed[0].status === 'rejected'
|
||||
? (failed[0].reason as Error)?.message
|
||||
: ''
|
||||
const isCloudWarning = errorMessage?.includes('Cloud')
|
||||
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('mediaAsset.selection.failedToDeleteAssets'),
|
||||
severity: isCloudWarning ? 'warn' : 'error',
|
||||
summary: isCloudWarning ? t('g.warning') : t('g.error'),
|
||||
detail:
|
||||
errorMessage ||
|
||||
(isSingleAsset
|
||||
? t('mediaAsset.failedToDeleteAsset')
|
||||
: t('mediaAsset.selection.failedToDeleteAssets')),
|
||||
life: 3000
|
||||
})
|
||||
} else {
|
||||
@@ -467,22 +440,26 @@ export function useMediaAssetActions() {
|
||||
life: 3000
|
||||
})
|
||||
}
|
||||
|
||||
resolve(failed.length === 0)
|
||||
} catch (error) {
|
||||
console.error('Failed to delete assets:', error)
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('mediaAsset.selection.failedToDeleteAssets'),
|
||||
detail: isSingleAsset
|
||||
? t('mediaAsset.failedToDeleteAsset')
|
||||
: t('mediaAsset.selection.failedToDeleteAssets'),
|
||||
life: 3000
|
||||
})
|
||||
|
||||
resolve(false)
|
||||
} finally {
|
||||
onDeleting?.(false)
|
||||
}
|
||||
|
||||
resolve()
|
||||
},
|
||||
onCancel: () => {
|
||||
resolve()
|
||||
resolve(false)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user