124_webcalibre2/src/app/update/[id]/update-view.tsx

270 lines
No EOL
9.8 KiB
TypeScript

'use client';
// src/app/update/[id]/update-view.tsx
import { useState } from "react";
import {
Box, Button, Typography, Paper, Stack,
TextField, MenuItem, IconButton, Grid, Divider,
Checkbox, CircularProgress, Chip, Tooltip
} from "@mui/material";
import SaveIcon from "@mui/icons-material/Save";
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import AssignmentIcon from '@mui/icons-material/Assignment';
import AddCircleOutlineIcon from '@mui/icons-material/AddCircleOutline';
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh';
import MapIcon from '@mui/icons-material/Map';
import { useRouter } from "next/navigation";
import { updateFileAction } from "./_actions";
import { getMetadataPreviewAction } from "@/app/dashboard/actions";
interface MetadataPair {
key: string;
value: string;
selected: boolean; // Checkbox state
isPending?: boolean; // Visual highlight for auto-extracted fields
}
export default function UpdateView({
fileNode,
folders: availablefolders // Renaming 'folders' to 'availablefolders'
}: {
fileNode: any;
folders: any[];
}) {
const router = useRouter();
const [loading, setLoading] = useState(false);
const [isExtracting, setIsExtracting] = useState(false);
// 1. Initialize Basic Info
const [name, setName] = useState(fileNode.name);
const [description, setDescription] = useState(fileNode.description || "");
const [parentId, setParentId] = useState(fileNode.parentId || "");
// 2. Initialize Metadata from DB (all checked by default)
const initialMetadata: MetadataPair[] = Object.entries(fileNode.metadata || {})
.filter(([key]) => !['type', 'mimeType'].includes(key))
.map(([key, value]) => ({
key,
value: String(value),
selected: true,
isPending: false
}));
const [customMetadata, setCustomMetadata] = useState<MetadataPair[]>(initialMetadata);
// --- MAGIC FILL LOGIC ---
const handleMagicEnhance = async () => {
setIsExtracting(true);
try {
const result = await getMetadataPreviewAction(fileNode.id);
if (result.success) {
// Convert extracted JSON into pending rows
const extractedRows: MetadataPair[] = Object.entries(result.data ?? {})
.filter(([key]) => !['type', 'mimeType'].includes(key))
.map(([key, value]) => ({
key,
value: String(value),
selected: true, // Default to checked as requested
isPending: true
}));
// Merge logic: Add only if the key doesn't already exist in our list
setCustomMetadata(prev => {
const existingKeys = new Set(prev.map(r => r.key));
const filteredNew = extractedRows.filter(r => !existingKeys.has(r.key));
return [...prev, ...filteredNew];
});
}
} catch (err) {
alert("Failed to extract metadata. Ensure service is configured correctly.");
} finally {
setIsExtracting(false);
}
};
const handleUpdate = async () => {
setLoading(true);
const formData = new FormData();
formData.append("id", fileNode.id);
formData.append("name", name);
formData.append("description", description);
formData.append("parentId", parentId);
// Convert array back to object, ONLY including selected/checked rows
const metadataObj = customMetadata.reduce((acc, curr) => {
if (curr.selected && curr.key.trim()) {
acc[curr.key.trim()] = curr.value;
}
return acc;
}, {} as Record<string, string>);
formData.append("customMetadata", JSON.stringify(metadataObj));
const res = await updateFileAction(formData);
if (res.success) {
router.push("/dashboard");
router.refresh();
} else {
alert("Update failed");
setLoading(false);
}
};
return (
<Paper sx={{ p: { xs: 3, md: 6 }, borderRadius: 4 }} elevation={3}>
<Button startIcon={<ArrowBackIcon />} onClick={() => router.back()} sx={{ mb: 2 }}>
Back
</Button>
<Typography variant="h4" fontWeight={900} color="primary" gutterBottom>
Edit File Details
</Typography>
{/* --- MAGIC FILL BUTTON --- */}
<Box sx={{ mb: 4, p: 2, bgcolor: '#f0f7ff', borderRadius: 2, border: '1px dashed #1976d2' }}>
<Stack direction="row" justifyContent="space-between" alignItems="center">
<Box>
<Typography variant="subtitle1" fontWeight="bold">Enrich Metadata</Typography>
<Typography variant="caption" color="text.secondary">
Extract tags like GPS, Author, and Dimensions from the original file.
</Typography>
</Box>
<Button
variant="contained"
color="secondary"
startIcon={isExtracting ? <CircularProgress size={20} color="inherit" /> : <AutoFixHighIcon />}
onClick={handleMagicEnhance}
disabled={isExtracting}
>
{isExtracting ? 'Extracting...' : 'Magic Fill'}
</Button>
</Stack>
</Box>
<Stack spacing={4} sx={{ mt: 2 }}>
<TextField
label="File Name"
fullWidth value={name}
onChange={(e) => setName(e.target.value)}
slotProps={{ inputLabel: { shrink: true } }}
/>
<TextField
id="update-dest-select"
select fullWidth label="Destination Folder"
value={parentId}
onChange={(e) => setParentId(e.target.value)}
slotProps={{
select: { displayEmpty: true },
inputLabel: { shrink: true }
}}
>
<MenuItem value=""><em>-- Root --</em></MenuItem>
{availablefolders?.map((f: any) => (
<MenuItem key={f.id} value={f.id}>{f.name}</MenuItem>
))}
</TextField>
<Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Typography variant="h6" fontWeight="700" sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<AssignmentIcon color="primary" /> Metadata Attributes
</Typography>
<Button
startIcon={<AddCircleOutlineIcon />}
size="small"
onClick={() => setCustomMetadata([...customMetadata, { key: "", value: "", selected: true }])}
>
Add Field
</Button>
</Box>
<Stack spacing={2}>
{customMetadata.map((row, index) => (
<Box key={index}>
<Grid container spacing={1} alignItems="center">
<Grid size={{ xs: 1 }}>
<Tooltip title={row.selected ? "Save this field" : "Ignore this field"}>
<Checkbox
checked={row.selected}
onChange={(e) => {
const updated = [...customMetadata];
updated[index].selected = e.target.checked;
setCustomMetadata(updated);
}}
/>
</Tooltip>
</Grid>
<Grid size={{ xs: 4 }}>
<TextField
fullWidth size="small" placeholder="Key"
value={row.key}
disabled={row.isPending} // Usually best to keep extracted keys as-is
sx={{ bgcolor: row.isPending ? '#e8f5e9' : 'transparent' }}
onChange={(e) => {
const updated = [...customMetadata];
updated[index].key = e.target.value;
setCustomMetadata(updated);
}}
/>
</Grid>
<Grid size={{ xs: 6 }}>
<TextField
fullWidth size="small" placeholder="Value"
value={row.value}
sx={{ bgcolor: row.isPending ? '#e8f5e9' : 'transparent' }}
onChange={(e) => {
const updated = [...customMetadata];
updated[index].value = e.target.value;
setCustomMetadata(updated);
}}
/>
</Grid>
<Grid size={{ xs: 1 }}>
<IconButton color="error" onClick={() => setCustomMetadata(customMetadata.filter((_, i) => i !== index))}>
<DeleteOutlineIcon />
</IconButton>
</Grid>
</Grid>
{/* --- GOOGLE MAPS SHORTCUT --- */}
{row.key.toLowerCase().includes('latitude') && row.value && (
<Box sx={{ ml: 6, mt: 0.5 }}>
<Button
size="small"
startIcon={<MapIcon />}
href={`https://www.google.com/maps?q=${row.value},${customMetadata.find(m => m.key.toLowerCase().includes('longitude'))?.value}`}
target="_blank"
>
Verify GPS on Map
</Button>
</Box>
)}
</Box>
))}
</Stack>
</Box>
<TextField
label="Description"
multiline rows={4} fullWidth
value={description}
onChange={(e) => setDescription(e.target.value)}
slotProps={{ inputLabel: { shrink: true } }}
/>
<Button
variant="contained" size="large" fullWidth
startIcon={<SaveIcon />}
onClick={handleUpdate}
disabled={loading}
sx={{ py: 1.5, fontWeight: 'bold' }}
>
{loading ? "Saving..." : "Save Changes"}
</Button>
</Stack>
</Paper>
);
}