feat(workspace): integrate persisted canvas, element editing and session renewal
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
import { createContext, useContext, useMemo, useState, type Dispatch, type ReactNode, type SetStateAction } from "react";
|
||||
import type { Board } from "@/lib/workspace";
|
||||
|
||||
type ActiveBoard = Pick<Board, "id" | "workspace_id" | "title"> | null;
|
||||
type ActiveBoard = (Pick<Board, "id" | "workspace_id" | "title" | "slug"> & { workspace_ref?: string }) | null;
|
||||
const ActiveBoardContext = createContext<{
|
||||
board: ActiveBoard;
|
||||
setBoard: Dispatch<SetStateAction<ActiveBoard>>;
|
||||
|
||||
@@ -4,64 +4,46 @@ import { useEffect, useState } from "react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { api, errorText } from "@/lib/api";
|
||||
import { readDemoBoards } from "@/lib/demo-workspace";
|
||||
import type { Board } from "@/lib/workspace";
|
||||
import { matchesRef, type Board } from "@/lib/workspace";
|
||||
import { loadBoardCanvas } from "@/lib/canvas-loader";
|
||||
import type { CanvasSnapshot } from "@/lib/canvas-sync";
|
||||
import styles from "./workspace.module.css";
|
||||
import { useActiveBoard } from "./active-board";
|
||||
import GraphCanvas from "@/components/canvas/graph-canvas";
|
||||
|
||||
type Loaded = { board: Board; snapshot?: CanvasSnapshot; route: string };
|
||||
export default function GraphPage({ workspaceId, boardId }: { workspaceId: string; boardId: string }) {
|
||||
const demo = useSearchParams().get("demo") === "1";
|
||||
const router = useRouter();
|
||||
const route = workspaceId + ":" + boardId;
|
||||
const { board: activeBoard, setBoard: setActiveBoard } = useActiveBoard();
|
||||
const [board, setBoard] = useState<Board | null>(null);
|
||||
const [loaded, setLoaded] = useState<Loaded | null>(null);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const [retry, setRetry] = useState(0);
|
||||
useEffect(() => {
|
||||
setBoard(null);
|
||||
setError("");
|
||||
setLoaded(null); setError("");
|
||||
if (demo) {
|
||||
try {
|
||||
const list = readDemoBoards(workspaceId);
|
||||
const item = list.find((b) => b.id === boardId);
|
||||
if (item) setBoard(item);
|
||||
else setError("图谱不存在。");
|
||||
} catch { setError("无法读取本地演示图谱。"); }
|
||||
const item = readDemoBoards(workspaceId).find((board) => matchesRef(board, boardId));
|
||||
if (item) setLoaded({ board: item, route });
|
||||
else setError("图谱不存在。");
|
||||
return;
|
||||
}
|
||||
let active = true;
|
||||
const controller = new AbortController();
|
||||
api<Board>(`workspaces/${encodeURIComponent(workspaceId)}/materials/${encodeURIComponent(boardId)}`, { signal: controller.signal })
|
||||
.then((item) => {
|
||||
if (!active) return;
|
||||
if (item.kind !== "board" || item.workspace_id !== workspaceId) {
|
||||
setError("当前页面不是此工作空间的图谱。");
|
||||
return;
|
||||
}
|
||||
setBoard(item);
|
||||
})
|
||||
.catch((e) => {
|
||||
if (!active) return;
|
||||
setError(errorText(e));
|
||||
if (e.message === "invalid_session") router.replace("/login");
|
||||
});
|
||||
loadBoardCanvas(api, workspaceId, boardId, controller.signal)
|
||||
.then((result) => { if (active) setLoaded({ ...result, route }); })
|
||||
.catch((error) => { if (active) { setError(errorText(error)); if (error.message === "invalid_session") router.replace("/login"); } });
|
||||
return () => { active = false; controller.abort(); };
|
||||
}, [workspaceId, boardId, demo, router]);
|
||||
|
||||
}, [workspaceId, boardId, route, demo, retry, router]);
|
||||
const current = loaded?.route === route ? loaded : null;
|
||||
const board = current?.board;
|
||||
useEffect(() => {
|
||||
if (board?.id === boardId && board.workspace_id === workspaceId) {
|
||||
setActiveBoard({ id: board.id, workspace_id: board.workspace_id, title: board.title });
|
||||
}
|
||||
if (board) setActiveBoard({ id: board.id, slug: board.slug, workspace_id: board.workspace_id, workspace_ref: workspaceId, title: board.title });
|
||||
return () => setActiveBoard(null);
|
||||
}, [board, boardId, workspaceId, setActiveBoard]);
|
||||
|
||||
const displayBoard = board && activeBoard?.id === board.id && activeBoard.workspace_id === workspaceId
|
||||
? { ...board, title: activeBoard.title } : board;
|
||||
|
||||
return (
|
||||
<main className={styles.graph} aria-label={displayBoard ? displayBoard.title + " · 无限图谱" : "无限图谱"} aria-busy={!board && !error}>
|
||||
{displayBoard ? <GraphCanvas key={workspaceId + ":" + boardId} board={displayBoard} /> : <div className={styles.graphSurface} aria-label="正在读取画布" />}
|
||||
{error ? <p className={styles.graphStatus} role="alert">{error}</p>
|
||||
: !board && <p className={styles.graphStatus} role="status">正在打开图谱…</p>}
|
||||
</main>
|
||||
);
|
||||
}, [board, workspaceId, setActiveBoard]);
|
||||
const displayBoard = board && activeBoard?.id === board.id && activeBoard.workspace_id === board.workspace_id ? { ...board, title: activeBoard.title } : board;
|
||||
return <main className={styles.graph} aria-label={displayBoard ? displayBoard.title + " · 无限图谱" : "无限图谱"} aria-busy={!board && !error}>
|
||||
{displayBoard ? <GraphCanvas key={displayBoard.workspace_id + ":" + displayBoard.id} board={displayBoard} snapshot={current?.snapshot} /> : <div className={styles.graphSurface} aria-label="正在读取画布" />}
|
||||
{error ? <div className={styles.graphStatus} role="alert">{error} <button onClick={() => setRetry((value) => value + 1)}>重新加载</button></div> : !board && <p className={styles.graphStatus} role="status">正在打开图谱…</p>}
|
||||
</main>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user