Spaces:
Running
Running
| import { useState } from "react"; | |
| import { Button } from "@/components/ui/button"; | |
| import { Input } from "@/components/ui/input"; | |
| import { Plus, X, Star, Pencil, Check, Undo, Redo, Upload, Download, FileJson, FlaskConical } from "lucide-react"; | |
| import { cn } from "@/lib/utils"; | |
| import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, DropdownMenuSeparator, DropdownMenuLabel } from "@/components/ui/dropdown-menu"; | |
| import { ExperimentTemplate } from "@/data/experimentTemplates"; | |
| export interface Workspace { | |
| id: string; | |
| name: string; | |
| canvasData: string | null; | |
| isFavorite?: boolean; | |
| history?: string[]; | |
| historyIndex?: number; | |
| } | |
| interface WorkspaceManagerProps { | |
| workspaces: Workspace[]; | |
| activeWorkspaceId: string; | |
| onWorkspaceChange: (id: string) => void; | |
| onWorkspaceAdd: () => void; | |
| onWorkspaceDelete: (id: string) => void; | |
| onWorkspaceRename: (id: string, name: string) => void; | |
| onWorkspaceFavorite: (id: string) => void; | |
| onUndo?: () => void; | |
| onRedo?: () => void; | |
| canUndo?: boolean; | |
| canRedo?: boolean; | |
| onImport?: () => void; | |
| onExport?: () => void; | |
| onLoadTemplate?: (templateId: string) => void; | |
| templates?: ExperimentTemplate[]; | |
| } | |
| const WorkspaceManager = ({ | |
| workspaces, | |
| activeWorkspaceId, | |
| onWorkspaceChange, | |
| onWorkspaceAdd, | |
| onWorkspaceDelete, | |
| onWorkspaceRename, | |
| onWorkspaceFavorite, | |
| onUndo, | |
| onRedo, | |
| canUndo = false, | |
| canRedo = false, | |
| onImport, | |
| onExport, | |
| onLoadTemplate, | |
| templates = [], | |
| }: WorkspaceManagerProps) => { | |
| const [editingId, setEditingId] = useState<string | null>(null); | |
| const [editingName, setEditingName] = useState(""); | |
| // Sort workspaces: favorites first, then by order | |
| const sortedWorkspaces = [...workspaces].sort((a, b) => { | |
| if (a.isFavorite && !b.isFavorite) return -1; | |
| if (!a.isFavorite && b.isFavorite) return 1; | |
| return 0; | |
| }); | |
| const handleStartEditing = (e: React.MouseEvent, workspace: Workspace) => { | |
| e.stopPropagation(); | |
| setEditingId(workspace.id); | |
| setEditingName(workspace.name); | |
| }; | |
| const handleFinishEditing = () => { | |
| if (editingId && editingName.trim()) { | |
| onWorkspaceRename(editingId, editingName.trim()); | |
| } | |
| setEditingId(null); | |
| setEditingName(""); | |
| }; | |
| const handleKeyDown = (e: React.KeyboardEvent) => { | |
| if (e.key === "Enter") { | |
| handleFinishEditing(); | |
| } else if (e.key === "Escape") { | |
| setEditingId(null); | |
| setEditingName(""); | |
| } | |
| }; | |
| return ( | |
| <div className="flex items-center gap-2 bg-card border border-border rounded-lg p-2"> | |
| {/* Undo/Redo buttons */} | |
| <div className="flex gap-1 flex-shrink-0"> | |
| <Button | |
| onClick={onUndo} | |
| size="sm" | |
| variant="ghost" | |
| disabled={!canUndo} | |
| title="Annuler (Ctrl+Z)" | |
| className="h-8 w-8 p-0" | |
| > | |
| <Undo className="w-4 h-4" /> | |
| </Button> | |
| <Button | |
| onClick={onRedo} | |
| size="sm" | |
| variant="ghost" | |
| disabled={!canRedo} | |
| title="Refaire (Ctrl+Y)" | |
| className="h-8 w-8 p-0" | |
| > | |
| <Redo className="w-4 h-4" /> | |
| </Button> | |
| </div> | |
| <div className="flex-1 overflow-x-auto"> | |
| <div className="flex gap-1 min-w-max"> | |
| {sortedWorkspaces.map((workspace) => ( | |
| <div | |
| key={workspace.id} | |
| className={cn( | |
| "flex items-center gap-1.5 px-3 py-1.5 rounded-md transition-all cursor-pointer group", | |
| activeWorkspaceId === workspace.id | |
| ? "bg-primary text-primary-foreground" | |
| : "bg-muted hover:bg-muted/80" | |
| )} | |
| onClick={() => onWorkspaceChange(workspace.id)} | |
| > | |
| {/* Favorite button */} | |
| <button | |
| onClick={(e) => { | |
| e.stopPropagation(); | |
| onWorkspaceFavorite(workspace.id); | |
| }} | |
| className={cn( | |
| "transition-opacity", | |
| workspace.isFavorite ? "opacity-100" : "opacity-0 group-hover:opacity-60" | |
| )} | |
| title={workspace.isFavorite ? "Retirer des favoris" : "Ajouter aux favoris"} | |
| > | |
| <Star | |
| className={cn( | |
| "w-3.5 h-3.5", | |
| workspace.isFavorite && "fill-current" | |
| )} | |
| /> | |
| </button> | |
| {/* Name or input for editing */} | |
| {editingId === workspace.id ? ( | |
| <div className="flex items-center gap-1" onClick={(e) => e.stopPropagation()}> | |
| <Input | |
| value={editingName} | |
| onChange={(e) => setEditingName(e.target.value)} | |
| onKeyDown={handleKeyDown} | |
| onBlur={handleFinishEditing} | |
| className="h-6 w-32 text-sm px-2" | |
| autoFocus | |
| /> | |
| <button | |
| onClick={handleFinishEditing} | |
| className="hover:bg-primary-foreground/20 rounded p-0.5" | |
| > | |
| <Check className="w-3 h-3" /> | |
| </button> | |
| </div> | |
| ) : ( | |
| <> | |
| <span className="text-sm font-medium">{workspace.name}</span> | |
| {/* Edit button */} | |
| <button | |
| onClick={(e) => handleStartEditing(e, workspace)} | |
| className={cn( | |
| "opacity-0 group-hover:opacity-100 transition-opacity", | |
| activeWorkspaceId === workspace.id && "hover:bg-primary-foreground/20 rounded p-0.5" | |
| )} | |
| title="Renommer" | |
| > | |
| <Pencil className="w-3 h-3" /> | |
| </button> | |
| </> | |
| )} | |
| {/* Delete button */} | |
| {workspaces.length > 1 && editingId !== workspace.id && ( | |
| <button | |
| onClick={(e) => { | |
| e.stopPropagation(); | |
| onWorkspaceDelete(workspace.id); | |
| }} | |
| className={cn( | |
| "opacity-0 group-hover:opacity-100 transition-opacity", | |
| activeWorkspaceId === workspace.id && "hover:bg-primary-foreground/20 rounded p-0.5" | |
| )} | |
| title="Supprimer" | |
| > | |
| <X className="w-3 h-3" /> | |
| </button> | |
| )} | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| {/* Action buttons */} | |
| <div className="flex gap-1 flex-shrink-0"> | |
| {/* Templates dropdown */} | |
| {templates.length > 0 && onLoadTemplate && ( | |
| <DropdownMenu> | |
| <DropdownMenuTrigger asChild> | |
| <Button size="sm" variant="outline" title="Charger un modèle"> | |
| <FlaskConical className="w-4 h-4 mr-1" /> | |
| Modèles | |
| </Button> | |
| </DropdownMenuTrigger> | |
| <DropdownMenuContent align="end" className="w-64"> | |
| <DropdownMenuLabel>Modèles d'expériences</DropdownMenuLabel> | |
| <DropdownMenuSeparator /> | |
| {templates.map((template) => ( | |
| <DropdownMenuItem | |
| key={template.id} | |
| onClick={() => onLoadTemplate(template.id)} | |
| className="flex flex-col items-start" | |
| > | |
| <span className="font-medium">{template.name}</span> | |
| <span className="text-xs text-muted-foreground">{template.description}</span> | |
| </DropdownMenuItem> | |
| ))} | |
| </DropdownMenuContent> | |
| </DropdownMenu> | |
| )} | |
| {/* Import/Export buttons */} | |
| <DropdownMenu> | |
| <DropdownMenuTrigger asChild> | |
| <Button size="sm" variant="outline" title="Importer/Exporter"> | |
| <FileJson className="w-4 h-4 mr-1" /> | |
| Fichier | |
| </Button> | |
| </DropdownMenuTrigger> | |
| <DropdownMenuContent align="end"> | |
| {onImport && ( | |
| <DropdownMenuItem onClick={onImport}> | |
| <Upload className="w-4 h-4 mr-2" /> | |
| Importer JSON | |
| </DropdownMenuItem> | |
| )} | |
| {onExport && ( | |
| <DropdownMenuItem onClick={onExport}> | |
| <Download className="w-4 h-4 mr-2" /> | |
| Exporter JSON | |
| </DropdownMenuItem> | |
| )} | |
| </DropdownMenuContent> | |
| </DropdownMenu> | |
| <Button | |
| onClick={onWorkspaceAdd} | |
| size="sm" | |
| variant="outline" | |
| className="flex-shrink-0" | |
| > | |
| <Plus className="w-4 h-4 mr-1" /> | |
| Nouveau | |
| </Button> | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| export default WorkspaceManager; | |