Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
f042042
feat: Add Media Assets sidebar tab for file management
viva-jinyi Oct 17, 2025
c20ea26
refactor: Apply PR #6112 review feedback for Media Assets feature
viva-jinyi Oct 20, 2025
fffdae1
chore: unexpected export
viva-jinyi Oct 20, 2025
8765a10
feat: Improve media asset display with file format tags and filename …
viva-jinyi Oct 20, 2025
52b2129
feat: Add includePublic parameter to getAssetsByTag API
viva-jinyi Oct 22, 2025
aa3354a
fix: test code
viva-jinyi Oct 22, 2025
cddd8ea
refactor: useQueueStore
viva-jinyi Oct 22, 2025
ea7e910
refactor: Apply review feedback for media assets implementation
viva-jinyi Oct 23, 2025
02a1810
Extract AssetsSidebarTab template and improve UI structure (#6164)
viva-jinyi Oct 23, 2025
5c01e61
feat: Implement centralized AssetsStore for reactive assets updates
viva-jinyi Oct 24, 2025
38885b7
refactor: Apply formatUtil code review feedback and improve type safety
viva-jinyi Oct 24, 2025
fd953c6
[automated] Update test expectations
invalid-email-address Oct 24, 2025
4e2fc4a
feat: Auto-refresh assets on file upload
viva-jinyi Oct 24, 2025
9d28ec8
fix: Add AssetsStore update trigger to WidgetSelectDropdown uploads
viva-jinyi Oct 24, 2025
cb33c8f
refactor:
viva-jinyi Oct 27, 2025
9125459
fix: Prevent gallery index shift when new outputs are generated
viva-jinyi Oct 27, 2025
993f08f
refactor: delete unused export
viva-jinyi Oct 27, 2025
e0a0d9f
refactor: Simplify asset ID handling and remove UUID extraction, Acce…
viva-jinyi Oct 27, 2025
b778cde
feat: implement asset deletion functionality (#6203)
viva-jinyi Oct 27, 2025
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
Prev Previous commit
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)
  • Loading branch information
viva-jinyi committed Oct 29, 2025
commit b778cdefea89737c9874db818393edf92181a174
1 change: 1 addition & 0 deletions src/components/sidebar/tabs/AssetsSidebarTab.vue
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
@click="handleAssetSelect(item)"
@zoom="handleZoomClick(item)"
@output-count-click="enterFolderView(item)"
@asset-deleted="refreshAssets"
/>
</template>
</VirtualGrid>
Expand Down
5 changes: 5 additions & 0 deletions src/locales/en/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -1777,6 +1777,11 @@
}
},
"mediaAsset": {
"deleteAssetTitle": "Delete this asset?",
"deleteAssetDescription": "This asset will be permanently removed.",
"assetDeletedSuccessfully": "Asset deleted successfully",
"deletingImportedFilesCloudOnly": "Deleting imported files is only supported in cloud version",
"failedToDeleteAsset": "Failed to delete asset",
"jobIdToast": {
"jobIdCopied": "Job ID copied to clipboard",
"jobIdCopyFailed": "Failed to copy Job ID",
Expand Down
27 changes: 21 additions & 6 deletions src/platform/assets/components/MediaAssetActions.vue
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
<template>
<IconGroup>
<IconButton size="sm" @click="handleDelete">
<IconButton v-if="showDeleteButton" size="sm" @click="handleDelete">
<i class="icon-[lucide--trash-2] size-4" />
</IconButton>
<IconButton v-if="assetType !== 'input'" size="sm" @click="handleDownload">
<IconButton size="sm" @click="handleDownload">
<i class="icon-[lucide--download] size-4" />
</IconButton>
<MoreButton
Expand All @@ -12,7 +12,11 @@
@menu-closed="emit('menuStateChanged', false)"
>
<template #default="{ close }">
<MediaAssetMoreMenu :close="close" @inspect="emit('inspect')" />
<MediaAssetMoreMenu
:close="close"
@inspect="emit('inspect')"
@asset-deleted="emit('asset-deleted')"
/>
</template>
</MoreButton>
</IconGroup>
Expand All @@ -24,6 +28,7 @@ import { computed, inject } from 'vue'
import IconButton from '@/components/button/IconButton.vue'
import IconGroup from '@/components/button/IconGroup.vue'
import MoreButton from '@/components/button/MoreButton.vue'
import { isCloud } from '@/platform/distribution/types'

import { useMediaAssetActions } from '../composables/useMediaAssetActions'
import { MediaAssetKey } from '../schemas/mediaAssetSchema'
Expand All @@ -32,6 +37,7 @@ import MediaAssetMoreMenu from './MediaAssetMoreMenu.vue'
const emit = defineEmits<{
menuStateChanged: [isOpen: boolean]
inspect: []
'asset-deleted': []
}>()

const { asset, context } = inject(MediaAssetKey)!
Expand All @@ -41,9 +47,18 @@ const assetType = computed(() => {
return context?.value?.type || asset.value?.tags?.[0] || 'output'
})

const handleDelete = () => {
if (asset.value) {
actions.deleteAsset(asset.value.id)
const showDeleteButton = computed(() => {
return (
assetType.value === 'output' || (assetType.value === 'input' && isCloud)
)
})

const handleDelete = async () => {
if (!asset.value) return

const success = await actions.confirmDelete(asset.value)
if (success) {
emit('asset-deleted')
}
}

Expand Down
6 changes: 6 additions & 0 deletions src/platform/assets/components/MediaAssetCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
<MediaAssetActions
@menu-state-changed="isMenuOpen = $event"
@inspect="handleZoomClick"
@asset-deleted="handleAssetDelete"
@mouseenter="handleOverlayMouseEnter"
@mouseleave="handleOverlayMouseLeave"
/>
Expand Down Expand Up @@ -184,6 +185,7 @@ const { asset, loading, selected, showOutputCount, outputCount } = defineProps<{
const emit = defineEmits<{
zoom: [asset: AssetItem]
'output-count-click': []
'asset-deleted': []
}>()

const cardContainerRef = ref<HTMLElement>()
Expand Down Expand Up @@ -337,4 +339,8 @@ const handleImageLoaded = (width: number, height: number) => {
const handleOutputCountClick = () => {
emit('output-count-click')
}

const handleAssetDelete = () => {
emit('asset-deleted')
}
</script>
40 changes: 30 additions & 10 deletions src/platform/assets/components/MediaAssetMoreMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
</IconTextButton>

<IconTextButton
v-if="showWorkflowOptions"
type="transparent"
class="dark-theme:text-white"
label="Add to current workflow"
Expand All @@ -34,7 +35,7 @@
</template>
</IconTextButton>

<MediaAssetButtonDivider />
<MediaAssetButtonDivider v-if="showWorkflowOptions" />

<IconTextButton
v-if="showWorkflowOptions"
Expand All @@ -60,7 +61,7 @@
</template>
</IconTextButton>

<MediaAssetButtonDivider v-if="showWorkflowOptions" />
<MediaAssetButtonDivider v-if="showWorkflowOptions && showCopyJobId" />

<IconTextButton
v-if="showCopyJobId"
Expand All @@ -74,9 +75,10 @@
</template>
</IconTextButton>

<MediaAssetButtonDivider v-if="showCopyJobId" />
<MediaAssetButtonDivider v-if="showCopyJobId && showDeleteButton" />

<IconTextButton
v-if="showDeleteButton"
type="transparent"
class="dark-theme:text-white"
label="Delete"
Expand All @@ -93,6 +95,7 @@
import { computed, inject } from 'vue'

import IconTextButton from '@/components/button/IconTextButton.vue'
import { isCloud } from '@/platform/distribution/types'

import { useMediaAssetActions } from '../composables/useMediaAssetActions'
import { MediaAssetKey } from '../schemas/mediaAssetSchema'
Expand All @@ -104,17 +107,30 @@ const { close } = defineProps<{

const emit = defineEmits<{
inspect: []
'asset-deleted': []
}>()

const { asset, context } = inject(MediaAssetKey)!
const actions = useMediaAssetActions()

const showWorkflowOptions = computed(() => context.value.type)
const assetType = computed(() => {
return asset.value?.tags?.[0] || context.value?.type || 'output'
})

const showWorkflowOptions = computed(() => assetType.value === 'output')

// Only show Copy Job ID for output assets (not for imported/input assets)
const showCopyJobId = computed(() => {
const assetType = asset.value?.tags?.[0] || context.value?.type
return assetType !== 'input'
return assetType.value !== 'input'
})

// Delete button should be shown for:
// - All output files (can be deleted via history)
// - Input files only in cloud environment
const showDeleteButton = computed(() => {
return (
assetType.value === 'output' || (assetType.value === 'input' && isCloud)
)
})

const handleInspect = () => {
Expand Down Expand Up @@ -157,10 +173,14 @@ const handleCopyJobId = async () => {
close()
}

const handleDelete = () => {
if (asset.value) {
actions.deleteAsset(asset.value.id)
const handleDelete = async () => {
if (!asset.value) return

close() // Close the menu first

const success = await actions.confirmDelete(asset.value)
if (success) {
emit('asset-deleted')
}
close()
}
</script>
102 changes: 100 additions & 2 deletions src/platform/assets/composables/useMediaAssetActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,23 @@
import { useToast } from 'primevue/usetoast'
import { inject } from 'vue'

import ConfirmationDialogContent from '@/components/dialog/content/ConfirmationDialogContent.vue'
import { downloadFile } from '@/base/common/downloadUtil'
import { t } from '@/i18n'
import { isCloud } from '@/platform/distribution/types'
import { api } from '@/scripts/api'
import { getOutputAssetMetadata } from '../schemas/assetMetadataSchema'
import { useAssetsStore } from '@/stores/assetsStore'
import { useDialogStore } from '@/stores/dialogStore'

import type { AssetItem } from '../schemas/assetSchema'
import type { AssetMeta } from '../schemas/mediaAssetSchema'
import { MediaAssetKey } from '../schemas/mediaAssetSchema'
import { assetService } from '../services/assetService'

export function useMediaAssetActions() {
const toast = useToast()
const dialogStore = useDialogStore()
const mediaContext = inject(MediaAssetKey, null)

const selectAsset = (asset: AssetMeta) => {
Expand Down Expand Up @@ -47,8 +54,98 @@ export function useMediaAssetActions() {
}
}

const deleteAsset = (assetId: string) => {
console.log('Deleting asset:', assetId)
/**
* Show confirmation dialog and delete asset if confirmed
* @param asset The asset to delete
* @returns true if the asset was deleted, false otherwise
*/
const confirmDelete = async (asset: AssetItem): Promise<boolean> => {
const assetType = asset.tags?.[0] || 'output'

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 () => {
const success = await deleteAsset(asset, assetType)
resolve(success)
},
onCancel: () => {
resolve(false)
}
}
})
})
}

const deleteAsset = async (asset: AssetItem, assetType: string) => {
const assetsStore = useAssetsStore()

try {
if (assetType === 'output') {
// For output files, delete from history
const promptId =
asset.id || getOutputAssetMetadata(asset.user_metadata)?.promptId
if (!promptId) {
throw new Error('Unable to extract prompt ID from asset')
}

await api.deleteItem('history', promptId)

// Update history assets in store after deletion
await assetsStore.updateHistory()

toast.add({
severity: 'success',
summary: t('g.success'),
detail: t('mediaAsset.assetDeletedSuccessfully'),
life: 2000
})
return true
} else {
// For input files, only allow deletion in cloud environment
if (!isCloud) {
toast.add({
severity: 'warn',
summary: t('g.warning'),
detail: t('mediaAsset.deletingImportedFilesCloudOnly'),
life: 3000
})
return false
}

// In cloud environment, use the assets API to delete
await assetService.deleteAsset(asset.id)

// Update input assets in store after deletion
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)
toast.add({
severity: 'error',
summary: t('g.error'),
detail:
error instanceof Error
? error.message
: t('mediaAsset.failedToDeleteAsset'),
life: 3000
})
return false
}
}

const playAsset = (assetId: string) => {
Expand Down Expand Up @@ -110,6 +207,7 @@ export function useMediaAssetActions() {
return {
selectAsset,
downloadAsset,
confirmDelete,
deleteAsset,
playAsset,
copyJobId,
Expand Down
23 changes: 22 additions & 1 deletion src/platform/assets/services/assetService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,13 +218,34 @@ function createAssetService() {
)
}

/**
* Deletes an asset by ID
* Only available in cloud environment
*
* @param id - The asset ID (UUID)
* @returns Promise<void>
* @throws Error if deletion fails
*/
async function deleteAsset(id: string): Promise<void> {
const res = await api.fetchApi(`${ASSETS_ENDPOINT}/${id}`, {
method: 'DELETE'
})

if (!res.ok) {
throw new Error(
`Unable to delete asset ${id}: Server returned ${res.status}`
)
}
}

return {
getAssetModelFolders,
getAssetModels,
isAssetBrowserEligible,
getAssetsForNodeType,
getAssetDetails,
getAssetsByTag
getAssetsByTag,
deleteAsset
}
}

Expand Down
Loading