This commit is contained in:
j3d1 2026-09-06 11:50:11 +02:00
parent cb0437c98b
commit b489b56e6a
8 changed files with 60 additions and 70 deletions

View file

@ -8,7 +8,7 @@
<b-icon-chevron-down v-else></b-icon-chevron-down> <b-icon-chevron-down v-else></b-icon-chevron-down>
</button> </button>
<span v-else class="tree-view-toggle-spacer"></span> <span v-else class="tree-view-toggle-spacer"></span>
<div class="tree-view-content"> <div class="tree-view-content text-truncate">
<slot :item="node" :expanded="isExpanded(node)" :has-children="hasChildren(node)" <slot :item="node" :expanded="isExpanded(node)" :has-children="hasChildren(node)"
:depth="depth" :toggle="() => toggleNode(node)"></slot> :depth="depth" :toggle="() => toggleNode(node)"></slot>
</div> </div>
@ -64,9 +64,6 @@
.tree-view-content { .tree-view-content {
flex: 1 1 auto; flex: 1 1 auto;
min-width: 0; min-width: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
} }
</style> </style>

View file

@ -13,45 +13,40 @@
<div class="upload-section mb-4"> <div class="upload-section mb-4">
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<div v-if="!uploadedFile" class="upload-dropzone text-center py-5" <drag-drop-file-source v-if="!uploadedFile" @input="handleFilesDropped">
@dragover.prevent <div class="upload-dropzone text-center py-5">
@dragenter.prevent <b-icon-cloud-upload class="text-primary mb-3" style="font-size: 4rem;"></b-icon-cloud-upload>
@drop.prevent="handleFileDrop" <h5 class="mb-3">Upload Your Data File</h5>
:class="{ 'dragover': isDragOver }" <p class="text-muted mb-4">
@dragenter="isDragOver = true" Drag and drop your CSV or Excel file here, or click to browse
@dragleave="isDragOver = false"> </p>
<b-icon-cloud-upload class="text-primary mb-3" style="font-size: 4rem;"></b-icon-cloud-upload> <input
<h5 class="mb-3">Upload Your Data File</h5> type="file"
<p class="text-muted mb-4"> ref="fileInput"
Drag and drop your CSV or Excel file here, or click to browse accept=".csv,.xlsx,.xls"
</p> @change="handleFileSelect"
class="d-none"
/>
<input <div class="mb-3">
type="file" <button class="btn btn-primary btn-lg me-2" @click="$refs.fileInput.click()">
ref="fileInput" <b-icon-folder-open class="me-2"></b-icon-folder-open>
accept=".csv,.xlsx,.xls" Choose File
@change="handleFileSelect" </button>
class="d-none" <button class="btn btn-outline-info" @click="downloadTemplate">
/> <b-icon-download class="me-2"></b-icon-download>
Download Template
</button>
</div>
<div class="mb-3"> <div class="supported-formats">
<button class="btn btn-primary btn-lg me-2" @click="$refs.fileInput.click()"> <small class="text-muted">
<b-icon-folder-open class="me-2"></b-icon-folder-open> Supported formats: CSV (.csv), Excel (.xlsx, .xls)
Choose File </small>
</button> </div>
<button class="btn btn-outline-info" @click="downloadTemplate">
<b-icon-download class="me-2"></b-icon-download>
Download Template
</button>
</div> </div>
</drag-drop-file-source>
<div class="supported-formats">
<small class="text-muted">
Supported formats: CSV (.csv), Excel (.xlsx, .xls)
</small>
</div>
</div>
<!-- File Info Display --> <!-- File Info Display -->
<div v-else class="file-info"> <div v-else class="file-info">
@ -264,6 +259,7 @@
<script> <script>
import * as BIcons from "bootstrap-icons-vue"; import * as BIcons from "bootstrap-icons-vue";
import { mapActions } from 'vuex'; import { mapActions } from 'vuex';
import DragDropFileSource from "@/components/inputs/DragDropFileSource.vue";
// Implements every step of 'import-items'; steps 2-5,7 use a generic placeholder. See docs/implementation.md#bulk-item-import-workflow. // Implements every step of 'import-items'; steps 2-5,7 use a generic placeholder. See docs/implementation.md#bulk-item-import-workflow.
export default { export default {
@ -299,6 +295,7 @@ export default {
} }
}, },
components: { components: {
DragDropFileSource,
...BIcons ...BIcons
}, },
props: { props: {
@ -322,7 +319,6 @@ export default {
uploadTime: null, uploadTime: null,
processingFile: false, processingFile: false,
processingStatus: '', processingStatus: '',
isDragOver: false,
uploadError: null, uploadError: null,
fileAnalysis: null, fileAnalysis: null,
detectedColumns: [], detectedColumns: [],
@ -372,9 +368,9 @@ export default {
...mapActions(['createInventoryItem']), ...mapActions(['createInventoryItem']),
// --- Step 1: File upload --- // --- Step 1: File upload ---
handleFileDrop(event) { // DragDropFileSource already hashed/base64-encoded the dropped file to match the
this.isDragOver = false; // content-addressed file model - readAsText() below reverses that back to text.
const files = event.dataTransfer.files; handleFilesDropped(files) {
if (files.length > 0) { if (files.length > 0) {
this.processFile(files[0]); this.processFile(files[0]);
} }
@ -418,10 +414,20 @@ export default {
} }
}, },
// Accepts either a browser File (from the "Choose File" input) or the hashed
// {name, size, mime_type, data, hash} descriptor DragDropFileSource emits on drop.
async readAsText(file) {
if (typeof file.text === 'function') {
return file.text();
}
const bytes = Uint8Array.from(atob(file.data), c => c.charCodeAt(0));
return new TextDecoder().decode(bytes);
},
async analyzeFile(file) { async analyzeFile(file) {
// Parsing happens entirely client-side; the backend only ever sees the resulting `items` list. // Parsing happens entirely client-side; the backend only ever sees the resulting `items` list.
if (file.name.endsWith('.csv')) { if (file.name.endsWith('.csv')) {
const text = await file.text(); const text = await this.readAsText(file);
this.parsedRows = this.parseCsv(text); this.parsedRows = this.parseCsv(text);
} else { } else {
// Mock implementation; a real Excel parser would use a library like SheetJS. // Mock implementation; a real Excel parser would use a library like SheetJS.
@ -619,7 +625,7 @@ export default {
} }
.upload-dropzone:hover, .upload-dropzone:hover,
.upload-dropzone.dragover { .dragover .upload-dropzone {
border-color: #007bff; border-color: #007bff;
background-color: #f8f9ff; background-color: #f8f9ff;
} }

View file

@ -67,4 +67,8 @@
width: 100%; width: 100%;
height: auto; height: auto;
object-fit: cover; object-fit: cover;
}
.table-action {
white-space: nowrap;
} }

View file

@ -77,4 +77,12 @@
.input-group-text:not(:last-child) { .input-group-text:not(:last-child) {
border-right-width: 0; border-right-width: 0;
}
.is-invalid input, .is-invalid select {
border: 1px solid var(--bs-danger);
}
.is-invalid .invalid-feedback {
display: block;
} }

View file

@ -300,10 +300,6 @@ export default {
</script> </script>
<style scoped> <style scoped>
.table-action {
white-space: nowrap;
}
.unguarded.btn-danger, .unguarded.btn-danger,
.unguarded.btn-danger:hover, .unguarded.btn-danger:hover,
.unguarded.btn-danger:focus { .unguarded.btn-danger:focus {

View file

@ -222,14 +222,6 @@ export default {
</script> </script>
<style scoped> <style scoped>
.is-invalid input, .is-invalid select {
border: 1px solid var(--bs-danger);
}
.is-invalid .invalid-feedback {
display: block;
}
.input-group-text svg { .input-group-text svg {
overflow: visible; overflow: visible;
} }

View file

@ -196,14 +196,6 @@ export default {
</script> </script>
<style scoped> <style scoped>
.is-invalid input, .is-invalid select {
border: 1px solid var(--bs-danger);
}
.is-invalid .invalid-feedback {
display: block;
}
.visually-hidden { .visually-hidden {
display: none; display: none;
} }

View file

@ -312,11 +312,6 @@ export default {
align-items: center; align-items: center;
} }
/* The th widths above leave Actions only the leftover 5% - without this it wraps mid-icon. */
.table-action {
white-space: nowrap;
}
.btn-group.mt-auto { .btn-group.mt-auto {
width: 100%; width: 100%;
} }