Extract upload dropzone and queue UI with shared pick validation utils.
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<div>
|
||||
<label
|
||||
class="inline-flex items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:opacity-90"
|
||||
:class="disabled ? 'cursor-not-allowed opacity-60' : 'cursor-pointer'"
|
||||
>
|
||||
Choose images
|
||||
<input
|
||||
ref="fileInputRef"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
multiple
|
||||
class="sr-only"
|
||||
:disabled="disabled"
|
||||
@change="onFileInputChange"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<p v-if="selectionError" class="mt-2 text-sm text-destructive">{{ selectionError }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, type PropType } from 'vue';
|
||||
|
||||
defineProps({
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
selectionError: {
|
||||
type: String as PropType<string | null>,
|
||||
default: null
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
pick: [files: File[]];
|
||||
}>();
|
||||
|
||||
const fileInputRef = ref<HTMLInputElement | null>(null);
|
||||
|
||||
const onFileInputChange = (event: Event): void => {
|
||||
const input = event.target as HTMLInputElement;
|
||||
const picked = input.files ? [...input.files] : [];
|
||||
|
||||
emit('pick', picked);
|
||||
};
|
||||
|
||||
const clearInput = (): void => {
|
||||
if (fileInputRef.value) {
|
||||
fileInputRef.value.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
defineExpose({ clearInput });
|
||||
</script>
|
||||
@@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<ul v-if="rows.length > 0" class="divide-y divide-border rounded-lg border border-border">
|
||||
<li v-for="row in rows" :key="row.key">
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full items-center gap-3 px-4 py-3 text-left transition-colors hover:bg-muted/60"
|
||||
:class="isRowSelected(row) ? 'bg-muted/80' : ''"
|
||||
@click="emit('select', row.file)"
|
||||
>
|
||||
<span class="min-w-0 flex-1 truncate text-sm font-medium">{{ row.file.name }}</span>
|
||||
<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>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { type PropType } from 'vue';
|
||||
import { useMetadataStore } from '@/stores/metadata';
|
||||
import type { MetadataPreviewEntry } from '@/types/metadata/MetadataPreview';
|
||||
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[]>,
|
||||
required: true
|
||||
},
|
||||
selectedFile: {
|
||||
type: Object as PropType<File | null>,
|
||||
default: null
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
select: [file: File];
|
||||
}>();
|
||||
|
||||
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';
|
||||
</script>
|
||||
Reference in New Issue
Block a user