Extract panel header to title bar (#625)
This commit is contained in:
@@ -484,6 +484,10 @@ export class PageObject {
|
|||||||
return this.page.getByText("Loading app preview...");
|
return this.page.getByText("Loading app preview...");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
locateStartingAppPreview() {
|
||||||
|
return this.page.getByText("Starting up your app...");
|
||||||
|
}
|
||||||
|
|
||||||
getPreviewIframeElement() {
|
getPreviewIframeElement() {
|
||||||
return this.page.getByTestId("preview-iframe-element");
|
return this.page.getByTestId("preview-iframe-element");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import { selectedAppIdAtom } from "@/atoms/appAtoms";
|
import { selectedAppIdAtom } from "@/atoms/appAtoms";
|
||||||
import { useLoadApps } from "@/hooks/useLoadApps";
|
import { useLoadApps } from "@/hooks/useLoadApps";
|
||||||
import { useRouter } from "@tanstack/react-router";
|
import { useRouter, useLocation } from "@tanstack/react-router";
|
||||||
import { useSettings } from "@/hooks/useSettings";
|
import { useSettings } from "@/hooks/useSettings";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@@ -20,11 +20,13 @@ import {
|
|||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
|
import { PreviewHeader } from "@/components/preview_panel/PreviewHeader";
|
||||||
|
|
||||||
export const TitleBar = () => {
|
export const TitleBar = () => {
|
||||||
const [selectedAppId] = useAtom(selectedAppIdAtom);
|
const [selectedAppId] = useAtom(selectedAppIdAtom);
|
||||||
const { apps } = useLoadApps();
|
const { apps } = useLoadApps();
|
||||||
const { navigate } = useRouter();
|
const { navigate } = useRouter();
|
||||||
|
const location = useLocation();
|
||||||
const { settings, refreshSettings } = useSettings();
|
const { settings, refreshSettings } = useSettings();
|
||||||
const [isSuccessDialogOpen, setIsSuccessDialogOpen] = useState(false);
|
const [isSuccessDialogOpen, setIsSuccessDialogOpen] = useState(false);
|
||||||
const [showWindowControls, setShowWindowControls] = useState(false);
|
const [showWindowControls, setShowWindowControls] = useState(false);
|
||||||
@@ -90,6 +92,14 @@ export const TitleBar = () => {
|
|||||||
{displayText}
|
{displayText}
|
||||||
</Button>
|
</Button>
|
||||||
{isDyadPro && <DyadProButton isDyadProEnabled={isDyadProEnabled} />}
|
{isDyadPro && <DyadProButton isDyadProEnabled={isDyadProEnabled} />}
|
||||||
|
|
||||||
|
{/* Preview Header */}
|
||||||
|
{location.pathname === "/chat" && (
|
||||||
|
<div className="flex-1 flex justify-end no-app-region-drag">
|
||||||
|
<PreviewHeader />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{showWindowControls && <WindowsControls />}
|
{showWindowControls && <WindowsControls />}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
214
src/components/preview_panel/PreviewHeader.tsx
Normal file
214
src/components/preview_panel/PreviewHeader.tsx
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
import { useAtom, useAtomValue } from "jotai";
|
||||||
|
import { previewModeAtom, selectedAppIdAtom } from "../../atoms/appAtoms";
|
||||||
|
import { IpcClient } from "@/ipc/ipc_client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Eye,
|
||||||
|
Code,
|
||||||
|
MoreVertical,
|
||||||
|
Cog,
|
||||||
|
Trash2,
|
||||||
|
AlertTriangle,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import { useEffect, useRef, useState, useCallback } from "react";
|
||||||
|
|
||||||
|
import { useRunApp } from "@/hooks/useRunApp";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import { showError, showSuccess } from "@/lib/toast";
|
||||||
|
import { useMutation } from "@tanstack/react-query";
|
||||||
|
import { useCheckProblems } from "@/hooks/useCheckProblems";
|
||||||
|
import { isPreviewOpenAtom } from "@/atoms/viewAtoms";
|
||||||
|
|
||||||
|
export type PreviewMode = "preview" | "code" | "problems";
|
||||||
|
|
||||||
|
// Preview Header component with preview mode toggle
|
||||||
|
export const PreviewHeader = () => {
|
||||||
|
const [previewMode, setPreviewMode] = useAtom(previewModeAtom);
|
||||||
|
const [isPreviewOpen, setIsPreviewOpen] = useAtom(isPreviewOpenAtom);
|
||||||
|
const selectedAppId = useAtomValue(selectedAppIdAtom);
|
||||||
|
const previewRef = useRef<HTMLButtonElement>(null);
|
||||||
|
const codeRef = useRef<HTMLButtonElement>(null);
|
||||||
|
const problemsRef = useRef<HTMLButtonElement>(null);
|
||||||
|
const [indicatorStyle, setIndicatorStyle] = useState({ left: 0, width: 0 });
|
||||||
|
const { problemReport } = useCheckProblems(selectedAppId);
|
||||||
|
const { restartApp, refreshAppIframe } = useRunApp();
|
||||||
|
|
||||||
|
const selectPanel = (panel: PreviewMode) => {
|
||||||
|
if (previewMode === panel) {
|
||||||
|
setIsPreviewOpen(!isPreviewOpen);
|
||||||
|
} else {
|
||||||
|
setPreviewMode(panel);
|
||||||
|
setIsPreviewOpen(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCleanRestart = useCallback(() => {
|
||||||
|
restartApp({ removeNodeModules: true });
|
||||||
|
}, [restartApp]);
|
||||||
|
|
||||||
|
const useClearSessionData = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: () => {
|
||||||
|
const ipcClient = IpcClient.getInstance();
|
||||||
|
return ipcClient.clearSessionData();
|
||||||
|
},
|
||||||
|
onSuccess: async () => {
|
||||||
|
await refreshAppIframe();
|
||||||
|
showSuccess("Preview data cleared");
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
showError(`Error clearing preview data: ${error}`);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const { mutate: clearSessionData } = useClearSessionData();
|
||||||
|
|
||||||
|
const onClearSessionData = useCallback(() => {
|
||||||
|
clearSessionData();
|
||||||
|
}, [clearSessionData]);
|
||||||
|
|
||||||
|
// Get the problem count for the selected app
|
||||||
|
const problemCount = problemReport ? problemReport.problems.length : 0;
|
||||||
|
|
||||||
|
// Format the problem count for display
|
||||||
|
const formatProblemCount = (count: number): string => {
|
||||||
|
if (count === 0) return "";
|
||||||
|
if (count > 100) return "100+";
|
||||||
|
return count.toString();
|
||||||
|
};
|
||||||
|
|
||||||
|
const displayCount = formatProblemCount(problemCount);
|
||||||
|
|
||||||
|
// Update indicator position when mode changes
|
||||||
|
useEffect(() => {
|
||||||
|
const updateIndicator = () => {
|
||||||
|
let targetRef: React.RefObject<HTMLButtonElement | null>;
|
||||||
|
|
||||||
|
switch (previewMode) {
|
||||||
|
case "preview":
|
||||||
|
targetRef = previewRef;
|
||||||
|
break;
|
||||||
|
case "code":
|
||||||
|
targetRef = codeRef;
|
||||||
|
break;
|
||||||
|
case "problems":
|
||||||
|
targetRef = problemsRef;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetRef.current) {
|
||||||
|
const button = targetRef.current;
|
||||||
|
const container = button.parentElement;
|
||||||
|
if (container) {
|
||||||
|
const containerRect = container.getBoundingClientRect();
|
||||||
|
const buttonRect = button.getBoundingClientRect();
|
||||||
|
const left = buttonRect.left - containerRect.left;
|
||||||
|
const width = buttonRect.width;
|
||||||
|
|
||||||
|
setIndicatorStyle({ left, width });
|
||||||
|
if (!isPreviewOpen) {
|
||||||
|
setIndicatorStyle({ left: left, width: 0 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Small delay to ensure DOM is updated
|
||||||
|
const timeoutId = setTimeout(updateIndicator, 10);
|
||||||
|
return () => clearTimeout(timeoutId);
|
||||||
|
}, [previewMode, displayCount, isPreviewOpen]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between px-4 py-2 mt-1 border-b border-border">
|
||||||
|
<div className="relative flex rounded-md p-0.5 gap-2">
|
||||||
|
<motion.div
|
||||||
|
className="absolute top-0.5 bottom-0.5 bg-[var(--background-lightest)] shadow rounded-md"
|
||||||
|
animate={{
|
||||||
|
left: indicatorStyle.left,
|
||||||
|
width: indicatorStyle.width,
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
type: "spring",
|
||||||
|
stiffness: 600,
|
||||||
|
damping: 35,
|
||||||
|
mass: 0.6,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
data-testid="preview-mode-button"
|
||||||
|
ref={previewRef}
|
||||||
|
className="cursor-pointer relative flex items-center gap-1 px-2 py-1 rounded-md text-sm font-medium z-10"
|
||||||
|
onClick={() => selectPanel("preview")}
|
||||||
|
>
|
||||||
|
<Eye size={14} />
|
||||||
|
<span>Preview</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
data-testid="problems-mode-button"
|
||||||
|
ref={problemsRef}
|
||||||
|
className="cursor-pointer relative flex items-center gap-1 px-2 py-1 rounded-md text-sm font-medium z-10"
|
||||||
|
onClick={() => selectPanel("problems")}
|
||||||
|
>
|
||||||
|
<AlertTriangle size={14} />
|
||||||
|
<span>Problems</span>
|
||||||
|
{displayCount && (
|
||||||
|
<span className="ml-0.5 px-1 py-0.5 text-xs font-medium bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300 rounded-full min-w-[16px] text-center">
|
||||||
|
{displayCount}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
data-testid="code-mode-button"
|
||||||
|
ref={codeRef}
|
||||||
|
className="cursor-pointer relative flex items-center gap-1 px-2 py-1 rounded-md text-sm font-medium z-10"
|
||||||
|
onClick={() => selectPanel("code")}
|
||||||
|
>
|
||||||
|
<Code size={14} />
|
||||||
|
<span>Code</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<button
|
||||||
|
data-testid="preview-more-options-button"
|
||||||
|
className="flex items-center justify-center p-1.5 rounded-md text-sm hover:bg-[var(--background-darkest)] transition-colors"
|
||||||
|
title="More options"
|
||||||
|
>
|
||||||
|
<MoreVertical size={16} />
|
||||||
|
</button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end" className="w-60">
|
||||||
|
<DropdownMenuItem onClick={onCleanRestart}>
|
||||||
|
<Cog size={16} />
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span>Rebuild</span>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
Re-installs node_modules and restarts
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={onClearSessionData}>
|
||||||
|
<Trash2 size={16} />
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span>Clear Cache</span>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
Clears cookies and local storage and other app cache
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
Lightbulb,
|
Lightbulb,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
MousePointerClick,
|
MousePointerClick,
|
||||||
|
Power,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { selectedChatIdAtom } from "@/atoms/chatAtoms";
|
import { selectedChatIdAtom } from "@/atoms/chatAtoms";
|
||||||
import { IpcClient } from "@/ipc/ipc_client";
|
import { IpcClient } from "@/ipc/ipc_client";
|
||||||
@@ -38,6 +39,7 @@ import {
|
|||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
|
import { useRunApp } from "@/hooks/useRunApp";
|
||||||
|
|
||||||
interface ErrorBannerProps {
|
interface ErrorBannerProps {
|
||||||
error: string | undefined;
|
error: string | undefined;
|
||||||
@@ -129,7 +131,7 @@ export const PreviewIframe = ({ loading }: { loading: boolean }) => {
|
|||||||
const [availableRoutes, setAvailableRoutes] = useState<
|
const [availableRoutes, setAvailableRoutes] = useState<
|
||||||
Array<{ path: string; label: string }>
|
Array<{ path: string; label: string }>
|
||||||
>([]);
|
>([]);
|
||||||
|
const { restartApp } = useRunApp();
|
||||||
// Load router related files to extract routes
|
// Load router related files to extract routes
|
||||||
const { content: routerContent } = useLoadAppFile(
|
const { content: routerContent } = useLoadAppFile(
|
||||||
selectedAppId,
|
selectedAppId,
|
||||||
@@ -423,6 +425,10 @@ export const PreviewIframe = ({ loading }: { loading: boolean }) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onRestart = () => {
|
||||||
|
restartApp();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
{/* Browser-style header */}
|
{/* Browser-style header */}
|
||||||
@@ -519,6 +525,14 @@ export const PreviewIframe = ({ loading }: { loading: boolean }) => {
|
|||||||
|
|
||||||
{/* Action Buttons */}
|
{/* Action Buttons */}
|
||||||
<div className="flex space-x-1">
|
<div className="flex space-x-1">
|
||||||
|
<button
|
||||||
|
onClick={onRestart}
|
||||||
|
className="flex items-center space-x-1 px-3 py-1 rounded-md text-sm hover:bg-[var(--background-darkest)] transition-colors"
|
||||||
|
title="Restart App"
|
||||||
|
>
|
||||||
|
<Power size={16} />
|
||||||
|
<span>Restart</span>
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
data-testid="preview-open-browser-button"
|
data-testid="preview-open-browser-button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
@@ -5,47 +5,15 @@ import {
|
|||||||
previewPanelKeyAtom,
|
previewPanelKeyAtom,
|
||||||
selectedAppIdAtom,
|
selectedAppIdAtom,
|
||||||
} from "../../atoms/appAtoms";
|
} from "../../atoms/appAtoms";
|
||||||
import { IpcClient } from "@/ipc/ipc_client";
|
|
||||||
|
|
||||||
import { CodeView } from "./CodeView";
|
import { CodeView } from "./CodeView";
|
||||||
import { PreviewIframe } from "./PreviewIframe";
|
import { PreviewIframe } from "./PreviewIframe";
|
||||||
import { Problems } from "./Problems";
|
import { Problems } from "./Problems";
|
||||||
import {
|
import { ChevronDown, ChevronUp, Logs } from "lucide-react";
|
||||||
Eye,
|
import { useEffect, useRef, useState } from "react";
|
||||||
Code,
|
|
||||||
ChevronDown,
|
|
||||||
ChevronUp,
|
|
||||||
Logs,
|
|
||||||
MoreVertical,
|
|
||||||
Cog,
|
|
||||||
Power,
|
|
||||||
Trash2,
|
|
||||||
AlertTriangle,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { motion } from "framer-motion";
|
|
||||||
import { useEffect, useRef, useState, useCallback } from "react";
|
|
||||||
import { PanelGroup, Panel, PanelResizeHandle } from "react-resizable-panels";
|
import { PanelGroup, Panel, PanelResizeHandle } from "react-resizable-panels";
|
||||||
import { Console } from "./Console";
|
import { Console } from "./Console";
|
||||||
import { useRunApp } from "@/hooks/useRunApp";
|
import { useRunApp } from "@/hooks/useRunApp";
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from "@/components/ui/dropdown-menu";
|
|
||||||
import { showError, showSuccess } from "@/lib/toast";
|
|
||||||
import { useMutation } from "@tanstack/react-query";
|
|
||||||
import { useCheckProblems } from "@/hooks/useCheckProblems";
|
|
||||||
|
|
||||||
type PreviewMode = "preview" | "code" | "problems";
|
|
||||||
|
|
||||||
interface PreviewHeaderProps {
|
|
||||||
previewMode: PreviewMode;
|
|
||||||
setPreviewMode: (mode: PreviewMode) => void;
|
|
||||||
onRestart: () => void;
|
|
||||||
onCleanRestart: () => void;
|
|
||||||
onClearSessionData: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ConsoleHeaderProps {
|
interface ConsoleHeaderProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -53,164 +21,6 @@ interface ConsoleHeaderProps {
|
|||||||
latestMessage?: string;
|
latestMessage?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Preview Header component with preview mode toggle
|
|
||||||
const PreviewHeader = ({
|
|
||||||
previewMode,
|
|
||||||
setPreviewMode,
|
|
||||||
onRestart,
|
|
||||||
onCleanRestart,
|
|
||||||
onClearSessionData,
|
|
||||||
}: PreviewHeaderProps) => {
|
|
||||||
const selectedAppId = useAtomValue(selectedAppIdAtom);
|
|
||||||
const previewRef = useRef<HTMLButtonElement>(null);
|
|
||||||
const codeRef = useRef<HTMLButtonElement>(null);
|
|
||||||
const problemsRef = useRef<HTMLButtonElement>(null);
|
|
||||||
const [indicatorStyle, setIndicatorStyle] = useState({ left: 0, width: 0 });
|
|
||||||
const { problemReport } = useCheckProblems(selectedAppId);
|
|
||||||
// Get the problem count for the selected app
|
|
||||||
const problemCount = problemReport ? problemReport.problems.length : 0;
|
|
||||||
|
|
||||||
// Format the problem count for display
|
|
||||||
const formatProblemCount = (count: number): string => {
|
|
||||||
if (count === 0) return "";
|
|
||||||
if (count > 100) return "100+";
|
|
||||||
return count.toString();
|
|
||||||
};
|
|
||||||
|
|
||||||
const displayCount = formatProblemCount(problemCount);
|
|
||||||
|
|
||||||
// Update indicator position when mode changes
|
|
||||||
useEffect(() => {
|
|
||||||
const updateIndicator = () => {
|
|
||||||
let targetRef: React.RefObject<HTMLButtonElement | null>;
|
|
||||||
|
|
||||||
switch (previewMode) {
|
|
||||||
case "preview":
|
|
||||||
targetRef = previewRef;
|
|
||||||
break;
|
|
||||||
case "code":
|
|
||||||
targetRef = codeRef;
|
|
||||||
break;
|
|
||||||
case "problems":
|
|
||||||
targetRef = problemsRef;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (targetRef.current) {
|
|
||||||
const button = targetRef.current;
|
|
||||||
const container = button.parentElement;
|
|
||||||
if (container) {
|
|
||||||
const containerRect = container.getBoundingClientRect();
|
|
||||||
const buttonRect = button.getBoundingClientRect();
|
|
||||||
const left = buttonRect.left - containerRect.left;
|
|
||||||
const width = buttonRect.width;
|
|
||||||
|
|
||||||
setIndicatorStyle({ left, width });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Small delay to ensure DOM is updated
|
|
||||||
const timeoutId = setTimeout(updateIndicator, 10);
|
|
||||||
return () => clearTimeout(timeoutId);
|
|
||||||
}, [previewMode, displayCount]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-between px-4 py-2 border-b border-border">
|
|
||||||
<div className="relative flex bg-[var(--background-darkest)] rounded-md p-0.5">
|
|
||||||
<motion.div
|
|
||||||
className="absolute top-0.5 bottom-0.5 bg-[var(--background-lightest)] shadow rounded-md"
|
|
||||||
animate={{
|
|
||||||
left: indicatorStyle.left,
|
|
||||||
width: indicatorStyle.width,
|
|
||||||
}}
|
|
||||||
transition={{
|
|
||||||
type: "spring",
|
|
||||||
stiffness: 600,
|
|
||||||
damping: 35,
|
|
||||||
mass: 0.6,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
data-testid="preview-mode-button"
|
|
||||||
ref={previewRef}
|
|
||||||
className="relative flex items-center gap-1 px-2 py-1 rounded-md text-sm font-medium z-10"
|
|
||||||
onClick={() => setPreviewMode("preview")}
|
|
||||||
>
|
|
||||||
<Eye size={14} />
|
|
||||||
<span>Preview</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
data-testid="problems-mode-button"
|
|
||||||
ref={problemsRef}
|
|
||||||
className="relative flex items-center gap-1 px-2 py-1 rounded-md text-sm font-medium z-10"
|
|
||||||
onClick={() => setPreviewMode("problems")}
|
|
||||||
>
|
|
||||||
<AlertTriangle size={14} />
|
|
||||||
<span>Problems</span>
|
|
||||||
{displayCount && (
|
|
||||||
<span className="ml-0.5 px-1 py-0.5 text-xs font-medium bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300 rounded-full min-w-[16px] text-center">
|
|
||||||
{displayCount}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
data-testid="code-mode-button"
|
|
||||||
ref={codeRef}
|
|
||||||
className="relative flex items-center gap-1 px-2 py-1 rounded-md text-sm font-medium z-10"
|
|
||||||
onClick={() => setPreviewMode("code")}
|
|
||||||
>
|
|
||||||
<Code size={14} />
|
|
||||||
<span>Code</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<button
|
|
||||||
onClick={onRestart}
|
|
||||||
className="flex items-center space-x-1 px-3 py-1 rounded-md text-sm hover:bg-[var(--background-darkest)] transition-colors"
|
|
||||||
title="Restart App"
|
|
||||||
>
|
|
||||||
<Power size={16} />
|
|
||||||
<span>Restart</span>
|
|
||||||
</button>
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<button
|
|
||||||
data-testid="preview-more-options-button"
|
|
||||||
className="flex items-center justify-center p-1.5 rounded-md text-sm hover:bg-[var(--background-darkest)] transition-colors"
|
|
||||||
title="More options"
|
|
||||||
>
|
|
||||||
<MoreVertical size={16} />
|
|
||||||
</button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="end" className="w-60">
|
|
||||||
<DropdownMenuItem onClick={onCleanRestart}>
|
|
||||||
<Cog size={16} />
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<span>Rebuild</span>
|
|
||||||
<span className="text-xs text-muted-foreground">
|
|
||||||
Re-installs node_modules and restarts
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem onClick={onClearSessionData}>
|
|
||||||
<Trash2 size={16} />
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<span>Clear Cache</span>
|
|
||||||
<span className="text-xs text-muted-foreground">
|
|
||||||
Clears cookies and local storage and other app cache
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Console header component
|
// Console header component
|
||||||
const ConsoleHeader = ({
|
const ConsoleHeader = ({
|
||||||
isOpen,
|
isOpen,
|
||||||
@@ -237,11 +47,10 @@ const ConsoleHeader = ({
|
|||||||
|
|
||||||
// Main PreviewPanel component
|
// Main PreviewPanel component
|
||||||
export function PreviewPanel() {
|
export function PreviewPanel() {
|
||||||
const [previewMode, setPreviewMode] = useAtom(previewModeAtom);
|
const [previewMode] = useAtom(previewModeAtom);
|
||||||
const selectedAppId = useAtomValue(selectedAppIdAtom);
|
const selectedAppId = useAtomValue(selectedAppIdAtom);
|
||||||
const [isConsoleOpen, setIsConsoleOpen] = useState(false);
|
const [isConsoleOpen, setIsConsoleOpen] = useState(false);
|
||||||
const { runApp, stopApp, restartApp, loading, app, refreshAppIframe } =
|
const { runApp, stopApp, loading, app } = useRunApp();
|
||||||
useRunApp();
|
|
||||||
const runningAppIdRef = useRef<number | null>(null);
|
const runningAppIdRef = useRef<number | null>(null);
|
||||||
const key = useAtomValue(previewPanelKeyAtom);
|
const key = useAtomValue(previewPanelKeyAtom);
|
||||||
const appOutput = useAtomValue(appOutputAtom);
|
const appOutput = useAtomValue(appOutputAtom);
|
||||||
@@ -250,37 +59,6 @@ export function PreviewPanel() {
|
|||||||
const latestMessage =
|
const latestMessage =
|
||||||
messageCount > 0 ? appOutput[messageCount - 1]?.message : undefined;
|
messageCount > 0 ? appOutput[messageCount - 1]?.message : undefined;
|
||||||
|
|
||||||
const handleRestart = useCallback(() => {
|
|
||||||
restartApp();
|
|
||||||
}, [restartApp]);
|
|
||||||
|
|
||||||
const handleCleanRestart = useCallback(() => {
|
|
||||||
restartApp({ removeNodeModules: true });
|
|
||||||
}, [restartApp]);
|
|
||||||
|
|
||||||
const useClearSessionData = () => {
|
|
||||||
return useMutation({
|
|
||||||
mutationFn: () => {
|
|
||||||
const ipcClient = IpcClient.getInstance();
|
|
||||||
return ipcClient.clearSessionData();
|
|
||||||
},
|
|
||||||
onSuccess: async () => {
|
|
||||||
await refreshAppIframe();
|
|
||||||
showSuccess("Preview data cleared");
|
|
||||||
// Optionally invalidate relevant queries
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
showError(`Error clearing preview data: ${error}`);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const { mutate: clearSessionData } = useClearSessionData();
|
|
||||||
|
|
||||||
const handleClearSessionData = useCallback(() => {
|
|
||||||
clearSessionData();
|
|
||||||
}, [selectedAppId, clearSessionData]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const previousAppId = runningAppIdRef.current;
|
const previousAppId = runningAppIdRef.current;
|
||||||
|
|
||||||
@@ -327,13 +105,6 @@ export function PreviewPanel() {
|
|||||||
}, [selectedAppId, runApp, stopApp]);
|
}, [selectedAppId, runApp, stopApp]);
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
<PreviewHeader
|
|
||||||
previewMode={previewMode}
|
|
||||||
setPreviewMode={setPreviewMode}
|
|
||||||
onRestart={handleRestart}
|
|
||||||
onCleanRestart={handleCleanRestart}
|
|
||||||
onClearSessionData={handleClearSessionData}
|
|
||||||
/>
|
|
||||||
<div className="flex-1 overflow-hidden">
|
<div className="flex-1 overflow-hidden">
|
||||||
<PanelGroup direction="vertical">
|
<PanelGroup direction="vertical">
|
||||||
<Panel id="content" minSize={30}>
|
<Panel id="content" minSize={30}>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useState, useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
|
import { atom } from "jotai";
|
||||||
import { IpcClient } from "@/ipc/ipc_client";
|
import { IpcClient } from "@/ipc/ipc_client";
|
||||||
import {
|
import {
|
||||||
appOutputAtom,
|
appOutputAtom,
|
||||||
@@ -11,8 +12,10 @@ import {
|
|||||||
import { useAtom, useAtomValue, useSetAtom } from "jotai";
|
import { useAtom, useAtomValue, useSetAtom } from "jotai";
|
||||||
import { AppOutput } from "@/ipc/ipc_types";
|
import { AppOutput } from "@/ipc/ipc_types";
|
||||||
|
|
||||||
|
const useRunAppLoadingAtom = atom(false);
|
||||||
|
|
||||||
export function useRunApp() {
|
export function useRunApp() {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useAtom(useRunAppLoadingAtom);
|
||||||
const [app, setApp] = useAtom(currentAppAtom);
|
const [app, setApp] = useAtom(currentAppAtom);
|
||||||
const setAppOutput = useSetAtom(appOutputAtom);
|
const setAppOutput = useSetAtom(appOutputAtom);
|
||||||
const [appUrlObj, setAppUrlObj] = useAtom(appUrlAtom);
|
const [appUrlObj, setAppUrlObj] = useAtom(appUrlAtom);
|
||||||
|
|||||||
Reference in New Issue
Block a user