feat: Show timeout indicator for repository selector (Vibe Kanban) (#1510)
* Done. I've added a 5-second delay to the `list_git_repos` endpoint at `crates/server/src/routes/filesystem.rs:46`. This will let you see: - "Searching for repositories..." for the first 2 seconds - "Still searching... (2s)", "Still searching... (3s)", etc. after that - The "This is taking longer than usual" message after 3 seconds Remember to remove this delay (marked with `TODO`) once you're done testing! * Done, the delay has been removed. * All checks pass. The i18n script now passes with no new violations introduced. * feat: Add timeout indicator to repository selector - Shows progressive feedback when repo search takes longer than expected - Displays "no repos found" message when search completes empty - Added translations for all 6 locales (EN, ES, JA, KO, ZH-Hans, ZH-Hant) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
@@ -38,6 +39,7 @@ const RepoPickerDialogImpl = NiceModal.create<RepoPickerDialogProps>(
|
||||
title = 'Select Repository',
|
||||
description = 'Choose or create a git repository',
|
||||
}) => {
|
||||
const { t } = useTranslation('projects');
|
||||
const modal = useModal();
|
||||
const [stage, setStage] = useState<Stage>('options');
|
||||
const [error, setError] = useState('');
|
||||
@@ -47,6 +49,8 @@ const RepoPickerDialogImpl = NiceModal.create<RepoPickerDialogProps>(
|
||||
const [allRepos, setAllRepos] = useState<DirectoryEntry[]>([]);
|
||||
const [reposLoading, setReposLoading] = useState(false);
|
||||
const [showMoreRepos, setShowMoreRepos] = useState(false);
|
||||
const [loadingDuration, setLoadingDuration] = useState(0);
|
||||
const [hasSearched, setHasSearched] = useState(false);
|
||||
|
||||
// Stage: new
|
||||
const [repoName, setRepoName] = useState('');
|
||||
@@ -60,12 +64,15 @@ const RepoPickerDialogImpl = NiceModal.create<RepoPickerDialogProps>(
|
||||
setShowMoreRepos(false);
|
||||
setRepoName('');
|
||||
setParentPath('');
|
||||
setLoadingDuration(0);
|
||||
setHasSearched(false);
|
||||
}
|
||||
}, [modal.visible]);
|
||||
|
||||
const loadRecentRepos = useCallback(async () => {
|
||||
setReposLoading(true);
|
||||
setError('');
|
||||
setLoadingDuration(0);
|
||||
try {
|
||||
const repos = await fileSystemApi.listGitRepos();
|
||||
setAllRepos(repos);
|
||||
@@ -74,14 +81,33 @@ const RepoPickerDialogImpl = NiceModal.create<RepoPickerDialogProps>(
|
||||
console.error('Failed to load repos:', err);
|
||||
} finally {
|
||||
setReposLoading(false);
|
||||
setHasSearched(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (stage === 'existing' && allRepos.length === 0 && !reposLoading) {
|
||||
if (
|
||||
stage === 'existing' &&
|
||||
allRepos.length === 0 &&
|
||||
!reposLoading &&
|
||||
!hasSearched
|
||||
) {
|
||||
loadRecentRepos();
|
||||
}
|
||||
}, [stage, allRepos.length, reposLoading, loadRecentRepos]);
|
||||
}, [stage, allRepos.length, reposLoading, hasSearched, loadRecentRepos]);
|
||||
|
||||
// Track loading duration to show timeout message
|
||||
useEffect(() => {
|
||||
if (!reposLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
const interval = setInterval(() => {
|
||||
setLoadingDuration((prev) => prev + 1);
|
||||
}, 1000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [reposLoading]);
|
||||
|
||||
const registerAndReturn = async (path: string) => {
|
||||
setIsWorking(true);
|
||||
@@ -220,9 +246,18 @@ const RepoPickerDialogImpl = NiceModal.create<RepoPickerDialogProps>(
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="animate-spin h-5 w-5 border-2 border-muted-foreground border-t-transparent rounded-full" />
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Loading repositories...
|
||||
{loadingDuration < 2
|
||||
? t('repoSearch.searching')
|
||||
: t('repoSearch.stillSearching', {
|
||||
seconds: loadingDuration,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
{loadingDuration >= 3 && (
|
||||
<div className="text-xs text-muted-foreground mt-2 ml-8">
|
||||
{t('repoSearch.takingLonger')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -269,6 +304,26 @@ const RepoPickerDialogImpl = NiceModal.create<RepoPickerDialogProps>(
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* No repos found state */}
|
||||
{!reposLoading &&
|
||||
hasSearched &&
|
||||
allRepos.length === 0 &&
|
||||
!error && (
|
||||
<div className="p-4 border rounded-lg bg-card">
|
||||
<div className="flex items-start gap-3">
|
||||
<Folder className="h-5 w-5 mt-0.5 flex-shrink-0 text-muted-foreground" />
|
||||
<div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{t('repoSearch.noReposFound')}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground mt-1">
|
||||
{t('repoSearch.browseHint')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
className="p-4 border border-dashed cursor-pointer hover:shadow-md transition-shadow rounded-lg bg-card"
|
||||
onClick={() => !isWorking && handleBrowseForRepo()}
|
||||
|
||||
@@ -4,6 +4,13 @@
|
||||
"createProject": "Create Project",
|
||||
"linkToOrganization": "Link to Remote Project",
|
||||
"loading": "Loading projects...",
|
||||
"repoSearch": {
|
||||
"searching": "Searching for repositories...",
|
||||
"stillSearching": "Still searching... ({{seconds}}s)",
|
||||
"takingLonger": "This is taking longer than usual. You can browse manually below.",
|
||||
"noReposFound": "No repositories found in common locations.",
|
||||
"browseHint": "Use the option below to browse for a repository."
|
||||
},
|
||||
"errors": {
|
||||
"fetchFailed": "Failed to fetch projects",
|
||||
"deleteFailed": "Failed to delete project"
|
||||
|
||||
@@ -4,6 +4,13 @@
|
||||
"createProject": "Crear Proyecto",
|
||||
"linkToOrganization": "Vincular a Proyecto Remoto",
|
||||
"loading": "Cargando proyectos...",
|
||||
"repoSearch": {
|
||||
"searching": "Buscando repositorios...",
|
||||
"stillSearching": "Aún buscando... ({{seconds}}s)",
|
||||
"takingLonger": "Esto está tardando más de lo habitual. Puedes buscar manualmente abajo.",
|
||||
"noReposFound": "No se encontraron repositorios en ubicaciones comunes.",
|
||||
"browseHint": "Usa la opción de abajo para buscar un repositorio."
|
||||
},
|
||||
"errors": {
|
||||
"fetchFailed": "Error al cargar proyectos",
|
||||
"deleteFailed": "Error al eliminar el proyecto"
|
||||
|
||||
@@ -4,6 +4,13 @@
|
||||
"createProject": "プロジェクトを作成",
|
||||
"linkToOrganization": "リモートプロジェクトにリンク",
|
||||
"loading": "プロジェクトを読み込み中...",
|
||||
"repoSearch": {
|
||||
"searching": "リポジトリを検索中...",
|
||||
"stillSearching": "検索中... ({{seconds}}秒)",
|
||||
"takingLonger": "通常より時間がかかっています。下から手動で選択できます。",
|
||||
"noReposFound": "一般的な場所にリポジトリが見つかりませんでした。",
|
||||
"browseHint": "下のオプションを使用してリポジトリを参照してください。"
|
||||
},
|
||||
"errors": {
|
||||
"fetchFailed": "プロジェクトの取得に失敗しました",
|
||||
"deleteFailed": "プロジェクトの削除に失敗しました"
|
||||
|
||||
@@ -4,6 +4,13 @@
|
||||
"createProject": "프로젝트 생성",
|
||||
"linkToOrganization": "원격 프로젝트에 연결",
|
||||
"loading": "프로젝트 로딩 중...",
|
||||
"repoSearch": {
|
||||
"searching": "저장소 검색 중...",
|
||||
"stillSearching": "검색 중... ({{seconds}}초)",
|
||||
"takingLonger": "평소보다 시간이 오래 걸리고 있습니다. 아래에서 수동으로 찾아볼 수 있습니다.",
|
||||
"noReposFound": "일반적인 위치에서 저장소를 찾을 수 없습니다.",
|
||||
"browseHint": "아래 옵션을 사용하여 저장소를 찾아보세요."
|
||||
},
|
||||
"errors": {
|
||||
"fetchFailed": "프로젝트를 불러오지 못했습니다",
|
||||
"deleteFailed": "프로젝트 삭제에 실패했습니다"
|
||||
|
||||
@@ -4,6 +4,13 @@
|
||||
"createProject": "创建项目",
|
||||
"linkToOrganization": "链接到远程项目",
|
||||
"loading": "加载项目中...",
|
||||
"repoSearch": {
|
||||
"searching": "正在搜索仓库...",
|
||||
"stillSearching": "仍在搜索... ({{seconds}}秒)",
|
||||
"takingLonger": "搜索时间比平时长。您可以在下方手动浏览。",
|
||||
"noReposFound": "在常见位置未找到仓库。",
|
||||
"browseHint": "使用下方选项浏览仓库。"
|
||||
},
|
||||
"errors": {
|
||||
"fetchFailed": "获取项目失败",
|
||||
"deleteFailed": "删除项目失败"
|
||||
|
||||
@@ -4,6 +4,13 @@
|
||||
"createProject": "建立專案",
|
||||
"linkToOrganization": "連結到遠端專案",
|
||||
"loading": "載入專案中...",
|
||||
"repoSearch": {
|
||||
"searching": "正在搜尋儲存庫...",
|
||||
"stillSearching": "仍在搜尋... ({{seconds}}秒)",
|
||||
"takingLonger": "搜尋時間比平時長。您可以在下方手動瀏覽。",
|
||||
"noReposFound": "在常見位置未找到儲存庫。",
|
||||
"browseHint": "使用下方選項瀏覽儲存庫。"
|
||||
},
|
||||
"errors": {
|
||||
"fetchFailed": "取得專案失敗",
|
||||
"deleteFailed": "刪除專案失敗"
|
||||
|
||||
Reference in New Issue
Block a user