feat(dockview): refactor dockview component and add problem-specific implementation

- Refactor Dockview component into more modular structure:
  - Extract layout persistence logic to custom hook
  - Extract component conversion logic to custom hook
  - Make storageKey optional
  - Improve type safety with PanelParams interface
  - Add better error handling and duplicate panel detection
- Add new ProblemDockview wrapper component:
  - Integrates with problem-dockview store
  - Adds locale awareness
  - Provides standardized storage key
- Update related type definitions and imports
This commit is contained in:
cfngc4594 2025-05-06 18:45:08 +08:00
parent c182452dd0
commit f464fb7636
2 changed files with 139 additions and 77 deletions

View File

@ -4,112 +4,140 @@ import type {
AddPanelOptions, AddPanelOptions,
DockviewApi, DockviewApi,
DockviewReadyEvent, DockviewReadyEvent,
IDockviewPanelHeaderProps,
IDockviewPanelProps,
} from "dockview"; } from "dockview";
import "@/styles/dockview.css"; import "@/styles/dockview.css";
import type { LucideIcon } from "lucide-react";
import { useEffect, useMemo, useState } from "react";
import { DockviewReact, themeAbyssSpaced } from "dockview"; import { DockviewReact, themeAbyssSpaced } from "dockview";
import { useCallback, useEffect, useMemo, useState } from "react";
interface PanelContent { export interface PanelParams {
icon?: LucideIcon;
content?: React.ReactNode;
title?: string;
autoAdd?: boolean; autoAdd?: boolean;
} }
interface DockviewProps { interface DockviewProps {
storageKey: string; storageKey?: string;
onApiReady?: (api: DockviewApi) => void; onApiReady?: (api: DockviewApi) => void;
options: AddPanelOptions<PanelContent>[]; components: Record<string, React.ReactNode>;
tabComponents: Record<string, React.ReactNode>;
panelOptions: AddPanelOptions<PanelParams>[];
} }
export default function DockView({ storageKey, onApiReady, options }: DockviewProps) { /**
const [api, setApi] = useState<DockviewApi>(); * Custom hook for handling dockview layout persistence
*/
const { components, tabComponents } = useMemo(() => { const useLayoutPersistence = (api: DockviewApi | null, storageKey?: string) => {
const components: Record<
string,
React.FunctionComponent<IDockviewPanelProps<PanelContent>>
> = {};
const tabComponents: Record<
string,
React.FunctionComponent<IDockviewPanelHeaderProps<PanelContent>>
> = {};
options.forEach((option) => {
const { id, params } = option;
components[id] = () => {
const content = params?.content;
return <>{content}</>;
};
tabComponents[id] = () => {
const Icon = params?.icon;
return (
<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"
/>
)}
{params?.title}
</div>
);
};
});
return { components, tabComponents };
}, [options]);
useEffect(() => { useEffect(() => {
if (!api) return; if (!api || !storageKey) return;
const disposable = api.onDidLayoutChange(() => { const handleLayoutChange = () => {
const layout = api.toJSON(); try {
localStorage.setItem(storageKey, JSON.stringify(layout)); const layout = api.toJSON();
}); localStorage.setItem(storageKey, JSON.stringify(layout));
} catch (error) {
console.error("Failed to save layout:", error);
}
};
const disposable = api.onDidLayoutChange(handleLayoutChange);
return () => disposable.dispose(); return () => disposable.dispose();
}, [api, storageKey]); }, [api, storageKey]);
};
const onReady = (event: DockviewReadyEvent) => { /**
setApi(event.api); * Converts React nodes to dockview component functions
onApiReady?.(event.api); */
const useDockviewComponents = (
components: Record<string, React.ReactNode>,
tabComponents: Record<string, React.ReactNode>
) => {
return useMemo(
() => ({
dockviewComponents: Object.fromEntries(
Object.entries(components).map(([key, value]) => [key, () => value])
),
dockviewTabComponents: Object.fromEntries(
Object.entries(tabComponents).map(([key, value]) => [key, () => value])
),
}),
[components, tabComponents]
);
};
let success = false; const Dockview = ({
const serializedLayout = localStorage.getItem(storageKey); storageKey,
onApiReady,
components,
tabComponents,
panelOptions: options,
}: DockviewProps) => {
const [api, setApi] = useState<DockviewApi | null>(null);
const { dockviewComponents, dockviewTabComponents } = useDockviewComponents(
components,
tabComponents
);
useLayoutPersistence(api, storageKey);
const loadLayoutFromStorage = useCallback(
(api: DockviewApi, key: string): boolean => {
if (!key) return false;
if (serializedLayout) {
try { try {
const layout = JSON.parse(serializedLayout); const serializedLayout = localStorage.getItem(key);
event.api.fromJSON(layout); if (!serializedLayout) return false;
success = true;
api.fromJSON(JSON.parse(serializedLayout));
return true;
} catch (error) { } catch (error) {
console.error("Failed to load layout:", error); console.error("Failed to load layout:", error);
localStorage.removeItem(storageKey); localStorage.removeItem(key);
return false;
} }
} },
[]
);
if (!success) { const addDefaultPanels = useCallback(
(api: DockviewApi, options: AddPanelOptions<PanelParams>[]) => {
const existingIds = new Set<string>();
options.forEach((option) => { options.forEach((option) => {
const autoAdd = option.params?.autoAdd ?? true; if (existingIds.has(option.id)) {
if (!autoAdd) return; console.warn(`Duplicate panel ID detected: ${option.id}`);
event.api.addPanel({ ...option }); return;
}
existingIds.add(option.id);
if (option.params?.autoAdd ?? true) {
api.addPanel(option);
}
}); });
} },
}; []
);
const handleReady = useCallback(
(event: DockviewReadyEvent) => {
setApi(event.api);
const layoutLoaded = storageKey
? loadLayoutFromStorage(event.api, storageKey)
: false;
if (!layoutLoaded) {
addDefaultPanels(event.api, options);
}
onApiReady?.(event.api);
},
[storageKey, loadLayoutFromStorage, addDefaultPanels, onApiReady, options]
);
return ( return (
<DockviewReact <DockviewReact
theme={themeAbyssSpaced} theme={themeAbyssSpaced}
onReady={onReady} onReady={handleReady}
components={components} components={dockviewComponents}
tabComponents={tabComponents} tabComponents={dockviewTabComponents}
/> />
); );
} };
export { Dockview };

View File

@ -0,0 +1,34 @@
"use client";
import { useLocale } from "next-intl";
import type { AddPanelOptions } from "dockview";
import { Dockview, type PanelParams } from "@/components/dockview";
import { useProblemDockviewStore } from "@/stores/problem-dockview";
interface ProblemDockviewProps {
components: Record<string, React.ReactNode>;
tabComponents: Record<string, React.ReactNode>;
panelOptions: AddPanelOptions<PanelParams>[];
}
const ProblemDockview = ({
components,
tabComponents,
panelOptions,
}: ProblemDockviewProps) => {
const locale = useLocale();
const { setApi } = useProblemDockviewStore();
return (
<Dockview
key={locale}
storageKey="dockview:problem"
onApiReady={setApi}
components={components}
tabComponents={tabComponents}
panelOptions={panelOptions}
/>
);
};
export { ProblemDockview };