205 lines
7.5 KiB
Vue
205 lines
7.5 KiB
Vue
<script setup>
|
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/Components/ui/dialog";
|
|
import { Button } from "@/Components/ui/button";
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/Components/ui/select";
|
|
import { Badge } from "@/Components/ui/badge";
|
|
import { Label } from "@/Components/ui/label";
|
|
import { Checkbox } from "@/Components/ui/checkbox";
|
|
import { ChevronRightIcon } from "@heroicons/vue/24/outline";
|
|
import { computed, ref } from "vue";
|
|
|
|
const props = defineProps({
|
|
show: Boolean,
|
|
limit: Number,
|
|
rows: Array,
|
|
columns: Array,
|
|
loading: Boolean,
|
|
truncated: Boolean,
|
|
hasHeader: Boolean,
|
|
})
|
|
|
|
const emits = defineEmits(['close','change-limit','refresh'])
|
|
|
|
// State
|
|
const selectedRow = ref(null);
|
|
const hideEmptyRows = ref(true);
|
|
|
|
// Filter out columns with empty headers
|
|
const visibleColumns = computed(() => {
|
|
if (!props.columns) return [];
|
|
return props.columns.filter(col => col && String(col).trim() !== '');
|
|
});
|
|
|
|
// Check if row is empty (first 2 columns are empty)
|
|
function isRowEmpty(row) {
|
|
if (!visibleColumns.value || visibleColumns.value.length === 0) return false;
|
|
const firstCols = visibleColumns.value.slice(0, 2);
|
|
return firstCols.every(col => !row[col] || String(row[col]).trim() === '');
|
|
}
|
|
|
|
// Filtered rows
|
|
const visibleRows = computed(() => {
|
|
if (!props.rows) return [];
|
|
let filtered = props.rows;
|
|
if (hideEmptyRows.value) {
|
|
filtered = filtered.filter(r => !isRowEmpty(r));
|
|
}
|
|
return filtered.map((r, idx) => ({ ...r, index: idx + 1 }));
|
|
});
|
|
|
|
// Select row
|
|
function selectRow(row) {
|
|
selectedRow.value = row;
|
|
}
|
|
|
|
function onLimit(val) {
|
|
emits('change-limit', Number(val));
|
|
emits('refresh');
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<Dialog :open="show" @update:open="(val) => !val && $emit('close')">
|
|
<DialogContent class="max-w-7xl max-h-[90vh] overflow-hidden flex flex-col p-0">
|
|
<!-- Header -->
|
|
<div class="px-6 py-4 border-b bg-linear-to-r from-gray-50 to-white">
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<h2 class="text-xl font-semibold text-gray-900">CSV Preview</h2>
|
|
<p class="text-sm text-gray-500 mt-1">
|
|
Showing {{ visibleRows.length }} of {{ rows.length }} rows
|
|
</p>
|
|
</div>
|
|
<div class="flex items-center gap-3">
|
|
<div class="flex items-center gap-2">
|
|
<Label for="limit-select" class="text-sm text-gray-600">Limit:</Label>
|
|
<Select :model-value="String(limit)" @update:model-value="onLimit">
|
|
<SelectTrigger id="limit-select" class="w-24 h-8">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="50">50</SelectItem>
|
|
<SelectItem value="100">100</SelectItem>
|
|
<SelectItem value="200">200</SelectItem>
|
|
<SelectItem value="300">300</SelectItem>
|
|
<SelectItem value="500">500</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<Button @click="$emit('refresh')" variant="outline" size="sm" :disabled="loading">
|
|
{{ loading ? 'Loading…' : 'Refresh' }}
|
|
</Button>
|
|
<div class="flex items-center gap-2">
|
|
<Checkbox
|
|
id="hide-empty-rows"
|
|
:checked="hideEmptyRows"
|
|
@update:checked="(val) => hideEmptyRows = val"
|
|
/>
|
|
<Label for="hide-empty-rows" class="text-xs cursor-pointer">
|
|
Hide empty rows
|
|
</Label>
|
|
</div>
|
|
<Badge v-if="truncated" variant="outline" class="bg-amber-50 text-amber-700 border-amber-200">
|
|
Truncated at limit
|
|
</Badge>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Split View -->
|
|
<div class="flex-1 flex overflow-hidden">
|
|
<!-- Left Panel - Row List -->
|
|
<div class="w-96 border-r bg-gray-50 overflow-y-auto">
|
|
<div v-if="loading" class="p-8 text-center text-gray-500">
|
|
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-2"></div>
|
|
Loading...
|
|
</div>
|
|
<div v-else-if="!visibleRows.length" class="p-8 text-center text-gray-500">
|
|
No rows to display
|
|
</div>
|
|
<div v-else class="divide-y">
|
|
<button
|
|
v-for="row in visibleRows"
|
|
:key="row.index"
|
|
@click="selectRow(row)"
|
|
class="w-full px-4 py-3 text-left hover:bg-white transition-colors"
|
|
:class="{
|
|
'bg-white shadow-sm': selectedRow?.index === row.index,
|
|
}"
|
|
>
|
|
<div class="flex items-center justify-between gap-3">
|
|
<div class="flex items-center gap-3 flex-1 min-w-0">
|
|
<!-- Row Number -->
|
|
<div class="flex-shrink-0">
|
|
<div class="w-8 h-8 rounded-full bg-blue-100 text-blue-700 flex items-center justify-center text-xs font-semibold">
|
|
{{ row.index }}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Row Preview -->
|
|
<div class="flex-1 min-w-0">
|
|
<div class="text-xs font-semibold text-gray-900 mb-1">
|
|
Row #{{ row.index }}
|
|
</div>
|
|
<div class="text-xs text-gray-600 truncate">
|
|
{{
|
|
visibleColumns.slice(0, 2).map(col => row[col]).filter(Boolean).join(' • ') || 'Empty row'
|
|
}}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Arrow -->
|
|
<ChevronRightIcon class="h-4 w-4 text-gray-400 flex-shrink-0" />
|
|
</div>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Right Panel - Row Details -->
|
|
<div v-if="selectedRow" class="flex-1 overflow-y-auto p-6">
|
|
<!-- Row Header -->
|
|
<div class="mb-6">
|
|
<h3 class="text-lg font-semibold text-gray-900">
|
|
Row #{{ selectedRow.index }}
|
|
</h3>
|
|
<p class="text-sm text-gray-500">Full row details</p>
|
|
</div>
|
|
|
|
<!-- Row Data -->
|
|
<div class="bg-gray-50 rounded-lg p-4">
|
|
<dl class="grid grid-cols-1 gap-3">
|
|
<div
|
|
v-for="col in visibleColumns"
|
|
:key="col"
|
|
class="flex items-start gap-3 py-2 border-b border-gray-200 last:border-0"
|
|
>
|
|
<dt class="text-sm font-medium text-gray-600 w-48 flex-shrink-0">
|
|
{{ col }}
|
|
</dt>
|
|
<dd class="text-sm text-gray-900 flex-1 font-medium whitespace-pre-wrap break-words">
|
|
{{ selectedRow[col] || '—' }}
|
|
</dd>
|
|
</div>
|
|
</dl>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Empty State for Right Panel -->
|
|
<div v-else class="flex-1 flex items-center justify-center text-gray-400">
|
|
<div class="text-center">
|
|
<div class="text-5xl mb-3">📄</div>
|
|
<p class="text-sm">Select a row to view details</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Footer -->
|
|
<div class="px-6 py-3 border-t bg-gray-50 text-xs text-gray-500">
|
|
Header detection: <span class="font-medium">{{ hasHeader ? 'header present' : 'no header' }}</span>
|
|
• Click a row to view full details
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</template>
|