feature: delete asset

This commit is contained in:
Jin Yi
2025-10-22 15:12:46 +09:00
parent 354c05ea68
commit 754eb9c3b4
7 changed files with 185 additions and 19 deletions

View File

@@ -64,6 +64,7 @@
@click="handleAssetSelect(item)"
@zoom="handleZoomClick(item)"
@output-count-click="enterFolderView(item)"
@asset-deleted="refreshAssets"
/>
</template>
</VirtualGrid>
@@ -218,6 +219,7 @@ const mediaAssetsWithKey = computed(() => {
const refreshAssets = async () => {
const files = await fetchMediaList(activeTab.value)
mediaAssets.value = files
selectedAsset.value = null // Clear selection after refresh
if (error.value) {
console.error('Failed to refresh assets:', error.value)
}

View File

@@ -2053,6 +2053,8 @@
"browseAssets": "Browse Assets",
"noAssetsFound": "No assets found",
"tryAdjustingFilters": "Try adjusting your search or filters",
"deleteAssetTitle": "Delete this asset?",
"deleteAssetDescription": "This asset will be permanently removed.",
"loadingModels": "Loading {type}...",
"connectionError": "Please check your connection and try again",
"failedToCreateNode": "Failed to create node. Please try again or check console for details.",

View File

@@ -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
@@ -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>
@@ -20,31 +24,63 @@
<script setup lang="ts">
import { computed, inject } from 'vue'
import { useI18n } from 'vue-i18n'
import IconButton from '@/components/button/IconButton.vue'
import IconGroup from '@/components/button/IconGroup.vue'
import MoreButton from '@/components/button/MoreButton.vue'
import ConfirmationDialogContent from '@/components/dialog/content/ConfirmationDialogContent.vue'
import { isCloud } from '@/platform/distribution/types'
import { useDialogStore } from '@/stores/dialogStore'
import { useMediaAssetActions } from '../composables/useMediaAssetActions'
import { MediaAssetKey } from '../schemas/mediaAssetSchema'
import MediaAssetMoreMenu from './MediaAssetMoreMenu.vue'
const { t } = useI18n()
const emit = defineEmits<{
menuStateChanged: [isOpen: boolean]
inspect: []
'asset-deleted': []
}>()
const { asset, context } = inject(MediaAssetKey)!
const actions = useMediaAssetActions()
const dialogStore = useDialogStore()
const assetType = computed(() => {
return context?.value?.type || asset.value?.tags?.[0] || 'output'
})
const showDeleteButton = computed(() => {
return (
assetType.value === 'output' || (assetType.value === 'input' && isCloud)
)
})
const handleDelete = () => {
if (asset.value) {
actions.deleteAsset(asset.value.id)
}
if (!asset.value?.id || !assetType.value) return
dialogStore.showDialog({
key: 'delete-asset-confirmation',
title: t('assetBrowser.deleteAssetTitle'),
component: ConfirmationDialogContent,
props: {
message: t('assetBrowser.deleteAssetDescription'),
type: 'delete',
itemList: [asset.value.name],
onConfirm: async () => {
const success = await actions.deleteAsset(
asset.value!.id,
assetType.value
)
if (success) {
emit('asset-deleted')
}
}
}
})
}
const handleDownload = () => {

View File

@@ -45,6 +45,7 @@
<MediaAssetActions
@menu-state-changed="isMenuOpen = $event"
@inspect="handleZoomClick"
@asset-deleted="handleAssetDelete"
@mouseenter="handleOverlayMouseEnter"
@mouseleave="handleOverlayMouseLeave"
/>
@@ -179,6 +180,7 @@ const { asset, loading, selected, showOutputCount, outputCount } = defineProps<{
const emit = defineEmits<{
zoom: [asset: AssetItem]
'output-count-click': []
'asset-deleted': []
}>()
const cardContainerRef = ref<HTMLElement>()
@@ -338,4 +340,8 @@ const handleImageLoaded = (dimensions: { width: number; height: number }) => {
const handleOutputCountClick = () => {
emit('output-count-click')
}
const handleAssetDelete = () => {
emit('asset-deleted')
}
</script>

View File

@@ -13,6 +13,7 @@
</IconTextButton>
<IconTextButton
v-if="showWorkflowOptions"
type="transparent"
class="dark-theme:text-white"
label="Add to current workflow"
@@ -34,7 +35,7 @@
</template>
</IconTextButton>
<MediaAssetButtonDivider />
<MediaAssetButtonDivider v-if="showWorkflowOptions" />
<IconTextButton
v-if="showWorkflowOptions"
@@ -60,7 +61,7 @@
</template>
</IconTextButton>
<MediaAssetButtonDivider v-if="showWorkflowOptions" />
<MediaAssetButtonDivider v-if="showWorkflowOptions && showCopyJobId" />
<IconTextButton
v-if="showCopyJobId"
@@ -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"
@@ -91,8 +93,12 @@
<script setup lang="ts">
import { computed, inject } from 'vue'
import { useI18n } from 'vue-i18n'
import IconTextButton from '@/components/button/IconTextButton.vue'
import ConfirmationDialogContent from '@/components/dialog/content/ConfirmationDialogContent.vue'
import { isCloud } from '@/platform/distribution/types'
import { useDialogStore } from '@/stores/dialogStore'
import { useMediaAssetActions } from '../composables/useMediaAssetActions'
import { MediaAssetKey } from '../schemas/mediaAssetSchema'
@@ -104,17 +110,32 @@ const { close } = defineProps<{
const emit = defineEmits<{
inspect: []
'asset-deleted': []
}>()
const { asset, context } = inject(MediaAssetKey)!
const actions = useMediaAssetActions()
const dialogStore = useDialogStore()
const { t } = useI18n()
const showWorkflowOptions = computed(() => context.value.type)
const assetType = computed(() => {
return (asset.value as any)?.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 as any)?.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 = () => {
@@ -158,9 +179,29 @@ const handleCopyJobId = async () => {
}
const handleDelete = () => {
if (asset.value) {
actions.deleteAsset(asset.value.id)
}
close()
if (!asset.value?.id || !assetType.value) return
close() // Close the menu first
// Show confirmation dialog
dialogStore.showDialog({
key: 'delete-asset-confirmation',
title: t('assetBrowser.deleteAssetTitle'),
component: ConfirmationDialogContent,
props: {
message: t('assetBrowser.deleteAssetDescription'),
type: 'delete',
itemList: [asset.value.name],
onConfirm: async () => {
const success = await actions.deleteAsset(
asset.value!.id,
assetType.value as 'input' | 'output'
)
if (success) {
emit('asset-deleted')
}
}
}
})
}
</script>

View File

@@ -4,12 +4,14 @@ import { inject } from 'vue'
import { downloadFile } from '@/base/common/downloadUtil'
import { t } from '@/i18n'
import { isCloud } from '@/platform/distribution/types'
import { api } from '@/scripts/api'
import { extractPromptIdFromAssetId } from '@/utils/uuidUtil'
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()
@@ -48,8 +50,64 @@ export function useMediaAssetActions() {
}
}
const deleteAsset = (assetId: string) => {
console.log('Deleting asset:', assetId)
const deleteAsset = async (
assetId: string,
assetType: 'input' | 'output'
) => {
try {
if (assetType === 'output') {
// For output files, delete from history
const promptId = extractPromptIdFromAssetId(assetId)
if (!promptId) {
throw new Error('Unable to extract prompt ID from asset')
}
await api.deleteItem('history', promptId)
toast.add({
severity: 'success',
summary: t('g.success'),
detail: 'Asset deleted successfully',
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:
'Deleting imported files is only supported in cloud version',
life: 3000
})
return false
}
// In cloud environment, use the assets API to delete
await assetService.deleteAsset(assetId)
toast.add({
severity: 'success',
summary: t('g.success'),
detail: 'Asset deleted successfully',
life: 2000
})
return true
}
throw new Error('Unable to determine asset type')
} catch (error) {
console.error('Failed to delete asset:', error)
toast.add({
severity: 'error',
summary: t('g.error'),
detail:
error instanceof Error ? error.message : 'Failed to delete asset',
life: 3000
})
return false
}
}
const playAsset = (assetId: string) => {

View File

@@ -213,13 +213,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
}
}