Wire TM Registry and Reference Library to real backend APIs
Both pages were showing hardcoded mock data (PDFs, TMX, DOCX files). Now they: - Fetch real data from /files/tm and /files/reference endpoints - Accept .json/.jsonl uploads (not PDF/TMX) - Support delete with confirmation - Auto-select Amazon as the default client - Show proper upload dialogs with locale/channel/file-type selectors - Fixed api.ts functions to pass client_id, channel, file_type as query params (matching backend expectations) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
17bf57e865
commit
8d4dc65993
3 changed files with 485 additions and 176 deletions
|
|
@ -1,11 +1,19 @@
|
|||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import React, { useState, useEffect, useCallback } from "react";
|
||||
import { AppShell } from "@/components/layout/AppShell";
|
||||
import { FileUploader } from "@/components/admin/FileUploader";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
|
|
@ -21,21 +29,130 @@ import {
|
|||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { formatDate } from "@/lib/utils";
|
||||
import { Upload, Trash2, FileText, Download } from "lucide-react";
|
||||
import type { ReferenceFile } from "@/lib/types";
|
||||
import { Upload, Trash2, FileText, Download, Loader2 } from "lucide-react";
|
||||
import {
|
||||
getReferenceFiles,
|
||||
uploadReferenceFile,
|
||||
deleteReferenceFile,
|
||||
getClients,
|
||||
} from "@/lib/api";
|
||||
import type { Client } from "@/lib/types";
|
||||
|
||||
const mockRefFiles: ReferenceFile[] = [
|
||||
{ id: "ref-001", name: "Brand_Guidelines_v3.pdf", file_type: "PDF", locale_code: undefined, client_id: undefined, file_path: "/files/ref/Brand_Guidelines_v3.pdf", uploaded_by: "Sarah Chen", uploaded_at: "2026-02-10T10:00:00Z" },
|
||||
{ id: "ref-002", name: "Glossary_Master.xlsx", file_type: "XLSX", locale_code: undefined, client_id: undefined, file_path: "/files/ref/Glossary_Master.xlsx", uploaded_by: "Sarah Chen", uploaded_at: "2026-02-15T14:30:00Z" },
|
||||
{ id: "ref-003", name: "StyleGuide_de-DE.pdf", file_type: "PDF", locale_code: "de-DE", client_id: undefined, file_path: "/files/ref/StyleGuide_de-DE.pdf", uploaded_by: "Emily Brown", uploaded_at: "2026-03-01T09:00:00Z" },
|
||||
{ id: "ref-004", name: "StyleGuide_fr-FR.pdf", file_type: "PDF", locale_code: "fr-FR", client_id: undefined, file_path: "/files/ref/StyleGuide_fr-FR.pdf", uploaded_by: "Emily Brown", uploaded_at: "2026-03-01T09:05:00Z" },
|
||||
{ id: "ref-005", name: "OMG_Voice_Profile.docx", file_type: "DOCX", locale_code: undefined, client_id: "client-001", file_path: "/files/ref/OMG_Voice_Profile.docx", uploaded_by: "James Miller", uploaded_at: "2026-03-10T11:00:00Z" },
|
||||
{ id: "ref-006", name: "Prime_Terminology.xlsx", file_type: "XLSX", locale_code: undefined, client_id: undefined, file_path: "/files/ref/Prime_Terminology.xlsx", uploaded_by: "Sarah Chen", uploaded_at: "2026-03-20T15:45:00Z" },
|
||||
const LOCALES = [
|
||||
"de-DE", "fr-FR", "it-IT", "es-ES", "nl-NL",
|
||||
"sv-SE", "pl-PL", "pt-PT", "de-AT", "fr-BE", "nl-BE", "ca-ES",
|
||||
];
|
||||
|
||||
const FILE_TYPES = [
|
||||
{ value: "glossary", label: "Glossary" },
|
||||
{ value: "blacklist", label: "Blacklist" },
|
||||
{ value: "tov_global", label: "TOV Global" },
|
||||
{ value: "tov_supplement", label: "TOV Supplement" },
|
||||
{ value: "locale_considerations", label: "Locale Considerations" },
|
||||
{ value: "date_pct_formats", label: "Date/Percent Formats" },
|
||||
];
|
||||
|
||||
const FILE_TYPE_LABELS: Record<string, string> = {
|
||||
glossary: "Glossary",
|
||||
blacklist: "Blacklist",
|
||||
tov_global: "TOV Global",
|
||||
tov_supplement: "TOV Supplement",
|
||||
locale_considerations: "Locale",
|
||||
date_pct_formats: "Date/Pct",
|
||||
};
|
||||
|
||||
interface RefFileRow {
|
||||
id: string;
|
||||
filename: string;
|
||||
file_type: string;
|
||||
locale_scope: string;
|
||||
uploaded_at: string;
|
||||
}
|
||||
|
||||
export default function ReferenceLibraryPage() {
|
||||
const [files] = useState(mockRefFiles);
|
||||
const [files, setFiles] = useState<RefFileRow[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [clients, setClients] = useState<Client[]>([]);
|
||||
const [clientId, setClientId] = useState<string>("");
|
||||
const [uploadOpen, setUploadOpen] = useState(false);
|
||||
const [deleting, setDeleting] = useState<string | null>(null);
|
||||
|
||||
// Upload form state
|
||||
const [uploadFile, setUploadFile] = useState<File | null>(null);
|
||||
const [uploadLocale, setUploadLocale] = useState("global");
|
||||
const [uploadFileType, setUploadFileType] = useState("");
|
||||
const [uploading, setUploading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
getClients().then((c) => {
|
||||
setClients(c);
|
||||
const amazon = c.find((cl) => cl.name.toLowerCase() === "amazon");
|
||||
if (amazon) setClientId(amazon.id);
|
||||
else if (c.length > 0) setClientId(c[0].id);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const fetchFiles = useCallback(async () => {
|
||||
if (!clientId) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await getReferenceFiles(clientId);
|
||||
setFiles(data as unknown as RefFileRow[]);
|
||||
} catch {
|
||||
setFiles([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [clientId]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchFiles();
|
||||
}, [fetchFiles]);
|
||||
|
||||
const handleDelete = async (fileId: string) => {
|
||||
if (!confirm("Delete this reference file?")) return;
|
||||
setDeleting(fileId);
|
||||
try {
|
||||
await deleteReferenceFile(fileId);
|
||||
setFiles((prev) => prev.filter((f) => f.id !== fileId));
|
||||
} catch {
|
||||
// handled by interceptor
|
||||
} finally {
|
||||
setDeleting(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownload = (fileId: string, filename: string) => {
|
||||
// Open download URL in new tab
|
||||
const baseUrl = process.env.NEXT_PUBLIC_API_URL || "";
|
||||
const token = localStorage.getItem("access_token");
|
||||
window.open(
|
||||
`${baseUrl}/api/v1/files/reference/${fileId}/download?token=${token}`,
|
||||
"_blank"
|
||||
);
|
||||
};
|
||||
|
||||
const handleUpload = async () => {
|
||||
if (!uploadFile || !uploadFileType || !clientId) return;
|
||||
setUploading(true);
|
||||
try {
|
||||
await uploadReferenceFile(
|
||||
uploadFile,
|
||||
clientId,
|
||||
uploadFileType,
|
||||
uploadLocale || "global"
|
||||
);
|
||||
setUploadOpen(false);
|
||||
setUploadFile(null);
|
||||
setUploadLocale("global");
|
||||
setUploadFileType("");
|
||||
fetchFiles();
|
||||
} catch {
|
||||
// handled by interceptor
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AppShell>
|
||||
|
|
@ -51,74 +168,75 @@ export default function ReferenceLibraryPage() {
|
|||
</div>
|
||||
|
||||
<Card>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>File Name</TableHead>
|
||||
<TableHead>Type</TableHead>
|
||||
<TableHead>Locale</TableHead>
|
||||
<TableHead>Client</TableHead>
|
||||
<TableHead>Uploaded By</TableHead>
|
||||
<TableHead>Date</TableHead>
|
||||
<TableHead className="w-[100px]"></TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{files.map((file) => (
|
||||
<TableRow key={file.id}>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
<FileText className="h-4 w-4 text-amazon-teal" />
|
||||
<span className="text-sm font-medium">
|
||||
{file.name}
|
||||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="gray">{file.file_type}</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{file.locale_code ? (
|
||||
<Badge variant="outline">{file.locale_code}</Badge>
|
||||
) : (
|
||||
<span className="text-xs text-gray-400">Global</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{file.client_id ? (
|
||||
<Badge variant="outline">OMG</Badge>
|
||||
) : (
|
||||
<span className="text-xs text-gray-400">All</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-sm text-gray-500">
|
||||
{file.uploaded_by}
|
||||
</TableCell>
|
||||
<TableCell className="text-sm text-gray-500">
|
||||
{formatDate(file.uploaded_at)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-gray-400"
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-gray-400 hover:text-amazon-error"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
{loading ? (
|
||||
<div className="py-12 text-center">
|
||||
<Loader2 className="h-6 w-6 animate-spin mx-auto text-gray-400" />
|
||||
<p className="text-sm text-gray-400 mt-2">Loading reference files...</p>
|
||||
</div>
|
||||
) : files.length === 0 ? (
|
||||
<div className="py-12 text-center">
|
||||
<FileText className="h-8 w-8 text-gray-300 mx-auto mb-2" />
|
||||
<p className="text-sm text-gray-400">No reference files uploaded yet</p>
|
||||
</div>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>File Name</TableHead>
|
||||
<TableHead>Type</TableHead>
|
||||
<TableHead>Locale</TableHead>
|
||||
<TableHead>Date</TableHead>
|
||||
<TableHead className="w-[100px]"></TableHead>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{files.map((file) => (
|
||||
<TableRow key={file.id}>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
<FileText className="h-4 w-4 text-amazon-teal" />
|
||||
<span className="text-sm font-medium">
|
||||
{file.filename}
|
||||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="gray">
|
||||
{FILE_TYPE_LABELS[file.file_type] || file.file_type}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{file.locale_scope === "global" ? (
|
||||
<span className="text-xs text-gray-400">Global</span>
|
||||
) : (
|
||||
<Badge variant="outline">{file.locale_scope}</Badge>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-sm text-gray-500">
|
||||
{formatDate(file.uploaded_at)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-gray-400 hover:text-amazon-error"
|
||||
onClick={() => handleDelete(file.id)}
|
||||
disabled={deleting === file.id}
|
||||
>
|
||||
{deleting === file.id ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Trash2 className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<Dialog open={uploadOpen} onOpenChange={setUploadOpen}>
|
||||
|
|
@ -126,11 +244,57 @@ export default function ReferenceLibraryPage() {
|
|||
<DialogHeader>
|
||||
<DialogTitle>Upload Reference File</DialogTitle>
|
||||
</DialogHeader>
|
||||
<FileUploader
|
||||
accept=".pdf,.docx,.xlsx,.csv,.txt"
|
||||
label="Drop your reference file here"
|
||||
onUpload={() => setUploadOpen(false)}
|
||||
/>
|
||||
<div className="space-y-4 py-2">
|
||||
<div className="space-y-2">
|
||||
<Label>File (.json)</Label>
|
||||
<Input
|
||||
type="file"
|
||||
accept=".json,.jsonl"
|
||||
onChange={(e) => setUploadFile(e.target.files?.[0] || null)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>File Type</Label>
|
||||
<Select value={uploadFileType} onValueChange={setUploadFileType}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{FILE_TYPES.map((ft) => (
|
||||
<SelectItem key={ft.value} value={ft.value}>
|
||||
{ft.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Locale</Label>
|
||||
<Select value={uploadLocale} onValueChange={setUploadLocale}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Global (all locales)" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="global">Global (all locales)</SelectItem>
|
||||
{LOCALES.map((l) => (
|
||||
<SelectItem key={l} value={l}>{l}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<Button
|
||||
onClick={handleUpload}
|
||||
disabled={!uploadFile || !uploadFileType || uploading}
|
||||
className="w-full gap-2"
|
||||
>
|
||||
{uploading ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Upload className="h-4 w-4" />
|
||||
)}
|
||||
Upload
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,19 @@
|
|||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import React, { useState, useEffect, useCallback } from "react";
|
||||
import { AppShell } from "@/components/layout/AppShell";
|
||||
import { FileUploader } from "@/components/admin/FileUploader";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
|
|
@ -21,23 +29,99 @@ import {
|
|||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { formatDate } from "@/lib/utils";
|
||||
import { Upload, Trash2, Database } from "lucide-react";
|
||||
import type { TMFile } from "@/lib/types";
|
||||
import { Upload, Trash2, Database, Loader2 } from "lucide-react";
|
||||
import { getTMFiles, uploadTMFile, deleteTMFile, getClients } from "@/lib/api";
|
||||
import type { Client } from "@/lib/types";
|
||||
|
||||
const mockTMFiles: TMFile[] = [
|
||||
{ id: "tm-001", name: "TM_de-DE_Master.tmx", locale_code: "de-DE", client_id: undefined, file_path: "/files/tm/TM_de-DE_Master.tmx", entry_count: 15420, uploaded_by: "Sarah Chen", uploaded_at: "2026-03-15T10:30:00Z" },
|
||||
{ id: "tm-002", name: "TM_fr-FR_Master.tmx", locale_code: "fr-FR", client_id: undefined, file_path: "/files/tm/TM_fr-FR_Master.tmx", entry_count: 12890, uploaded_by: "Sarah Chen", uploaded_at: "2026-03-15T10:35:00Z" },
|
||||
{ id: "tm-003", name: "TM_it-IT_Master.tmx", locale_code: "it-IT", client_id: undefined, file_path: "/files/tm/TM_it-IT_Master.tmx", entry_count: 11230, uploaded_by: "Emily Brown", uploaded_at: "2026-03-20T14:00:00Z" },
|
||||
{ id: "tm-004", name: "TM_es-ES_Master.tmx", locale_code: "es-ES", client_id: undefined, file_path: "/files/tm/TM_es-ES_Master.tmx", entry_count: 13670, uploaded_by: "Emily Brown", uploaded_at: "2026-03-20T14:05:00Z" },
|
||||
{ id: "tm-005", name: "TM_de-DE_OMG.tmx", locale_code: "de-DE", client_id: "client-001", file_path: "/files/tm/TM_de-DE_OMG.tmx", entry_count: 3240, uploaded_by: "James Miller", uploaded_at: "2026-04-01T09:00:00Z" },
|
||||
{ id: "tm-006", name: "TM_nl-NL_Master.tmx", locale_code: "nl-NL", client_id: undefined, file_path: "/files/tm/TM_nl-NL_Master.tmx", entry_count: 8450, uploaded_by: "Sarah Chen", uploaded_at: "2026-04-05T11:20:00Z" },
|
||||
{ id: "tm-007", name: "TM_sv-SE_Master.tmx", locale_code: "sv-SE", client_id: undefined, file_path: "/files/tm/TM_sv-SE_Master.tmx", entry_count: 7120, uploaded_by: "Sarah Chen", uploaded_at: "2026-04-05T11:25:00Z" },
|
||||
{ id: "tm-008", name: "TM_pl-PL_Master.tmx", locale_code: "pl-PL", client_id: undefined, file_path: "/files/tm/TM_pl-PL_Master.tmx", entry_count: 6890, uploaded_by: "Emily Brown", uploaded_at: "2026-04-06T16:00:00Z" },
|
||||
const LOCALES = [
|
||||
"de-DE", "fr-FR", "it-IT", "es-ES", "nl-NL",
|
||||
"sv-SE", "pl-PL", "pt-PT", "de-AT", "fr-BE", "nl-BE", "ca-ES",
|
||||
];
|
||||
|
||||
const CHANNELS = [
|
||||
"MASS", "VALUE", "ONSITE", "OUTBOUND", "UEFA",
|
||||
"PrimeDualBenefit", "PrimeGourmetGuard", "PrimeMidfunnel",
|
||||
"PrimeSpeed", "TheKiss", "DoubleDonut", "EUSelection", "BDA",
|
||||
];
|
||||
|
||||
interface TMFileRow {
|
||||
id: string;
|
||||
filename: string;
|
||||
locale_code: string;
|
||||
channel: string;
|
||||
segment_count: number;
|
||||
uploaded_at: string;
|
||||
}
|
||||
|
||||
export default function TMRegistryPage() {
|
||||
const [files] = useState(mockTMFiles);
|
||||
const [files, setFiles] = useState<TMFileRow[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [clients, setClients] = useState<Client[]>([]);
|
||||
const [clientId, setClientId] = useState<string>("");
|
||||
const [uploadOpen, setUploadOpen] = useState(false);
|
||||
const [deleting, setDeleting] = useState<string | null>(null);
|
||||
|
||||
// Upload form state
|
||||
const [uploadFile, setUploadFile] = useState<File | null>(null);
|
||||
const [uploadLocale, setUploadLocale] = useState("");
|
||||
const [uploadChannel, setUploadChannel] = useState("");
|
||||
const [uploading, setUploading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
getClients().then((c) => {
|
||||
setClients(c);
|
||||
const amazon = c.find((cl) => cl.name.toLowerCase() === "amazon");
|
||||
if (amazon) setClientId(amazon.id);
|
||||
else if (c.length > 0) setClientId(c[0].id);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const fetchFiles = useCallback(async () => {
|
||||
if (!clientId) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await getTMFiles(clientId);
|
||||
setFiles(data as unknown as TMFileRow[]);
|
||||
} catch {
|
||||
setFiles([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [clientId]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchFiles();
|
||||
}, [fetchFiles]);
|
||||
|
||||
const handleDelete = async (fileId: string) => {
|
||||
if (!confirm("Delete this TM file?")) return;
|
||||
setDeleting(fileId);
|
||||
try {
|
||||
await deleteTMFile(fileId);
|
||||
setFiles((prev) => prev.filter((f) => f.id !== fileId));
|
||||
} catch {
|
||||
// handled by interceptor
|
||||
} finally {
|
||||
setDeleting(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpload = async () => {
|
||||
if (!uploadFile || !uploadLocale || !uploadChannel || !clientId) return;
|
||||
setUploading(true);
|
||||
try {
|
||||
await uploadTMFile(uploadFile, clientId, uploadLocale, uploadChannel);
|
||||
setUploadOpen(false);
|
||||
setUploadFile(null);
|
||||
setUploadLocale("");
|
||||
setUploadChannel("");
|
||||
fetchFiles();
|
||||
} catch {
|
||||
// handled by interceptor
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AppShell>
|
||||
|
|
@ -53,61 +137,71 @@ export default function TMRegistryPage() {
|
|||
</div>
|
||||
|
||||
<Card>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>File Name</TableHead>
|
||||
<TableHead>Locale</TableHead>
|
||||
<TableHead>Client</TableHead>
|
||||
<TableHead>Entries</TableHead>
|
||||
<TableHead>Uploaded By</TableHead>
|
||||
<TableHead>Date</TableHead>
|
||||
<TableHead className="w-[60px]"></TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{files.map((file) => (
|
||||
<TableRow key={file.id}>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
<Database className="h-4 w-4 text-amazon-teal" />
|
||||
<span className="text-sm font-medium font-mono">
|
||||
{file.name}
|
||||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="gray">{file.locale_code}</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{file.client_id ? (
|
||||
<Badge variant="outline">OMG</Badge>
|
||||
) : (
|
||||
<span className="text-xs text-gray-400">Global</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-sm">
|
||||
{file.entry_count.toLocaleString()}
|
||||
</TableCell>
|
||||
<TableCell className="text-sm text-gray-500">
|
||||
{file.uploaded_by}
|
||||
</TableCell>
|
||||
<TableCell className="text-sm text-gray-500">
|
||||
{formatDate(file.uploaded_at)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-gray-400 hover:text-amazon-error"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
{loading ? (
|
||||
<div className="py-12 text-center">
|
||||
<Loader2 className="h-6 w-6 animate-spin mx-auto text-gray-400" />
|
||||
<p className="text-sm text-gray-400 mt-2">Loading TM files...</p>
|
||||
</div>
|
||||
) : files.length === 0 ? (
|
||||
<div className="py-12 text-center">
|
||||
<Database className="h-8 w-8 text-gray-300 mx-auto mb-2" />
|
||||
<p className="text-sm text-gray-400">No TM files uploaded yet</p>
|
||||
</div>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>File Name</TableHead>
|
||||
<TableHead>Locale</TableHead>
|
||||
<TableHead>Channel</TableHead>
|
||||
<TableHead>Entries</TableHead>
|
||||
<TableHead>Date</TableHead>
|
||||
<TableHead className="w-[60px]"></TableHead>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{files.map((file) => (
|
||||
<TableRow key={file.id}>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
<Database className="h-4 w-4 text-amazon-teal" />
|
||||
<span className="text-sm font-medium font-mono">
|
||||
{file.filename}
|
||||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="gray">{file.locale_code}</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-sm text-gray-600">{file.channel}</span>
|
||||
</TableCell>
|
||||
<TableCell className="text-sm">
|
||||
{file.segment_count?.toLocaleString() ?? "—"}
|
||||
</TableCell>
|
||||
<TableCell className="text-sm text-gray-500">
|
||||
{formatDate(file.uploaded_at)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-gray-400 hover:text-amazon-error"
|
||||
onClick={() => handleDelete(file.id)}
|
||||
disabled={deleting === file.id}
|
||||
>
|
||||
{deleting === file.id ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Trash2 className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<Dialog open={uploadOpen} onOpenChange={setUploadOpen}>
|
||||
|
|
@ -115,11 +209,54 @@ export default function TMRegistryPage() {
|
|||
<DialogHeader>
|
||||
<DialogTitle>Upload Translation Memory</DialogTitle>
|
||||
</DialogHeader>
|
||||
<FileUploader
|
||||
accept=".tmx,.xliff,.sdlxliff"
|
||||
label="Drop your TM file here (.tmx, .xliff)"
|
||||
onUpload={() => setUploadOpen(false)}
|
||||
/>
|
||||
<div className="space-y-4 py-2">
|
||||
<div className="space-y-2">
|
||||
<Label>File (.json / .jsonl)</Label>
|
||||
<Input
|
||||
type="file"
|
||||
accept=".json,.jsonl"
|
||||
onChange={(e) => setUploadFile(e.target.files?.[0] || null)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Locale</Label>
|
||||
<Select value={uploadLocale} onValueChange={setUploadLocale}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select locale" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{LOCALES.map((l) => (
|
||||
<SelectItem key={l} value={l}>{l}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Channel</Label>
|
||||
<Select value={uploadChannel} onValueChange={setUploadChannel}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select channel" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{CHANNELS.map((ch) => (
|
||||
<SelectItem key={ch} value={ch}>{ch}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<Button
|
||||
onClick={handleUpload}
|
||||
disabled={!uploadFile || !uploadLocale || !uploadChannel || uploading}
|
||||
className="w-full gap-2"
|
||||
>
|
||||
{uploading ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Upload className="h-4 w-4" />
|
||||
)}
|
||||
Upload
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -477,21 +477,23 @@ export async function updateClient(
|
|||
|
||||
// ─── TM Files ────────────────────────────────────────────────────────
|
||||
|
||||
export async function getTMFiles(): Promise<TMFile[]> {
|
||||
const response = await api.get<TMFile[]>("/files/tm");
|
||||
export async function getTMFiles(clientId: string): Promise<TMFile[]> {
|
||||
const response = await api.get<TMFile[]>("/files/tm", {
|
||||
params: { client_id: clientId },
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function uploadTMFile(
|
||||
file: File,
|
||||
clientId: string,
|
||||
localeCode: string,
|
||||
clientId?: string
|
||||
): Promise<TMFile> {
|
||||
channel: string
|
||||
): Promise<unknown> {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
formData.append("locale_code", localeCode);
|
||||
if (clientId) formData.append("client_id", clientId);
|
||||
const response = await api.post<TMFile>("/files/tm", formData, {
|
||||
const response = await api.post("/files/tm", formData, {
|
||||
params: { client_id: clientId, locale_code: localeCode, channel },
|
||||
headers: { "Content-Type": "multipart/form-data" },
|
||||
});
|
||||
return response.data;
|
||||
|
|
@ -503,25 +505,31 @@ export async function deleteTMFile(fileId: string): Promise<void> {
|
|||
|
||||
// ─── Reference Files ─────────────────────────────────────────────────
|
||||
|
||||
export async function getReferenceFiles(): Promise<ReferenceFile[]> {
|
||||
const response = await api.get<ReferenceFile[]>("/files/reference");
|
||||
export async function getReferenceFiles(
|
||||
clientId: string
|
||||
): Promise<ReferenceFile[]> {
|
||||
const response = await api.get<ReferenceFile[]>("/files/reference", {
|
||||
params: { client_id: clientId },
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function uploadReferenceFile(
|
||||
file: File,
|
||||
localeCode?: string,
|
||||
clientId?: string
|
||||
): Promise<ReferenceFile> {
|
||||
clientId: string,
|
||||
fileType: string,
|
||||
localeScope: string
|
||||
): Promise<unknown> {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
if (localeCode) formData.append("locale_code", localeCode);
|
||||
if (clientId) formData.append("client_id", clientId);
|
||||
const response = await api.post<ReferenceFile>(
|
||||
"/files/reference",
|
||||
formData,
|
||||
{ headers: { "Content-Type": "multipart/form-data" } }
|
||||
);
|
||||
const response = await api.post("/files/reference", formData, {
|
||||
params: {
|
||||
client_id: clientId,
|
||||
file_type: fileType,
|
||||
locale_scope: localeScope,
|
||||
},
|
||||
headers: { "Content-Type": "multipart/form-data" },
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue