Polish upload UI for dual queue status and batch confirm flow.

Metadata error confirm, success styling, upload-confirmed hiding of configure, and remove stale PR plan doc.
This commit is contained in:
2026-10-23 18:34:17 +02:00
parent 271c2b7b2d
commit b4cc26a632
6 changed files with 156 additions and 61 deletions
@@ -8,9 +8,16 @@
<div class="flex flex-1 flex-col gap-3 p-4">
<p v-if="!fileName" class="text-sm text-muted-foreground">Select an image to view its metadata.</p>
<p v-else-if="entry.status === 'error'" class="text-sm text-destructive">
{{ entry.message }}
</p>
<template v-else-if="entry.status === 'error'">
<p class="text-sm text-destructive">{{ entry.message }}</p>
<template v-if="entry.output">
<p class="text-xs font-medium text-foreground">Original</p>
<pre
class="max-h-96 min-h-32 flex-1 overflow-auto rounded-md bg-muted p-3 text-xs leading-relaxed whitespace-pre-wrap text-foreground"
>{{ entry.output }}</pre
>
</template>
</template>
<template v-else-if="entry.status === 'ready'">
<div
@@ -39,11 +39,53 @@
</footer>
</div>
</div>
<div
v-if="open && metadataErrorConfirmOpen"
class="fixed inset-0 z-60 flex items-end justify-center bg-black/50 p-4 sm:items-center"
>
<div
role="alertdialog"
aria-modal="true"
aria-labelledby="upload-metadata-error-confirm-title"
aria-describedby="upload-metadata-error-confirm-desc"
class="flex w-full max-w-md flex-col rounded-lg border border-border bg-card shadow-lg"
>
<header class="border-b border-border px-4 py-3">
<h2 id="upload-metadata-error-confirm-title" class="text-base font-medium text-card-foreground">
Metadata errors
</h2>
</header>
<p id="upload-metadata-error-confirm-desc" class="px-4 py-4 text-sm text-muted-foreground">
{{ metadataErrorCount }}
{{ metadataErrorCount === 1 ? 'image has' : 'images have' }} metadata error. Do you want to upload
them with original metadata?
</p>
<footer class="flex flex-col-reverse gap-2 border-t border-border px-4 py-3 sm:flex-row sm:justify-end">
<button
type="button"
class="rounded-md border border-border px-4 py-2 text-sm font-medium transition-colors hover:bg-muted/60"
@click="metadataErrorConfirmOpen = false"
>
Cancel
</button>
<button
type="button"
class="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:opacity-90"
@click="emitUpload(true)"
>
Upload
</button>
</footer>
</div>
</div>
</Teleport>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
import { ref, watch, type PropType } from 'vue';
import type { UploadBatchSettingsForm } from '@/types/upload/UploadBatchSettingsForm';
import { validateUploadBatchSettings } from '@/utils/upload/validateUploadBatchSettings';
@@ -51,15 +93,24 @@ const open = defineModel<boolean>('open', { required: true });
const settings = defineModel<UploadBatchSettingsForm>('settings', { required: true });
const props = defineProps({
metadataErrorCount: {
type: Number as PropType<number>,
required: true
}
});
const emit = defineEmits<{
upload: [];
upload: [uploadDespiteMetadataFailure?: boolean];
}>();
const uploadValidationError = ref<string | null>(null);
const metadataErrorConfirmOpen = ref(false);
watch(open, isOpen => {
if (!isOpen) {
uploadValidationError.value = null;
metadataErrorConfirmOpen.value = false;
}
});
@@ -67,6 +118,13 @@ const close = (): void => {
open.value = false;
};
const emitUpload = (uploadDespiteMetadataFailure?: boolean): void => {
uploadValidationError.value = null;
metadataErrorConfirmOpen.value = false;
emit('upload', uploadDespiteMetadataFailure);
};
const onUploadClick = (): void => {
const error = validateUploadBatchSettings(settings.value);
@@ -76,8 +134,12 @@ const onUploadClick = (): void => {
return;
}
uploadValidationError.value = null;
if (props.metadataErrorCount > 0) {
metadataErrorConfirmOpen.value = true;
emit('upload');
return;
}
emitUpload();
};
</script>
@@ -11,8 +11,11 @@
<span class="shrink-0 text-xs text-muted-foreground">
{{ prettyBytes(row.file.size, { maximumFractionDigits: 1 }) }}
</span>
<span class="shrink-0 text-xs" :class="statusTextClass(row.preview)">
{{ row.preview.message }}
<span class="shrink-0 max-w-32 truncate text-xs sm:max-w-40" :class="metadataStatusClass(row.metadata)">
{{ row.metadata.message }}
</span>
<span class="shrink-0 max-w-32 truncate text-xs sm:max-w-40" :class="uploadStatusClass(row.upload)">
{{ row.upload.message }}
</span>
</button>
</li>
@@ -21,15 +24,12 @@
<script setup lang="ts">
import { type PropType } from 'vue';
import { useMetadataStore } from '@/stores/metadata';
import type { MetadataPreviewEntry } from '@/types/metadata/MetadataPreview';
import { makeKeyFromFile } from '@/utils/file/makeKeyFromFile';
import type { UploadQueueEntry } from '@/types/upload/UploadQueueEntry';
import type { UploadQueueRow } from '@/types/upload/UploadQueueRow';
import prettyBytes from 'pretty-bytes';
const metadataStore = useMetadataStore();
const { makeKeyFromFile } = metadataStore;
const props = defineProps({
rows: {
type: Array as PropType<UploadQueueRow[]>,
@@ -48,6 +48,27 @@ const emit = defineEmits<{
const isRowSelected = (row: UploadQueueRow): boolean =>
props.selectedFile !== null && makeKeyFromFile(props.selectedFile) === row.key;
const statusTextClass = (entry: MetadataPreviewEntry): string =>
entry.status === 'error' ? 'text-destructive' : 'text-muted-foreground';
const metadataStatusClass = (entry: MetadataPreviewEntry): string => {
if (entry.status === 'error') {
return 'text-destructive';
}
if (entry.status === 'ready') {
return 'text-success';
}
return 'text-muted-foreground';
};
const uploadStatusClass = (entry: UploadQueueEntry): string => {
if (entry.status === 'failed') {
return 'text-destructive';
}
if (entry.status === 'uploaded') {
return 'text-success';
}
return 'text-muted-foreground';
};
</script>