210 lines
No EOL
8.4 KiB
TypeScript
210 lines
No EOL
8.4 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,
|
|
Checkbox, CircularProgress, 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;
|
|
isPending?: boolean;
|
|
}
|
|
|
|
/** * Utility to turn nested objects into flat key-value pairs for the UI
|
|
*/
|
|
const flattenObject = (obj: any, prefix = ''): Record<string, string> => {
|
|
let results: Record<string, string> = {};
|
|
for (const key in obj) {
|
|
const value = obj[key];
|
|
const newKey = prefix ? `${prefix}.${key}` : key;
|
|
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
Object.assign(results, flattenObject(value, newKey));
|
|
} else {
|
|
results[newKey] = String(value);
|
|
}
|
|
}
|
|
return results;
|
|
};
|
|
|
|
export default function UpdateView({ fileNode, folders: availablefolders }: { fileNode: any; folders: any[]; }) {
|
|
const router = useRouter();
|
|
const [loading, setLoading] = useState(false);
|
|
const [isExtracting, setIsExtracting] = useState(false);
|
|
|
|
const [name, setName] = useState(fileNode.name);
|
|
const [description, setDescription] = useState(fileNode.description || "");
|
|
const [parentId, setParentId] = useState(fileNode.parentId || "");
|
|
|
|
const initialMetadata: MetadataPair[] = Object.entries(fileNode.metadata || {})
|
|
.filter(([key]) => !['type', 'mimeType', 'magicFilled', 'details'].includes(key))
|
|
.map(([key, value]) => ({
|
|
key,
|
|
value: String(value),
|
|
selected: true,
|
|
isPending: false
|
|
}));
|
|
|
|
const [customMetadata, setCustomMetadata] = useState<MetadataPair[]>(initialMetadata);
|
|
|
|
const handleMagicEnhance = async () => {
|
|
setIsExtracting(true);
|
|
try {
|
|
const result = await getMetadataPreviewAction(fileNode.id);
|
|
if (result.success) {
|
|
// Flatten the nested 'details' and top level props
|
|
const flatData = flattenObject(result.data);
|
|
|
|
const extractedRows: MetadataPair[] = Object.entries(flatData)
|
|
.filter(([key]) => !['type', 'mimeType', 'title'].includes(key) && !key.includes('Binary Data'))
|
|
.map(([key, value]) => ({
|
|
key,
|
|
value: String(value),
|
|
selected: true,
|
|
isPending: true
|
|
}));
|
|
|
|
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.");
|
|
} 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);
|
|
|
|
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>
|
|
|
|
<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 GPS, Camera Specs, and Dimensions.</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}>
|
|
<TextField label="File Name" fullWidth value={name} onChange={(e) => setName(e.target.value)} slotProps={{ inputLabel: { shrink: true } }} />
|
|
|
|
<TextField select fullWidth label="Destination" value={parentId} onChange={(e) => setParentId(e.target.value)}>
|
|
<MenuItem value=""><em>-- Root --</em></MenuItem>
|
|
{availablefolders?.map((f: any) => (<MenuItem key={f.id} value={f.id}>{f.name}</MenuItem>))}
|
|
</TextField>
|
|
|
|
<Box>
|
|
<Stack direction="row" justifyContent="space-between" mb={2}>
|
|
<Typography variant="h6" fontWeight="700"><AssignmentIcon /> Attributes</Typography>
|
|
<Button startIcon={<AddCircleOutlineIcon />} onClick={() => setCustomMetadata([...customMetadata, { key: "", value: "", selected: true }])}>Add Field</Button>
|
|
</Stack>
|
|
|
|
<Stack spacing={2}>
|
|
{customMetadata.map((row, index) => (
|
|
<Box key={index}>
|
|
<Grid container spacing={1} alignItems="center">
|
|
<Grid item xs={1}>
|
|
<Checkbox checked={row.selected} onChange={(e) => {
|
|
const updated = [...customMetadata];
|
|
updated[index].selected = e.target.checked;
|
|
setCustomMetadata(updated);
|
|
}} />
|
|
</Grid>
|
|
<Grid item xs={4}>
|
|
<TextField fullWidth size="small" value={row.key} disabled={row.isPending} sx={{ bgcolor: row.isPending ? '#e8f5e9' : 'transparent' }}
|
|
onChange={(e) => {
|
|
const updated = [...customMetadata];
|
|
updated[index].key = e.target.value;
|
|
setCustomMetadata(updated);
|
|
}} />
|
|
</Grid>
|
|
<Grid item xs={6}>
|
|
<TextField fullWidth size="small" value={row.value} sx={{ bgcolor: row.isPending ? '#e8f5e9' : 'transparent' }}
|
|
onChange={(e) => {
|
|
const updated = [...customMetadata];
|
|
updated[index].value = e.target.value;
|
|
setCustomMetadata(updated);
|
|
}} />
|
|
</Grid>
|
|
<Grid item xs={1}>
|
|
<IconButton color="error" onClick={() => setCustomMetadata(customMetadata.filter((_, i) => i !== index))}><DeleteOutlineIcon /></IconButton>
|
|
</Grid>
|
|
</Grid>
|
|
|
|
{row.key.toLowerCase().includes('latitude') && row.value && (
|
|
<Box sx={{ ml: 7, mt: 0.5 }}>
|
|
<Button size="small" startIcon={<MapIcon />} target="_blank"
|
|
href={`https://www.google.com/maps?q=${row.value},${customMetadata.find(m => m.key.toLowerCase().includes('longitude'))?.value}`}>
|
|
View on Map
|
|
</Button>
|
|
</Box>
|
|
)}
|
|
</Box>
|
|
))}
|
|
</Stack>
|
|
</Box>
|
|
|
|
<TextField label="Description" multiline rows={3} fullWidth value={description} onChange={(e) => setDescription(e.target.value)} />
|
|
|
|
<Button variant="contained" size="large" fullWidth startIcon={<SaveIcon />} onClick={handleUpdate} disabled={loading}>
|
|
{loading ? "Saving..." : "Save Changes"}
|
|
</Button>
|
|
</Stack>
|
|
</Paper>
|
|
);
|
|
} |