feat: revue de code, doc CODE_REVIEW, forfaits 2026, traduction LLM, providers avec modèle
Made-with: Cursor
This commit is contained in:
216
frontend/src/app/dashboard/glossaries/EditGlossaryDialog.tsx
Normal file
216
frontend/src/app/dashboard/glossaries/EditGlossaryDialog.tsx
Normal file
@@ -0,0 +1,216 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useCallback, useRef } from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Download, Upload } from 'lucide-react';
|
||||
import { TermEditor } from './TermEditor';
|
||||
import { exportGlossaryToCsv, parseCsvToTerms } from './csvUtils';
|
||||
import { useToast } from '@/components/ui/toast';
|
||||
import type { Glossary, GlossaryTermInput } from './types';
|
||||
import { MAX_TERMS_PER_GLOSSARY } from './types';
|
||||
|
||||
interface EditGlossaryDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
glossary: Glossary | null;
|
||||
onSave: (id: string, data: { name: string; terms: GlossaryTermInput[] }) => Promise<void>;
|
||||
isSaving: boolean;
|
||||
}
|
||||
|
||||
export function EditGlossaryDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
glossary,
|
||||
onSave,
|
||||
isSaving,
|
||||
}: EditGlossaryDialogProps) {
|
||||
const [name, setName] = useState('');
|
||||
const [terms, setTerms] = useState<GlossaryTermInput[]>([]);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const isInitialized = useRef(false);
|
||||
|
||||
if (glossary && !isInitialized.current) {
|
||||
setName(glossary.name);
|
||||
setTerms(glossary.terms.map(t => ({ source: t.source, target: t.target })));
|
||||
isInitialized.current = true;
|
||||
}
|
||||
|
||||
if (!open && isInitialized.current) {
|
||||
isInitialized.current = false;
|
||||
}
|
||||
|
||||
const handleSave = useCallback(async () => {
|
||||
if (!glossary || !name.trim()) return;
|
||||
|
||||
const validTerms = terms.filter(t => t.source.trim() && t.target.trim());
|
||||
|
||||
await onSave(glossary.id, {
|
||||
name: name.trim(),
|
||||
terms: validTerms,
|
||||
});
|
||||
}, [glossary, name, terms, onSave]);
|
||||
|
||||
const handleExport = useCallback(() => {
|
||||
if (!glossary) return;
|
||||
|
||||
const glossaryWithCurrentTerms: Glossary = {
|
||||
...glossary,
|
||||
name,
|
||||
terms: terms.map((t, i) => ({
|
||||
id: `temp-${i}`,
|
||||
source: t.source,
|
||||
target: t.target,
|
||||
created_at: null,
|
||||
})),
|
||||
};
|
||||
|
||||
exportGlossaryToCsv(glossaryWithCurrentTerms);
|
||||
}, [glossary, name, terms]);
|
||||
|
||||
const handleImportClick = useCallback(() => {
|
||||
fileInputRef.current?.click();
|
||||
}, []);
|
||||
|
||||
const { toast } = useToast();
|
||||
|
||||
const handleFileChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
const text = event.target?.result;
|
||||
if (typeof text === 'string') {
|
||||
const importedTerms = parseCsvToTerms(text);
|
||||
if (importedTerms.length > 0) {
|
||||
if (importedTerms.length > MAX_TERMS_PER_GLOSSARY) {
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: 'Import failed',
|
||||
description: `CSV contains ${importedTerms.length} terms, but maximum is ${MAX_TERMS_PER_GLOSSARY}. Please reduce the number of terms.`,
|
||||
});
|
||||
e.target.value = '';
|
||||
return;
|
||||
}
|
||||
setTerms(importedTerms);
|
||||
toast({
|
||||
title: 'Import successful',
|
||||
description: `${importedTerms.length} terms imported successfully.`,
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: 'Import failed',
|
||||
description: 'No valid terms found in CSV file.',
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
reader.onerror = () => {
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: 'Import failed',
|
||||
description: 'Failed to read CSV file.',
|
||||
});
|
||||
};
|
||||
reader.readAsText(file);
|
||||
|
||||
e.target.value = '';
|
||||
}, [toast]);
|
||||
|
||||
const validTermsCount = terms.filter(t => t.source.trim() && t.target.trim()).length;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Edit Glossary</DialogTitle>
|
||||
<DialogDescription>
|
||||
Update the glossary name and term pairs.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-6 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="glossary-name">Glossary Name</Label>
|
||||
<Input
|
||||
id="glossary-name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="Enter glossary name..."
|
||||
disabled={isSaving}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Terms ({validTermsCount} valid)</Label>
|
||||
<TermEditor
|
||||
terms={terms}
|
||||
onChange={setTerms}
|
||||
disabled={isSaving}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 pt-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleExport}
|
||||
disabled={isSaving || validTermsCount === 0}
|
||||
className="gap-1.5"
|
||||
>
|
||||
<Download className="size-3.5" />
|
||||
Export CSV
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleImportClick}
|
||||
disabled={isSaving}
|
||||
className="gap-1.5"
|
||||
>
|
||||
<Upload className="size-3.5" />
|
||||
Import CSV
|
||||
</Button>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept=".csv"
|
||||
onChange={handleFileChange}
|
||||
className="hidden"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
disabled={isSaving}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
disabled={isSaving || !name.trim()}
|
||||
>
|
||||
{isSaving ? 'Saving...' : 'Save Changes'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user