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