UX: Add progress and confirmation to deletion

This commit is contained in:
DrJKL
2025-12-01 13:09:08 -08:00
parent a5bd37ef56
commit 64a7955eca
5 changed files with 57 additions and 14 deletions

View File

@@ -2,12 +2,14 @@
<section class="w-full flex gap-2 justify-end px-2 pb-2">
<TextButton
:label="cancelTextX"
:disabled
type="transparent"
autofocus
@click="$emit('cancel')"
/>
<TextButton
:label="confirmTextX"
:disabled
type="transparent"
:class="confirmClass"
@click="$emit('confirm')"
@@ -15,17 +17,19 @@
</section>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { computed, toValue } from 'vue'
import type { MaybeRefOrGetter } from 'vue'
import { useI18n } from 'vue-i18n'
import TextButton from '@/components/button/TextButton.vue'
const { t } = useI18n()
const { cancelText, confirmText, confirmClass } = defineProps<{
const { cancelText, confirmText, confirmClass, optionsDisabled } = defineProps<{
cancelText?: string
confirmText?: string
confirmClass?: string
optionsDisabled?: MaybeRefOrGetter<boolean>
}>()
defineEmits<{
@@ -35,4 +39,5 @@ defineEmits<{
const confirmTextX = computed(() => confirmText || t('g.confirm'))
const cancelTextX = computed(() => cancelText || t('g.cancel'))
const disabled = computed(() => toValue(optionsDisabled))
</script>

View File

@@ -163,6 +163,7 @@ export const i18n = createI18n({
legacy: false,
locale: navigator.language.split('-')[0] || 'en',
fallbackLocale: 'en',
escapeParameter: true,
messages,
// Ignore warnings for locale options as each option is in its own language.
// e.g. "English", "中文", "Русский", "日本語", "한국어", "Français", "Español"

View File

@@ -2154,7 +2154,10 @@
},
"deletion": {
"header": "Delete this model?",
"body": "This model will be permanently removed from your library."
"body": "This model will be permanently removed from your library.",
"inProgress": "Deleting {asset}...",
"complete": "{asset} has been deleted.",
"failed": "{asset} could not deleted."
}
},
"mediaAsset": {

View File

@@ -1,10 +1,11 @@
<template>
<div
v-if="!deletedLocal"
data-component-id="AssetCard"
:data-asset-id="asset.id"
:aria-labelledby="titleId"
:aria-describedby="descId"
tabindex="0"
:tabindex="interactive ? 0 : -1"
:class="
cn(
'rounded-2xl overflow-hidden transition-all duration-200 bg-modal-card-background p-2 gap-2 flex flex-col h-full',
@@ -24,6 +25,7 @@
<img
v-else
:src="asset.preview_url"
:alt="asset.name"
class="size-full object-contain cursor-pointer"
role="button"
@click.self="interactive && $emit('select', asset)"
@@ -154,7 +156,8 @@ const titleId = useId()
const descId = useId()
const isEditing = ref(false)
const newNameRef = ref<string>() // TEMPORARY: Replace with actual response from API
const newNameRef = ref<string>()
const deletedLocal = ref(false)
const tooltipDelay = computed<number>(() =>
settingStore.get('LiteGraph.Node.TooltipDelay')
@@ -167,12 +170,14 @@ const { isLoading, error } = useImage({
function confirmDeletion() {
dropdownMenuButton.value?.hide()
const promptText = ref<string>(t('assetBrowser.deletion.body'))
const optionsDisabled = ref(false)
const confirmDialog = showConfirmDialog({
headerProps: {
title: t('assetBrowser.deletion.header')
},
props: {
promptText: t('assetBrowser.deletion.body')
promptText
},
footerProps: {
confirmText: t('g.delete'),
@@ -180,17 +185,32 @@ function confirmDeletion() {
confirmClass: cn(
'bg-danger-200 text-base-foreground hover:bg-danger-200/80 focus:bg-danger-200/80 focus:ring ring-base-foreground'
),
optionsDisabled,
onCancel: () => {
closeDialog(confirmDialog)
},
onConfirm: async () => {
try {
promptText.value = t('assetBrowser.deletion.inProgress', {
asset: asset.name
})
await assetService.deleteAsset(asset.id)
// TODO: Remove this from the list on success.
promptText.value = t('assetBrowser.deletion.complete', {
asset: asset.name
})
// Give a second for the completion message
await new Promise((resolve) => setTimeout(resolve, 1_000))
deletedLocal.value = true
} catch (err: unknown) {
console.error(err)
promptText.value = t('assetBrowser.deletion.failed', {
asset: asset.name
})
// Give a second for the completion message
await new Promise((resolve) => setTimeout(resolve, 3_000))
} finally {
closeDialog(confirmDialog)
}
closeDialog(confirmDialog)
}
}
})
@@ -204,10 +224,13 @@ function startAssetRename() {
async function assetRename(newName?: string) {
isEditing.value = false
if (newName) {
await assetService.updateAsset(asset.id, {
// Optimistic update
newNameRef.value = newName
const result = await assetService.updateAsset(asset.id, {
name: newName
})
newNameRef.value = newName
// Update with the actual name once the server responds
newNameRef.value = result.name
}
}
</script>

View File

@@ -1,7 +1,10 @@
import { fromZodError } from 'zod-validation-error'
import { st } from '@/i18n'
import { assetResponseSchema } from '@/platform/assets/schemas/assetSchema'
import {
assetItemSchema,
assetResponseSchema
} from '@/platform/assets/schemas/assetSchema'
import type {
AssetItem,
AssetMetadata,
@@ -287,13 +290,13 @@ function createAssetService() {
*
* @param id - The asset ID (UUID)
* @param newData - The data to update
* @returns Promise<void>
* @returns Promise<AssetItem>
* @throws Error if update fails
*/
async function updateAsset(
id: string,
newData: Partial<AssetMetadata>
): Promise<string> {
): Promise<AssetItem> {
const res = await api.fetchApi(`${ASSETS_ENDPOINT}/${id}`, {
method: 'PUT',
headers: {
@@ -307,7 +310,15 @@ function createAssetService() {
`Unable to update asset ${id}: Server returned ${res.status}`
)
}
return await res.json()
const newAsset = assetItemSchema.safeParse(await res.json())
if (newAsset.success) {
return newAsset.data
}
throw new Error(
`Unable to update asset ${id}: Invalid response - ${newAsset.error}`
)
}
/**