This commit is contained in:
2026-02-28 02:30:41 +01:00
parent 3b35685d61
commit 9200e1cfad
3 changed files with 131 additions and 4 deletions
+5 -1
View File
@@ -116,7 +116,11 @@ export default function Workspace() {
} }
return ( return (
<AppSidebar workspace={workspace} setRoute={setRoute}> <AppSidebar
workspace={workspace}
setRoute={setRoute}
setWorkspace={setWorkspace}
>
<SidebarTrigger className="w-full px-4"> <SidebarTrigger className="w-full px-4">
<div className="px-4"> <div className="px-4">
<header className="w-full border-b pb-3 pt-2 hover:bg-accent/20 flex items-center text-foreground/90 gap-2"> <header className="w-full border-b pb-3 pt-2 hover:bg-accent/20 flex items-center text-foreground/90 gap-2">
+92 -3
View File
@@ -1,5 +1,7 @@
"use client"; "use client";
import * as React from "react";
import { import {
Sidebar, Sidebar,
SidebarContent, SidebarContent,
@@ -16,11 +18,13 @@ import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "../ui/dropdown-menu"; } from "../ui/dropdown-menu";
import { ChevronDown } from "lucide-react"; import { ChevronDown } from "lucide-react";
import { Route, SetState, Workspace } from "@/lib/types"; import { Route, SetState, Workspace } from "@/lib/types";
import { listWorkspaces, saveWorkspaceToLocalStorage } from "@/lib/workspace";
// Simple ID generator // Simple ID generator
const id = (prefix = "t") => const id = (prefix = "t") =>
@@ -30,13 +34,77 @@ type AppSidebarProps = {
children: React.ReactNode; children: React.ReactNode;
workspace: Workspace; workspace: Workspace;
setRoute: SetState<Route>; setRoute: SetState<Route>;
/** optional setter for the current workspace, used for rename */
setWorkspace?: React.Dispatch<React.SetStateAction<Workspace | undefined>>;
}; };
export default function AppSidebar({ export default function AppSidebar({
children, children,
workspace, workspace,
setRoute, setRoute,
setWorkspace,
}: AppSidebarProps) { }: AppSidebarProps) {
const [availableWorkspaces, setAvailableWorkspaces] = React.useState<
Workspace[]
>([]);
// reload list when component mounts or whenever the current workspace changes
React.useEffect(() => {
setAvailableWorkspaces(listWorkspaces());
}, [workspace]);
const switchWorkspace = (id: string) => {
setRoute({ workspace: id });
};
const importWorkspace = () => {
const json = window.prompt(
"Paste a workspace JSON to import (generated by export)",
);
if (!json) return;
try {
const parsed = JSON.parse(json);
if (parsed && typeof parsed.id === "string") {
saveWorkspaceToLocalStorage(parsed);
setAvailableWorkspaces(listWorkspaces());
switchWorkspace(parsed.id);
} else {
throw new Error("invalid shape");
}
} catch (err) {
console.error("Failed to import workspace:", err);
window.alert("Import failed: invalid JSON");
}
};
const exportWorkspace = () => {
try {
const text = JSON.stringify(workspace, null, 2);
navigator.clipboard.writeText(text);
window.alert("Workspace JSON copied to clipboard");
} catch (err) {
console.error("Export failed", err);
window.alert("Failed to copy workspace JSON");
}
};
const renameWorkspace = () => {
const newName = window.prompt("New workspace name", workspace.name);
if (!newName || newName === workspace.name) return;
const updated: Workspace = {
...workspace,
name: newName,
updatedAt: Date.now(),
};
saveWorkspaceToLocalStorage(updated);
setAvailableWorkspaces(listWorkspaces());
if (setWorkspace) {
setWorkspace(updated);
}
};
return ( return (
<SidebarProvider> <SidebarProvider>
<Sidebar> <Sidebar>
@@ -56,9 +124,30 @@ export default function AppSidebar({
align="start" align="start"
className="w-[--radix-popper-anchor-width]" className="w-[--radix-popper-anchor-width]"
> >
{/* Future: real workspace switching */} {/* list all known workspaces */}
<DropdownMenuItem>Rename Workspace</DropdownMenuItem> {availableWorkspaces.map((w) => (
<DropdownMenuItem>Export Workspace</DropdownMenuItem> <DropdownMenuItem
key={w.id}
className={
w.id === workspace.id ? "font-semibold" : undefined
}
onSelect={() => switchWorkspace(w.id)}
>
{w.name}
{w.id === workspace.id ? " (current)" : ""}
</DropdownMenuItem>
))}
<DropdownMenuSeparator />
<DropdownMenuItem onSelect={importWorkspace}>
Import Workspace
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onSelect={renameWorkspace}>
Rename Workspace
</DropdownMenuItem>
<DropdownMenuItem onSelect={exportWorkspace}>
Export Workspace
</DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</SidebarMenuItem> </SidebarMenuItem>
+34
View File
@@ -90,8 +90,42 @@ export function loadWorkspaceFromLocalStorage(id: string): Workspace {
export function saveWorkspaceToLocalStorage(workspace: Workspace): void { export function saveWorkspaceToLocalStorage(workspace: Workspace): void {
try { try {
const serialized = JSON.stringify(workspace); const serialized = JSON.stringify(workspace);
// keep using raw id for backwards compatibility
localStorage.setItem(workspace.id, serialized); localStorage.setItem(workspace.id, serialized);
} catch (err) { } catch (err) {
console.error("Failed to save workspace:", err); console.error("Failed to save workspace:", err);
} }
} }
/**
* Scan localStorage and return all entries that appear to be workspaces.
* We ignore keys starting with "route_" which are used for routing state.
*/
export function listWorkspaces(): Workspace[] {
const workspaces: Workspace[] = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (!key) continue;
if (key.startsWith("route_")) continue;
const raw = localStorage.getItem(key);
if (!raw) continue;
try {
const parsed = JSON.parse(raw);
if (
parsed &&
typeof parsed === "object" &&
typeof parsed.id === "string" &&
typeof parsed.name === "string" &&
parsed.entities &&
parsed.layout
) {
workspaces.push(parsed as Workspace);
}
} catch {
// ignore non-json values
}
}
return workspaces;
}