feat: override auto-detected preview url (#1573)
* override auto-detected preview url * i18n * remove redundant useMemo
This commit is contained in:
committed by
GitHub
parent
48d2ce1b80
commit
7224376170
@@ -23,6 +23,7 @@ export function PreviewPanel() {
|
||||
const [showHelp, setShowHelp] = useState(false);
|
||||
const [refreshKey, setRefreshKey] = useState(0);
|
||||
const [showLogs, setShowLogs] = useState(false);
|
||||
const [customUrl, setCustomUrl] = useState<string | null>(null);
|
||||
const listenerRef = useRef<ClickToComponentListener | null>(null);
|
||||
|
||||
const { t } = useTranslation('tasks');
|
||||
@@ -51,6 +52,9 @@ export function PreviewPanel() {
|
||||
lastKnownUrl,
|
||||
});
|
||||
|
||||
// Compute effective URL - custom URL overrides auto-detected
|
||||
const effectiveUrl = customUrl ?? previewState.url;
|
||||
|
||||
const handleRefresh = () => {
|
||||
setIframeError(false);
|
||||
setRefreshKey((prev) => prev + 1);
|
||||
@@ -62,8 +66,8 @@ export function PreviewPanel() {
|
||||
const { addElement } = useClickedElements();
|
||||
|
||||
const handleCopyUrl = async () => {
|
||||
if (previewState.url) {
|
||||
await navigator.clipboard.writeText(previewState.url);
|
||||
if (effectiveUrl) {
|
||||
await navigator.clipboard.writeText(effectiveUrl);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -117,12 +121,12 @@ export function PreviewPanel() {
|
||||
}, [loadingTimeFinished, isReady, latestDevServerProcess, runningDevServer]);
|
||||
|
||||
const isPreviewReady =
|
||||
previewState.status === 'ready' &&
|
||||
Boolean(previewState.url) &&
|
||||
!iframeError;
|
||||
(previewState.status === 'ready' && Boolean(previewState.url)) ||
|
||||
(customUrl !== null && runningDevServer);
|
||||
const isPreviewReadyWithoutError = isPreviewReady && !iframeError;
|
||||
const mode = iframeError
|
||||
? 'error'
|
||||
: isPreviewReady
|
||||
: isPreviewReadyWithoutError
|
||||
? 'ready'
|
||||
: runningDevServer
|
||||
? 'searching'
|
||||
@@ -165,15 +169,18 @@ export function PreviewPanel() {
|
||||
<>
|
||||
<PreviewToolbar
|
||||
mode={mode}
|
||||
url={previewState.url}
|
||||
url={effectiveUrl}
|
||||
onRefresh={handleRefresh}
|
||||
onCopyUrl={handleCopyUrl}
|
||||
onStop={stopDevServer}
|
||||
isStopping={isStoppingDevServer}
|
||||
customUrl={customUrl}
|
||||
detectedUrl={lastKnownUrl?.url}
|
||||
onUrlChange={setCustomUrl}
|
||||
/>
|
||||
<ReadyContent
|
||||
url={previewState.url}
|
||||
iframeKey={`${previewState.url}-${refreshKey}`}
|
||||
url={effectiveUrl}
|
||||
iframeKey={`${effectiveUrl}-${refreshKey}`}
|
||||
onIframeError={handleIframeError}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ExternalLink, RefreshCw, Copy, Loader2, Pause } from 'lucide-react';
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { ExternalLink, RefreshCw, Copy, Loader2, Pause, X } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
@@ -8,6 +9,7 @@ import {
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip';
|
||||
import { NewCardHeader } from '@/components/ui/new-card';
|
||||
import { Input } from '@/components/ui/input';
|
||||
|
||||
interface PreviewToolbarProps {
|
||||
mode: 'noServer' | 'error' | 'ready';
|
||||
@@ -16,6 +18,9 @@ interface PreviewToolbarProps {
|
||||
onCopyUrl: () => void;
|
||||
onStop: () => void;
|
||||
isStopping?: boolean;
|
||||
customUrl: string | null;
|
||||
detectedUrl: string | undefined;
|
||||
onUrlChange: (url: string | null) => void;
|
||||
}
|
||||
|
||||
export function PreviewToolbar({
|
||||
@@ -25,8 +30,49 @@ export function PreviewToolbar({
|
||||
onCopyUrl,
|
||||
onStop,
|
||||
isStopping,
|
||||
customUrl,
|
||||
detectedUrl,
|
||||
onUrlChange,
|
||||
}: PreviewToolbarProps) {
|
||||
const { t } = useTranslation('tasks');
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [urlInput, setUrlInput] = useState('');
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (isEditing && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
inputRef.current.select();
|
||||
}
|
||||
}, [isEditing]);
|
||||
|
||||
const handleStartEdit = () => {
|
||||
setUrlInput(url ?? '');
|
||||
setIsEditing(true);
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
const trimmed = urlInput.trim();
|
||||
if (!trimmed || trimmed === detectedUrl) {
|
||||
// Empty input or detected URL: reset to detected
|
||||
onUrlChange(null);
|
||||
} else {
|
||||
onUrlChange(trimmed);
|
||||
}
|
||||
setIsEditing(false);
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleSubmit();
|
||||
} else if (e.key === 'Escape') {
|
||||
setIsEditing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClearCustomUrl = () => {
|
||||
onUrlChange(null);
|
||||
};
|
||||
|
||||
const actions =
|
||||
mode !== 'noServer' ? (
|
||||
@@ -119,13 +165,49 @@ export function PreviewToolbar({
|
||||
|
||||
return (
|
||||
<NewCardHeader className="shrink-0" actions={actions}>
|
||||
<div className="flex items-center">
|
||||
<span
|
||||
className="text-sm text-muted-foreground font-mono truncate whitespace-nowrap"
|
||||
aria-live="polite"
|
||||
>
|
||||
{url || <Loader2 className="h-4 w-4 animate-spin" />}
|
||||
</span>
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
{isEditing ? (
|
||||
<Input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={urlInput}
|
||||
onChange={(e) => setUrlInput(e.target.value)}
|
||||
onBlur={handleSubmit}
|
||||
onKeyDown={handleKeyDown}
|
||||
className="h-7 text-sm font-mono flex-1"
|
||||
placeholder="http://localhost:3000"
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
onClick={handleStartEdit}
|
||||
className="text-sm text-muted-foreground font-mono truncate hover:text-foreground transition-colors cursor-text text-left"
|
||||
aria-live="polite"
|
||||
title={t('preview.toolbar.clickToEdit')}
|
||||
>
|
||||
{url || <Loader2 className="h-4 w-4 animate-spin" />}
|
||||
</button>
|
||||
{customUrl !== null && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleClearCustomUrl}
|
||||
className="h-5 w-5 p-0 shrink-0"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
{t('preview.toolbar.resetUrl')}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</NewCardHeader>
|
||||
);
|
||||
|
||||
@@ -102,7 +102,9 @@
|
||||
"refresh": "Refresh preview",
|
||||
"copyUrl": "Copy URL",
|
||||
"openInTab": "Open in new tab",
|
||||
"stopDevServer": "Stop dev server"
|
||||
"stopDevServer": "Stop dev server",
|
||||
"clickToEdit": "Click to edit URL",
|
||||
"resetUrl": "Reset to detected URL"
|
||||
}
|
||||
},
|
||||
"diff": {
|
||||
|
||||
@@ -312,7 +312,9 @@
|
||||
"copyUrl": "Copiar URL",
|
||||
"openInTab": "Abrir en nueva pestaña",
|
||||
"refresh": "Actualizar vista previa",
|
||||
"stopDevServer": "Detener servidor de desarrollo"
|
||||
"stopDevServer": "Detener servidor de desarrollo",
|
||||
"clickToEdit": "Clic para editar URL",
|
||||
"resetUrl": "Restablecer a URL detectada"
|
||||
},
|
||||
"troubleAlert": {
|
||||
"item1": "¿Se inició correctamente el servidor de desarrollo? Puede haber un error que necesites resolver, o quizás sea necesario instalar dependencias.",
|
||||
|
||||
@@ -312,7 +312,9 @@
|
||||
"copyUrl": "URLをコピー",
|
||||
"openInTab": "新しいタブで開く",
|
||||
"refresh": "プレビューを更新",
|
||||
"stopDevServer": "開発サーバーを停止"
|
||||
"stopDevServer": "開発サーバーを停止",
|
||||
"clickToEdit": "クリックしてURLを編集",
|
||||
"resetUrl": "検出されたURLにリセット"
|
||||
},
|
||||
"troubleAlert": {
|
||||
"item1": "開発サーバーが正常に起動しましたか?解決すべきバグがあるか、依存関係のインストールが必要な可能性があります。",
|
||||
|
||||
@@ -312,7 +312,9 @@
|
||||
"copyUrl": "URL 복사",
|
||||
"openInTab": "새 탭에서 열기",
|
||||
"refresh": "미리보기 새로고침",
|
||||
"stopDevServer": "개발 서버 중지"
|
||||
"stopDevServer": "개발 서버 중지",
|
||||
"clickToEdit": "URL 편집하려면 클릭",
|
||||
"resetUrl": "감지된 URL로 재설정"
|
||||
},
|
||||
"troubleAlert": {
|
||||
"item1": "개발 서버가 성공적으로 시작되었나요? 해결해야 할 버그가 있거나 종속성을 설치해야 할 수 있습니다.",
|
||||
|
||||
@@ -115,7 +115,9 @@
|
||||
"refresh": "刷新预览",
|
||||
"copyUrl": "复制 URL",
|
||||
"openInTab": "在新标签页中打开",
|
||||
"stopDevServer": "停止开发服务器"
|
||||
"stopDevServer": "停止开发服务器",
|
||||
"clickToEdit": "点击编辑 URL",
|
||||
"resetUrl": "重置为检测到的 URL"
|
||||
}
|
||||
},
|
||||
"diff": {
|
||||
|
||||
Reference in New Issue
Block a user