Skip to content

Commit 208e522

Browse files
viva-jinyiDrJKL
authored andcommitted
feat: implement asset deletion functionality (#6203)
## Summary - Implement asset deletion for media assets - Add delete confirmation dialog - Support both cloud and internal asset deletion - Refresh assets list after successful deletion ## Changes - Add delete button to MediaAssetActions component - Implement deleteAsset method in useMediaAssetActions - Add confirmation dialog before deletion - Handle asset-deleted event to refresh the list - Refactor to use QueueStore.tasks for proper grouping ## Test plan - [x] Delete output assets in internal mode - [x] Delete input/output assets in cloud mode - [x] Verify confirmation dialog appears - [x] Check assets list refreshes after deletion - [x] Test folder view functionality [screen-capture (3).webm](https://github.com/user-attachments/assets/3306262e-627a-4db0-90c1-fd59ba3abf7c)
1 parent 32e2f3f commit 208e522

File tree

7 files changed

+185
-19
lines changed

7 files changed

+185
-19
lines changed

src/components/sidebar/tabs/AssetsSidebarTab.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
@click="handleAssetSelect(item)"
6161
@zoom="handleZoomClick(item)"
6262
@output-count-click="enterFolderView(item)"
63+
@asset-deleted="refreshAssets"
6364
/>
6465
</template>
6566
</VirtualGrid>

src/locales/en/main.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1777,6 +1777,11 @@
17771777
}
17781778
},
17791779
"mediaAsset": {
1780+
"deleteAssetTitle": "Delete this asset?",
1781+
"deleteAssetDescription": "This asset will be permanently removed.",
1782+
"assetDeletedSuccessfully": "Asset deleted successfully",
1783+
"deletingImportedFilesCloudOnly": "Deleting imported files is only supported in cloud version",
1784+
"failedToDeleteAsset": "Failed to delete asset",
17801785
"jobIdToast": {
17811786
"jobIdCopied": "Job ID copied to clipboard",
17821787
"jobIdCopyFailed": "Failed to copy Job ID",

src/platform/assets/components/MediaAssetActions.vue

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
<template>
22
<IconGroup>
3-
<IconButton size="sm" @click="handleDelete">
3+
<IconButton v-if="showDeleteButton" size="sm" @click="handleDelete">
44
<i class="icon-[lucide--trash-2] size-4" />
55
</IconButton>
6-
<IconButton v-if="assetType !== 'input'" size="sm" @click="handleDownload">
6+
<IconButton size="sm" @click="handleDownload">
77
<i class="icon-[lucide--download] size-4" />
88
</IconButton>
99
<MoreButton
@@ -12,7 +12,11 @@
1212
@menu-closed="emit('menuStateChanged', false)"
1313
>
1414
<template #default="{ close }">
15-
<MediaAssetMoreMenu :close="close" @inspect="emit('inspect')" />
15+
<MediaAssetMoreMenu
16+
:close="close"
17+
@inspect="emit('inspect')"
18+
@asset-deleted="emit('asset-deleted')"
19+
/>
1620
</template>
1721
</MoreButton>
1822
</IconGroup>
@@ -24,6 +28,7 @@ import { computed, inject } from 'vue'
2428
import IconButton from '@/components/button/IconButton.vue'
2529
import IconGroup from '@/components/button/IconGroup.vue'
2630
import MoreButton from '@/components/button/MoreButton.vue'
31+
import { isCloud } from '@/platform/distribution/types'
2732
2833
import { useMediaAssetActions } from '../composables/useMediaAssetActions'
2934
import { MediaAssetKey } from '../schemas/mediaAssetSchema'
@@ -32,6 +37,7 @@ import MediaAssetMoreMenu from './MediaAssetMoreMenu.vue'
3237
const emit = defineEmits<{
3338
menuStateChanged: [isOpen: boolean]
3439
inspect: []
40+
'asset-deleted': []
3541
}>()
3642
3743
const { asset, context } = inject(MediaAssetKey)!
@@ -41,9 +47,18 @@ const assetType = computed(() => {
4147
return context?.value?.type || asset.value?.tags?.[0] || 'output'
4248
})
4349
44-
const handleDelete = () => {
45-
if (asset.value) {
46-
actions.deleteAsset(asset.value.id)
50+
const showDeleteButton = computed(() => {
51+
return (
52+
assetType.value === 'output' || (assetType.value === 'input' && isCloud)
53+
)
54+
})
55+
56+
const handleDelete = async () => {
57+
if (!asset.value) return
58+
59+
const success = await actions.confirmDelete(asset.value)
60+
if (success) {
61+
emit('asset-deleted')
4762
}
4863
}
4964

src/platform/assets/components/MediaAssetCard.vue

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
<MediaAssetActions
5353
@menu-state-changed="isMenuOpen = $event"
5454
@inspect="handleZoomClick"
55+
@asset-deleted="handleAssetDelete"
5556
@mouseenter="handleOverlayMouseEnter"
5657
@mouseleave="handleOverlayMouseLeave"
5758
/>
@@ -184,6 +185,7 @@ const { asset, loading, selected, showOutputCount, outputCount } = defineProps<{
184185
const emit = defineEmits<{
185186
zoom: [asset: AssetItem]
186187
'output-count-click': []
188+
'asset-deleted': []
187189
}>()
188190
189191
const cardContainerRef = ref<HTMLElement>()
@@ -337,4 +339,8 @@ const handleImageLoaded = (width: number, height: number) => {
337339
const handleOutputCountClick = () => {
338340
emit('output-count-click')
339341
}
342+
343+
const handleAssetDelete = () => {
344+
emit('asset-deleted')
345+
}
340346
</script>

src/platform/assets/components/MediaAssetMoreMenu.vue

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
</IconTextButton>
1414

1515
<IconTextButton
16+
v-if="showWorkflowOptions"
1617
type="transparent"
1718
class="dark-theme:text-white"
1819
label="Add to current workflow"
@@ -34,7 +35,7 @@
3435
</template>
3536
</IconTextButton>
3637

37-
<MediaAssetButtonDivider />
38+
<MediaAssetButtonDivider v-if="showWorkflowOptions" />
3839

3940
<IconTextButton
4041
v-if="showWorkflowOptions"
@@ -60,7 +61,7 @@
6061
</template>
6162
</IconTextButton>
6263

63-
<MediaAssetButtonDivider v-if="showWorkflowOptions" />
64+
<MediaAssetButtonDivider v-if="showWorkflowOptions && showCopyJobId" />
6465

6566
<IconTextButton
6667
v-if="showCopyJobId"
@@ -74,9 +75,10 @@
7475
</template>
7576
</IconTextButton>
7677

77-
<MediaAssetButtonDivider v-if="showCopyJobId" />
78+
<MediaAssetButtonDivider v-if="showCopyJobId && showDeleteButton" />
7879

7980
<IconTextButton
81+
v-if="showDeleteButton"
8082
type="transparent"
8183
class="dark-theme:text-white"
8284
label="Delete"
@@ -93,6 +95,7 @@
9395
import { computed, inject } from 'vue'
9496
9597
import IconTextButton from '@/components/button/IconTextButton.vue'
98+
import { isCloud } from '@/platform/distribution/types'
9699
97100
import { useMediaAssetActions } from '../composables/useMediaAssetActions'
98101
import { MediaAssetKey } from '../schemas/mediaAssetSchema'
@@ -104,17 +107,30 @@ const { close } = defineProps<{
104107
105108
const emit = defineEmits<{
106109
inspect: []
110+
'asset-deleted': []
107111
}>()
108112
109113
const { asset, context } = inject(MediaAssetKey)!
110114
const actions = useMediaAssetActions()
111115
112-
const showWorkflowOptions = computed(() => context.value.type)
116+
const assetType = computed(() => {
117+
return asset.value?.tags?.[0] || context.value?.type || 'output'
118+
})
119+
120+
const showWorkflowOptions = computed(() => assetType.value === 'output')
113121
114122
// Only show Copy Job ID for output assets (not for imported/input assets)
115123
const showCopyJobId = computed(() => {
116-
const assetType = asset.value?.tags?.[0] || context.value?.type
117-
return assetType !== 'input'
124+
return assetType.value !== 'input'
125+
})
126+
127+
// Delete button should be shown for:
128+
// - All output files (can be deleted via history)
129+
// - Input files only in cloud environment
130+
const showDeleteButton = computed(() => {
131+
return (
132+
assetType.value === 'output' || (assetType.value === 'input' && isCloud)
133+
)
118134
})
119135
120136
const handleInspect = () => {
@@ -157,10 +173,14 @@ const handleCopyJobId = async () => {
157173
close()
158174
}
159175
160-
const handleDelete = () => {
161-
if (asset.value) {
162-
actions.deleteAsset(asset.value.id)
176+
const handleDelete = async () => {
177+
if (!asset.value) return
178+
179+
close() // Close the menu first
180+
181+
const success = await actions.confirmDelete(asset.value)
182+
if (success) {
183+
emit('asset-deleted')
163184
}
164-
close()
165185
}
166186
</script>

src/platform/assets/composables/useMediaAssetActions.ts

Lines changed: 100 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,23 @@
22
import { useToast } from 'primevue/usetoast'
33
import { inject } from 'vue'
44

5+
import ConfirmationDialogContent from '@/components/dialog/content/ConfirmationDialogContent.vue'
56
import { downloadFile } from '@/base/common/downloadUtil'
67
import { t } from '@/i18n'
8+
import { isCloud } from '@/platform/distribution/types'
79
import { api } from '@/scripts/api'
810
import { getOutputAssetMetadata } from '../schemas/assetMetadataSchema'
11+
import { useAssetsStore } from '@/stores/assetsStore'
12+
import { useDialogStore } from '@/stores/dialogStore'
913

14+
import type { AssetItem } from '../schemas/assetSchema'
1015
import type { AssetMeta } from '../schemas/mediaAssetSchema'
1116
import { MediaAssetKey } from '../schemas/mediaAssetSchema'
17+
import { assetService } from '../services/assetService'
1218

1319
export function useMediaAssetActions() {
1420
const toast = useToast()
21+
const dialogStore = useDialogStore()
1522
const mediaContext = inject(MediaAssetKey, null)
1623

1724
const selectAsset = (asset: AssetMeta) => {
@@ -47,8 +54,98 @@ export function useMediaAssetActions() {
4754
}
4855
}
4956

50-
const deleteAsset = (assetId: string) => {
51-
console.log('Deleting asset:', assetId)
57+
/**
58+
* Show confirmation dialog and delete asset if confirmed
59+
* @param asset The asset to delete
60+
* @returns true if the asset was deleted, false otherwise
61+
*/
62+
const confirmDelete = async (asset: AssetItem): Promise<boolean> => {
63+
const assetType = asset.tags?.[0] || 'output'
64+
65+
return new Promise((resolve) => {
66+
dialogStore.showDialog({
67+
key: 'delete-asset-confirmation',
68+
title: t('mediaAsset.deleteAssetTitle'),
69+
component: ConfirmationDialogContent,
70+
props: {
71+
message: t('mediaAsset.deleteAssetDescription'),
72+
type: 'delete',
73+
itemList: [asset.name],
74+
onConfirm: async () => {
75+
const success = await deleteAsset(asset, assetType)
76+
resolve(success)
77+
},
78+
onCancel: () => {
79+
resolve(false)
80+
}
81+
}
82+
})
83+
})
84+
}
85+
86+
const deleteAsset = async (asset: AssetItem, assetType: string) => {
87+
const assetsStore = useAssetsStore()
88+
89+
try {
90+
if (assetType === 'output') {
91+
// For output files, delete from history
92+
const promptId =
93+
asset.id || getOutputAssetMetadata(asset.user_metadata)?.promptId
94+
if (!promptId) {
95+
throw new Error('Unable to extract prompt ID from asset')
96+
}
97+
98+
await api.deleteItem('history', promptId)
99+
100+
// Update history assets in store after deletion
101+
await assetsStore.updateHistory()
102+
103+
toast.add({
104+
severity: 'success',
105+
summary: t('g.success'),
106+
detail: t('mediaAsset.assetDeletedSuccessfully'),
107+
life: 2000
108+
})
109+
return true
110+
} else {
111+
// For input files, only allow deletion in cloud environment
112+
if (!isCloud) {
113+
toast.add({
114+
severity: 'warn',
115+
summary: t('g.warning'),
116+
detail: t('mediaAsset.deletingImportedFilesCloudOnly'),
117+
life: 3000
118+
})
119+
return false
120+
}
121+
122+
// In cloud environment, use the assets API to delete
123+
await assetService.deleteAsset(asset.id)
124+
125+
// Update input assets in store after deletion
126+
await assetsStore.updateInputs()
127+
128+
toast.add({
129+
severity: 'success',
130+
summary: t('g.success'),
131+
detail: t('mediaAsset.assetDeletedSuccessfully'),
132+
life: 2000
133+
})
134+
return true
135+
}
136+
} catch (error) {
137+
console.error('Failed to delete asset:', error)
138+
toast.add({
139+
severity: 'error',
140+
summary: t('g.error'),
141+
detail:
142+
error instanceof Error
143+
? error.message
144+
: t('mediaAsset.failedToDeleteAsset'),
145+
life: 3000
146+
})
147+
return false
148+
}
52149
}
53150

54151
const playAsset = (assetId: string) => {
@@ -110,6 +207,7 @@ export function useMediaAssetActions() {
110207
return {
111208
selectAsset,
112209
downloadAsset,
210+
confirmDelete,
113211
deleteAsset,
114212
playAsset,
115213
copyJobId,

src/platform/assets/services/assetService.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,13 +218,34 @@ function createAssetService() {
218218
)
219219
}
220220

221+
/**
222+
* Deletes an asset by ID
223+
* Only available in cloud environment
224+
*
225+
* @param id - The asset ID (UUID)
226+
* @returns Promise<void>
227+
* @throws Error if deletion fails
228+
*/
229+
async function deleteAsset(id: string): Promise<void> {
230+
const res = await api.fetchApi(`${ASSETS_ENDPOINT}/${id}`, {
231+
method: 'DELETE'
232+
})
233+
234+
if (!res.ok) {
235+
throw new Error(
236+
`Unable to delete asset ${id}: Server returned ${res.status}`
237+
)
238+
}
239+
}
240+
221241
return {
222242
getAssetModelFolders,
223243
getAssetModels,
224244
isAssetBrowserEligible,
225245
getAssetsForNodeType,
226246
getAssetDetails,
227-
getAssetsByTag
247+
getAssetsByTag,
248+
deleteAsset
228249
}
229250
}
230251

0 commit comments

Comments
 (0)