From f464fb76366a79ebcb840d96414f0f9e2c923405 Mon Sep 17 00:00:00 2001 From: cfngc4594 Date: Tue, 6 May 2025 18:45:08 +0800 Subject: [PATCH] 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 --- src/components/dockview.tsx | 182 ++++++++++++--------- src/features/problems/problem-dockview.tsx | 34 ++++ 2 files changed, 139 insertions(+), 77 deletions(-) create mode 100644 src/features/problems/problem-dockview.tsx diff --git a/src/components/dockview.tsx b/src/components/dockview.tsx index 40bd24b..601e1be 100644 --- a/src/components/dockview.tsx +++ b/src/components/dockview.tsx @@ -4,112 +4,140 @@ import type { AddPanelOptions, DockviewApi, DockviewReadyEvent, - IDockviewPanelHeaderProps, - IDockviewPanelProps, } from "dockview"; import "@/styles/dockview.css"; -import type { LucideIcon } from "lucide-react"; -import { useEffect, useMemo, useState } from "react"; import { DockviewReact, themeAbyssSpaced } from "dockview"; +import { useCallback, useEffect, useMemo, useState } from "react"; -interface PanelContent { - icon?: LucideIcon; - content?: React.ReactNode; - title?: string; +export interface PanelParams { autoAdd?: boolean; } interface DockviewProps { - storageKey: string; + storageKey?: string; onApiReady?: (api: DockviewApi) => void; - options: AddPanelOptions[]; + components: Record; + tabComponents: Record; + panelOptions: AddPanelOptions[]; } -export default function DockView({ storageKey, onApiReady, options }: DockviewProps) { - const [api, setApi] = useState(); - - const { components, tabComponents } = useMemo(() => { - const components: Record< - string, - React.FunctionComponent> - > = {}; - const tabComponents: Record< - string, - React.FunctionComponent> - > = {}; - - options.forEach((option) => { - const { id, params } = option; - - components[id] = () => { - const content = params?.content; - return <>{content}; - }; - - tabComponents[id] = () => { - const Icon = params?.icon; - return ( -
- {Icon && ( -
- ); - }; - }); - - return { components, tabComponents }; - }, [options]); - +/** + * Custom hook for handling dockview layout persistence + */ +const useLayoutPersistence = (api: DockviewApi | null, storageKey?: string) => { useEffect(() => { - if (!api) return; + if (!api || !storageKey) return; - const disposable = api.onDidLayoutChange(() => { - const layout = api.toJSON(); - localStorage.setItem(storageKey, JSON.stringify(layout)); - }); + const handleLayoutChange = () => { + try { + 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(); }, [api, storageKey]); +}; - const onReady = (event: DockviewReadyEvent) => { - setApi(event.api); - onApiReady?.(event.api); +/** + * Converts React nodes to dockview component functions + */ +const useDockviewComponents = ( + components: Record, + tabComponents: Record +) => { + 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 serializedLayout = localStorage.getItem(storageKey); +const Dockview = ({ + storageKey, + onApiReady, + components, + tabComponents, + panelOptions: options, +}: DockviewProps) => { + const [api, setApi] = useState(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 { - const layout = JSON.parse(serializedLayout); - event.api.fromJSON(layout); - success = true; + const serializedLayout = localStorage.getItem(key); + if (!serializedLayout) return false; + + api.fromJSON(JSON.parse(serializedLayout)); + return true; } catch (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[]) => { + const existingIds = new Set(); options.forEach((option) => { - const autoAdd = option.params?.autoAdd ?? true; - if (!autoAdd) return; - event.api.addPanel({ ...option }); + if (existingIds.has(option.id)) { + console.warn(`Duplicate panel ID detected: ${option.id}`); + 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 ( ); -} +}; + +export { Dockview }; diff --git a/src/features/problems/problem-dockview.tsx b/src/features/problems/problem-dockview.tsx new file mode 100644 index 0000000..1e4da7e --- /dev/null +++ b/src/features/problems/problem-dockview.tsx @@ -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; + tabComponents: Record; + panelOptions: AddPanelOptions[]; +} + +const ProblemDockview = ({ + components, + tabComponents, + panelOptions, +}: ProblemDockviewProps) => { + const locale = useLocale(); + const { setApi } = useProblemDockviewStore(); + + return ( + + ); +}; + +export { ProblemDockview };