feat(dockview): refactor to generic configurable dockview component

This commit is contained in:
cfngc4594 2025-04-05 17:50:07 +08:00
parent 6c34fa0171
commit b8576dbf10

View File

@ -1,142 +1,98 @@
"use client"; "use client";
import { import {
DockviewReact, BotIcon,
themeAbyssSpaced, CircleCheckBigIcon,
type DockviewReadyEvent, FileTextIcon,
type IDockviewPanelHeaderProps, FlaskConicalIcon,
} from "dockview"; SquareCheckIcon,
import { SquarePenIcon,
CircleCheckBigIcon, TerminalIcon,
FileTextIcon,
FlaskConicalIcon,
type LucideProps,
SquareCheckIcon,
SquarePenIcon,
TerminalIcon
} from "lucide-react"; } from "lucide-react";
import "@/styles/dockview.css"; import "@/styles/dockview.css";
import { type ForwardRefExoticComponent, type RefAttributes, useMemo } from "react"; import { useEffect, useMemo, useState } from "react";
import { DockviewReact, themeAbyssSpaced } from "dockview";
import type { AddPanelOptions, DockviewReadyEvent, DockviewApi } from "dockview";
const iconMap = {
FileTextIcon,
FlaskConicalIcon,
CircleCheckBigIcon,
SquarePenIcon,
SquareCheckIcon,
TerminalIcon,
BotIcon,
} as const;
type IconKey = keyof typeof iconMap;
interface DockviewProps { interface DockviewProps {
Description: React.ReactNode; options: (AddPanelOptions & {
Solutions: React.ReactNode; node: React.ReactNode;
Submissions: React.ReactNode; icon: IconKey;
Code: React.ReactNode; })[];
Testcase: React.ReactNode; storageKey: string;
TestResult: React.ReactNode;
} }
const PanelIcons: Record<string, ForwardRefExoticComponent<Omit<LucideProps, "ref"> & RefAttributes<SVGSVGElement>>> = { const DockView = ({ options, storageKey }: DockviewProps) => {
Description: FileTextIcon, const [api, setApi] = useState<DockviewApi>();
Solutions: FlaskConicalIcon,
Submissions: CircleCheckBigIcon,
Code: SquarePenIcon,
Testcase: SquareCheckIcon,
TestResult: TerminalIcon,
};
const LAYOUT_STORAGE_KEY = "dockview:layout"; const { components, tabComponents } = useMemo(() => {
const comps: Record<string, () => React.ReactNode> = {};
const tabs: Record<string, () => React.ReactNode> = {};
const DefaultTab = ({ params }: IDockviewPanelHeaderProps<{ title: string }>) => { options.forEach((option) => {
const { title } = params; const { id, icon, node, title } = option;
const Icon = PanelIcons[title];
return ( comps[id] = () => <>{node}</>;
<div className="flex items-center px-1 text-sm font-medium">
{Icon && (
<Icon
className="-ms-0.5 me-1.5 opacity-60"
size={16}
aria-hidden="true"
/>
)}
<span>{title}</span>
</div>
);
};
const tabComponents = { const Icon = iconMap[icon];
default: DefaultTab, tabs[id] = () => (
}; <div className="flex items-center px-1 text-sm font-medium">
<Icon
className="-ms-0.5 me-1.5 opacity-60"
size={16}
aria-hidden="true"
/>
<span>{title}</span>
</div>
);
});
const DEFAULT_PANELS = [ return { components: comps, tabComponents: tabs };
{ id: "Description", component: "Description", position: null }, }, [options]);
{
id: "Solutions",
component: "Solutions",
position: { referencePanel: "Description", direction: "within" },
},
{
id: "Submissions",
component: "Submissions",
position: { referencePanel: "Solutions", direction: "within" },
},
{
id: "Code",
component: "Code",
position: { referencePanel: "Submissions", direction: "right" },
},
{
id: "Testcase",
component: "Testcase",
position: { referencePanel: "Code", direction: "below" },
},
{
id: "TestResult",
component: "TestResult",
position: { referencePanel: "Testcase", direction: "within" },
},
];
export default function DockView(props: DockviewProps) { useEffect(() => {
const components = useMemo( if (!api) return;
() => ({
Description: () => props.Description, const disposable = api.onDidLayoutChange(() => {
Solutions: () => props.Solutions, localStorage.setItem(storageKey, JSON.stringify(api.toJSON()));
Submissions: () => props.Submissions, });
Code: () => props.Code,
Testcase: () => props.Testcase, return () => disposable.dispose();
TestResult: () => props.TestResult, }, [api, storageKey]);
}),
[props]
);
const handleReady = (event: DockviewReadyEvent) => { const handleReady = (event: DockviewReadyEvent) => {
let success = false; setApi(event.api);
const serializedLayout = localStorage.getItem(storageKey);
try { const addDefaultPanels = () => {
const layout = localStorage.getItem(LAYOUT_STORAGE_KEY); options.forEach((option) => {
if (layout) { event.api.addPanel({ ...option });
event.api.fromJSON(JSON.parse(layout));
success = true;
}
} catch (error) {
console.error("Failed to load layout:", error);
localStorage.removeItem(LAYOUT_STORAGE_KEY);
}
if (!success) {
DEFAULT_PANELS.forEach(({ id, component, position }) => {
event.api.addPanel({
id,
component,
tabComponent: "default",
params: { title: id },
position: position || undefined,
});
}); });
}
const saveLayout = () => {
localStorage.setItem(
LAYOUT_STORAGE_KEY,
JSON.stringify(event.api.toJSON())
);
}; };
const disposable = event.api.onDidLayoutChange(saveLayout); if (serializedLayout) {
return () => disposable.dispose(); try {
event.api.fromJSON(JSON.parse(serializedLayout));
} catch (error) {
console.error("Failed to parse layout:", error);
localStorage.removeItem(storageKey);
addDefaultPanels();
}
} else {
addDefaultPanels();
}
}; };
return ( return (
@ -147,4 +103,6 @@ export default function DockView(props: DockviewProps) {
tabComponents={tabComponents} tabComponents={tabComponents}
/> />
); );
} };
export default DockView;