mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-04 09:13:08 +00:00
refactor(ui): migrate worktree panel to React Query
- Migrate use-worktrees to useWorktrees query hook - Migrate use-branches to useWorktreeBranches query hook - Migrate use-available-editors to useAvailableEditors query hook - Migrate use-worktree-actions to use mutation hooks - Update worktree-panel component to use query data - Remove manual state management for loading/errors Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,65 +1,46 @@
|
|||||||
import { useState, useEffect, useCallback, useMemo } from 'react';
|
import { useMemo, useCallback } from 'react';
|
||||||
import { createLogger } from '@automaker/utils/logger';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { getElectronAPI } from '@/lib/electron';
|
import { getElectronAPI } from '@/lib/electron';
|
||||||
import { useAppStore } from '@/store/app-store';
|
import { useAppStore } from '@/store/app-store';
|
||||||
|
import { useAvailableEditors as useAvailableEditorsQuery } from '@/hooks/queries';
|
||||||
|
import { queryKeys } from '@/lib/query-keys';
|
||||||
import type { EditorInfo } from '@automaker/types';
|
import type { EditorInfo } from '@automaker/types';
|
||||||
|
|
||||||
const logger = createLogger('AvailableEditors');
|
|
||||||
|
|
||||||
// Re-export EditorInfo for convenience
|
// Re-export EditorInfo for convenience
|
||||||
export type { EditorInfo };
|
export type { EditorInfo };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook for fetching and managing available editors
|
||||||
|
*
|
||||||
|
* Uses React Query for data fetching with caching.
|
||||||
|
* Provides a refresh function that clears server cache and re-detects editors.
|
||||||
|
*/
|
||||||
export function useAvailableEditors() {
|
export function useAvailableEditors() {
|
||||||
const [editors, setEditors] = useState<EditorInfo[]>([]);
|
const queryClient = useQueryClient();
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const { data: editors = [], isLoading } = useAvailableEditorsQuery();
|
||||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
|
||||||
|
|
||||||
const fetchAvailableEditors = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
const api = getElectronAPI();
|
|
||||||
if (!api?.worktree?.getAvailableEditors) {
|
|
||||||
setIsLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const result = await api.worktree.getAvailableEditors();
|
|
||||||
if (result.success && result.result?.editors) {
|
|
||||||
setEditors(result.result.editors);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Failed to fetch available editors:', error);
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Refresh editors by clearing the server cache and re-detecting
|
* Mutation to refresh editors by clearing the server cache and re-detecting
|
||||||
* Use this when the user has installed/uninstalled editors
|
* Use this when the user has installed/uninstalled editors
|
||||||
*/
|
*/
|
||||||
const refresh = useCallback(async () => {
|
const { mutate: refreshMutate, isPending: isRefreshing } = useMutation({
|
||||||
setIsRefreshing(true);
|
mutationFn: async () => {
|
||||||
try {
|
|
||||||
const api = getElectronAPI();
|
const api = getElectronAPI();
|
||||||
if (!api?.worktree?.refreshEditors) {
|
|
||||||
// Fallback to regular fetch if refresh not available
|
|
||||||
await fetchAvailableEditors();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const result = await api.worktree.refreshEditors();
|
const result = await api.worktree.refreshEditors();
|
||||||
if (result.success && result.result?.editors) {
|
if (!result.success) {
|
||||||
setEditors(result.result.editors);
|
throw new Error(result.error || 'Failed to refresh editors');
|
||||||
logger.info(`Editor cache refreshed, found ${result.result.editors.length} editors`);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
return result.result?.editors ?? [];
|
||||||
logger.error('Failed to refresh editors:', error);
|
},
|
||||||
} finally {
|
onSuccess: (newEditors) => {
|
||||||
setIsRefreshing(false);
|
// Update the cache with new editors
|
||||||
}
|
queryClient.setQueryData(queryKeys.worktrees.editors(), newEditors);
|
||||||
}, [fetchAvailableEditors]);
|
},
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
const refresh = useCallback(() => {
|
||||||
fetchAvailableEditors();
|
refreshMutate();
|
||||||
}, [fetchAvailableEditors]);
|
}, [refreshMutate]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
editors,
|
editors,
|
||||||
|
|||||||
@@ -1,66 +1,43 @@
|
|||||||
import { useState, useCallback } from 'react';
|
import { useState, useCallback } from 'react';
|
||||||
import { createLogger } from '@automaker/utils/logger';
|
import { useWorktreeBranches } from '@/hooks/queries';
|
||||||
import { getElectronAPI } from '@/lib/electron';
|
import type { GitRepoStatus } from '../types';
|
||||||
import type { BranchInfo, GitRepoStatus } from '../types';
|
|
||||||
|
|
||||||
const logger = createLogger('Branches');
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook for managing branch data with React Query
|
||||||
|
*
|
||||||
|
* Uses useWorktreeBranches for data fetching while maintaining
|
||||||
|
* the current interface for backward compatibility. Tracks which
|
||||||
|
* worktree path is currently being viewed and fetches branches on demand.
|
||||||
|
*/
|
||||||
export function useBranches() {
|
export function useBranches() {
|
||||||
const [branches, setBranches] = useState<BranchInfo[]>([]);
|
const [currentWorktreePath, setCurrentWorktreePath] = useState<string | undefined>();
|
||||||
const [aheadCount, setAheadCount] = useState(0);
|
|
||||||
const [behindCount, setBehindCount] = useState(0);
|
|
||||||
const [isLoadingBranches, setIsLoadingBranches] = useState(false);
|
|
||||||
const [branchFilter, setBranchFilter] = useState('');
|
const [branchFilter, setBranchFilter] = useState('');
|
||||||
const [gitRepoStatus, setGitRepoStatus] = useState<GitRepoStatus>({
|
|
||||||
isGitRepo: true,
|
|
||||||
hasCommits: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
/** Helper to reset branch state to initial values */
|
const {
|
||||||
const resetBranchState = useCallback(() => {
|
data: branchData,
|
||||||
setBranches([]);
|
isLoading: isLoadingBranches,
|
||||||
setAheadCount(0);
|
refetch,
|
||||||
setBehindCount(0);
|
} = useWorktreeBranches(currentWorktreePath);
|
||||||
}, []);
|
|
||||||
|
const branches = branchData?.branches ?? [];
|
||||||
|
const aheadCount = branchData?.aheadCount ?? 0;
|
||||||
|
const behindCount = branchData?.behindCount ?? 0;
|
||||||
|
const gitRepoStatus: GitRepoStatus = {
|
||||||
|
isGitRepo: branchData?.isGitRepo ?? true,
|
||||||
|
hasCommits: branchData?.hasCommits ?? true,
|
||||||
|
};
|
||||||
|
|
||||||
const fetchBranches = useCallback(
|
const fetchBranches = useCallback(
|
||||||
async (worktreePath: string) => {
|
(worktreePath: string) => {
|
||||||
setIsLoadingBranches(true);
|
if (worktreePath === currentWorktreePath) {
|
||||||
try {
|
// Same path - just refetch to get latest data
|
||||||
const api = getElectronAPI();
|
refetch();
|
||||||
if (!api?.worktree?.listBranches) {
|
} else {
|
||||||
logger.warn('List branches API not available');
|
// Different path - update the tracked path (triggers new query)
|
||||||
return;
|
setCurrentWorktreePath(worktreePath);
|
||||||
}
|
|
||||||
const result = await api.worktree.listBranches(worktreePath);
|
|
||||||
if (result.success && result.result) {
|
|
||||||
setBranches(result.result.branches);
|
|
||||||
setAheadCount(result.result.aheadCount || 0);
|
|
||||||
setBehindCount(result.result.behindCount || 0);
|
|
||||||
setGitRepoStatus({ isGitRepo: true, hasCommits: true });
|
|
||||||
} else if (result.code === 'NOT_GIT_REPO') {
|
|
||||||
// Not a git repository - clear branches silently without logging an error
|
|
||||||
resetBranchState();
|
|
||||||
setGitRepoStatus({ isGitRepo: false, hasCommits: false });
|
|
||||||
} else if (result.code === 'NO_COMMITS') {
|
|
||||||
// Git repo but no commits yet - clear branches silently without logging an error
|
|
||||||
resetBranchState();
|
|
||||||
setGitRepoStatus({ isGitRepo: true, hasCommits: false });
|
|
||||||
} else if (!result.success) {
|
|
||||||
// Other errors - log them
|
|
||||||
logger.warn('Failed to fetch branches:', result.error);
|
|
||||||
resetBranchState();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Failed to fetch branches:', error);
|
|
||||||
resetBranchState();
|
|
||||||
// Reset git status to unknown state on network/API errors
|
|
||||||
setGitRepoStatus({ isGitRepo: true, hasCommits: true });
|
|
||||||
} finally {
|
|
||||||
setIsLoadingBranches(false);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[resetBranchState]
|
[currentWorktreePath, refetch]
|
||||||
);
|
);
|
||||||
|
|
||||||
const resetBranchFilter = useCallback(() => {
|
const resetBranchFilter = useCallback(() => {
|
||||||
|
|||||||
@@ -1,152 +1,64 @@
|
|||||||
import { useState, useCallback } from 'react';
|
import { useState, useCallback } from 'react';
|
||||||
import { createLogger } from '@automaker/utils/logger';
|
import {
|
||||||
import { getElectronAPI } from '@/lib/electron';
|
useSwitchBranch,
|
||||||
import { toast } from 'sonner';
|
usePullWorktree,
|
||||||
|
usePushWorktree,
|
||||||
|
useOpenInEditor,
|
||||||
|
} from '@/hooks/mutations';
|
||||||
import type { WorktreeInfo } from '../types';
|
import type { WorktreeInfo } from '../types';
|
||||||
|
|
||||||
const logger = createLogger('WorktreeActions');
|
export function useWorktreeActions() {
|
||||||
|
|
||||||
// Error codes that need special user-friendly handling
|
|
||||||
const GIT_STATUS_ERROR_CODES = ['NOT_GIT_REPO', 'NO_COMMITS'] as const;
|
|
||||||
type GitStatusErrorCode = (typeof GIT_STATUS_ERROR_CODES)[number];
|
|
||||||
|
|
||||||
// User-friendly messages for git status errors
|
|
||||||
const GIT_STATUS_ERROR_MESSAGES: Record<GitStatusErrorCode, string> = {
|
|
||||||
NOT_GIT_REPO: 'This directory is not a git repository',
|
|
||||||
NO_COMMITS: 'Repository has no commits yet. Create an initial commit first.',
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper to handle git status errors with user-friendly messages.
|
|
||||||
* @returns true if the error was a git status error and was handled, false otherwise.
|
|
||||||
*/
|
|
||||||
function handleGitStatusError(result: { code?: string; error?: string }): boolean {
|
|
||||||
const errorCode = result.code as GitStatusErrorCode | undefined;
|
|
||||||
if (errorCode && GIT_STATUS_ERROR_CODES.includes(errorCode)) {
|
|
||||||
toast.info(GIT_STATUS_ERROR_MESSAGES[errorCode] || result.error);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UseWorktreeActionsOptions {
|
|
||||||
fetchWorktrees: () => Promise<Array<{ path: string; branch: string }> | undefined>;
|
|
||||||
fetchBranches: (worktreePath: string) => Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useWorktreeActions({ fetchWorktrees, fetchBranches }: UseWorktreeActionsOptions) {
|
|
||||||
const [isPulling, setIsPulling] = useState(false);
|
|
||||||
const [isPushing, setIsPushing] = useState(false);
|
|
||||||
const [isSwitching, setIsSwitching] = useState(false);
|
|
||||||
const [isActivating, setIsActivating] = useState(false);
|
const [isActivating, setIsActivating] = useState(false);
|
||||||
|
|
||||||
|
// Use React Query mutations
|
||||||
|
const switchBranchMutation = useSwitchBranch();
|
||||||
|
const pullMutation = usePullWorktree();
|
||||||
|
const pushMutation = usePushWorktree();
|
||||||
|
const openInEditorMutation = useOpenInEditor();
|
||||||
|
|
||||||
const handleSwitchBranch = useCallback(
|
const handleSwitchBranch = useCallback(
|
||||||
async (worktree: WorktreeInfo, branchName: string) => {
|
async (worktree: WorktreeInfo, branchName: string) => {
|
||||||
if (isSwitching || branchName === worktree.branch) return;
|
if (switchBranchMutation.isPending || branchName === worktree.branch) return;
|
||||||
setIsSwitching(true);
|
switchBranchMutation.mutate({
|
||||||
try {
|
worktreePath: worktree.path,
|
||||||
const api = getElectronAPI();
|
branchName,
|
||||||
if (!api?.worktree?.switchBranch) {
|
});
|
||||||
toast.error('Switch branch API not available');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const result = await api.worktree.switchBranch(worktree.path, branchName);
|
|
||||||
if (result.success && result.result) {
|
|
||||||
toast.success(result.result.message);
|
|
||||||
fetchWorktrees();
|
|
||||||
} else {
|
|
||||||
if (handleGitStatusError(result)) return;
|
|
||||||
toast.error(result.error || 'Failed to switch branch');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Switch branch failed:', error);
|
|
||||||
toast.error('Failed to switch branch');
|
|
||||||
} finally {
|
|
||||||
setIsSwitching(false);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[isSwitching, fetchWorktrees]
|
[switchBranchMutation]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handlePull = useCallback(
|
const handlePull = useCallback(
|
||||||
async (worktree: WorktreeInfo) => {
|
async (worktree: WorktreeInfo) => {
|
||||||
if (isPulling) return;
|
if (pullMutation.isPending) return;
|
||||||
setIsPulling(true);
|
pullMutation.mutate(worktree.path);
|
||||||
try {
|
|
||||||
const api = getElectronAPI();
|
|
||||||
if (!api?.worktree?.pull) {
|
|
||||||
toast.error('Pull API not available');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const result = await api.worktree.pull(worktree.path);
|
|
||||||
if (result.success && result.result) {
|
|
||||||
toast.success(result.result.message);
|
|
||||||
fetchWorktrees();
|
|
||||||
} else {
|
|
||||||
if (handleGitStatusError(result)) return;
|
|
||||||
toast.error(result.error || 'Failed to pull latest changes');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Pull failed:', error);
|
|
||||||
toast.error('Failed to pull latest changes');
|
|
||||||
} finally {
|
|
||||||
setIsPulling(false);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[isPulling, fetchWorktrees]
|
[pullMutation]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handlePush = useCallback(
|
const handlePush = useCallback(
|
||||||
async (worktree: WorktreeInfo) => {
|
async (worktree: WorktreeInfo) => {
|
||||||
if (isPushing) return;
|
if (pushMutation.isPending) return;
|
||||||
setIsPushing(true);
|
pushMutation.mutate({
|
||||||
try {
|
worktreePath: worktree.path,
|
||||||
const api = getElectronAPI();
|
});
|
||||||
if (!api?.worktree?.push) {
|
|
||||||
toast.error('Push API not available');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const result = await api.worktree.push(worktree.path);
|
|
||||||
if (result.success && result.result) {
|
|
||||||
toast.success(result.result.message);
|
|
||||||
fetchBranches(worktree.path);
|
|
||||||
fetchWorktrees();
|
|
||||||
} else {
|
|
||||||
if (handleGitStatusError(result)) return;
|
|
||||||
toast.error(result.error || 'Failed to push changes');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Push failed:', error);
|
|
||||||
toast.error('Failed to push changes');
|
|
||||||
} finally {
|
|
||||||
setIsPushing(false);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[isPushing, fetchBranches, fetchWorktrees]
|
[pushMutation]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleOpenInEditor = useCallback(async (worktree: WorktreeInfo, editorCommand?: string) => {
|
const handleOpenInEditor = useCallback(
|
||||||
try {
|
async (worktree: WorktreeInfo, editorCommand?: string) => {
|
||||||
const api = getElectronAPI();
|
openInEditorMutation.mutate({
|
||||||
if (!api?.worktree?.openInEditor) {
|
worktreePath: worktree.path,
|
||||||
logger.warn('Open in editor API not available');
|
editorCommand,
|
||||||
return;
|
});
|
||||||
}
|
},
|
||||||
const result = await api.worktree.openInEditor(worktree.path, editorCommand);
|
[openInEditorMutation]
|
||||||
if (result.success && result.result) {
|
);
|
||||||
toast.success(result.result.message);
|
|
||||||
} else if (result.error) {
|
|
||||||
toast.error(result.error);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Open in editor failed:', error);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isPulling,
|
isPulling: pullMutation.isPending,
|
||||||
isPushing,
|
isPushing: pushMutation.isPending,
|
||||||
isSwitching,
|
isSwitching: switchBranchMutation.isPending,
|
||||||
isActivating,
|
isActivating,
|
||||||
setIsActivating,
|
setIsActivating,
|
||||||
handleSwitchBranch,
|
handleSwitchBranch,
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
import { useEffect, useCallback, useRef } from 'react';
|
||||||
import { createLogger } from '@automaker/utils/logger';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { useAppStore } from '@/store/app-store';
|
import { useAppStore } from '@/store/app-store';
|
||||||
import { getElectronAPI } from '@/lib/electron';
|
import { useWorktrees as useWorktreesQuery } from '@/hooks/queries';
|
||||||
|
import { queryKeys } from '@/lib/query-keys';
|
||||||
import { pathsEqual } from '@/lib/utils';
|
import { pathsEqual } from '@/lib/utils';
|
||||||
import type { WorktreeInfo } from '../types';
|
import type { WorktreeInfo } from '../types';
|
||||||
|
|
||||||
const logger = createLogger('Worktrees');
|
|
||||||
|
|
||||||
interface UseWorktreesOptions {
|
interface UseWorktreesOptions {
|
||||||
projectPath: string;
|
projectPath: string;
|
||||||
refreshTrigger?: number;
|
refreshTrigger?: number;
|
||||||
@@ -18,59 +17,46 @@ export function useWorktrees({
|
|||||||
refreshTrigger = 0,
|
refreshTrigger = 0,
|
||||||
onRemovedWorktrees,
|
onRemovedWorktrees,
|
||||||
}: UseWorktreesOptions) {
|
}: UseWorktreesOptions) {
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const queryClient = useQueryClient();
|
||||||
const [worktrees, setWorktrees] = useState<WorktreeInfo[]>([]);
|
|
||||||
|
|
||||||
const currentWorktree = useAppStore((s) => s.getCurrentWorktree(projectPath));
|
const currentWorktree = useAppStore((s) => s.getCurrentWorktree(projectPath));
|
||||||
const setCurrentWorktree = useAppStore((s) => s.setCurrentWorktree);
|
const setCurrentWorktree = useAppStore((s) => s.setCurrentWorktree);
|
||||||
const setWorktreesInStore = useAppStore((s) => s.setWorktrees);
|
const setWorktreesInStore = useAppStore((s) => s.setWorktrees);
|
||||||
const useWorktreesEnabled = useAppStore((s) => s.useWorktrees);
|
const useWorktreesEnabled = useAppStore((s) => s.useWorktrees);
|
||||||
|
|
||||||
const fetchWorktrees = useCallback(
|
// Use the React Query hook
|
||||||
async (options?: { silent?: boolean }) => {
|
const { data, isLoading, refetch } = useWorktreesQuery(projectPath);
|
||||||
if (!projectPath) return;
|
const worktrees = (data?.worktrees ?? []) as WorktreeInfo[];
|
||||||
const silent = options?.silent ?? false;
|
|
||||||
if (!silent) {
|
|
||||||
setIsLoading(true);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const api = getElectronAPI();
|
|
||||||
if (!api?.worktree?.listAll) {
|
|
||||||
logger.warn('Worktree API not available');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const result = await api.worktree.listAll(projectPath, true);
|
|
||||||
if (result.success && result.worktrees) {
|
|
||||||
setWorktrees(result.worktrees);
|
|
||||||
setWorktreesInStore(projectPath, result.worktrees);
|
|
||||||
}
|
|
||||||
// Return removed worktrees so they can be handled by the caller
|
|
||||||
return result.removedWorktrees;
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Failed to fetch worktrees:', error);
|
|
||||||
return undefined;
|
|
||||||
} finally {
|
|
||||||
if (!silent) {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[projectPath, setWorktreesInStore]
|
|
||||||
);
|
|
||||||
|
|
||||||
|
// Sync worktrees to Zustand store when they change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchWorktrees();
|
if (worktrees.length > 0) {
|
||||||
}, [fetchWorktrees]);
|
setWorktreesInStore(projectPath, worktrees);
|
||||||
|
}
|
||||||
|
}, [worktrees, projectPath, setWorktreesInStore]);
|
||||||
|
|
||||||
|
// Handle removed worktrees callback when data changes
|
||||||
|
const prevRemovedWorktreesRef = useRef<string | null>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
if (data?.removedWorktrees && data.removedWorktrees.length > 0) {
|
||||||
|
// Create a stable key to avoid duplicate callbacks
|
||||||
|
const key = JSON.stringify(data.removedWorktrees);
|
||||||
|
if (key !== prevRemovedWorktreesRef.current) {
|
||||||
|
prevRemovedWorktreesRef.current = key;
|
||||||
|
onRemovedWorktrees?.(data.removedWorktrees);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [data?.removedWorktrees, onRemovedWorktrees]);
|
||||||
|
|
||||||
|
// Handle refresh trigger
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (refreshTrigger > 0) {
|
if (refreshTrigger > 0) {
|
||||||
fetchWorktrees().then((removedWorktrees) => {
|
// Invalidate and refetch to get fresh data including any removed worktrees
|
||||||
if (removedWorktrees && removedWorktrees.length > 0 && onRemovedWorktrees) {
|
queryClient.invalidateQueries({
|
||||||
onRemovedWorktrees(removedWorktrees);
|
queryKey: queryKeys.worktrees.all(projectPath),
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [refreshTrigger, fetchWorktrees, onRemovedWorktrees]);
|
}, [refreshTrigger, projectPath, queryClient]);
|
||||||
|
|
||||||
// Use a ref to track the current worktree to avoid running validation
|
// Use a ref to track the current worktree to avoid running validation
|
||||||
// when selection changes (which could cause a race condition with stale worktrees list)
|
// when selection changes (which could cause a race condition with stale worktrees list)
|
||||||
@@ -108,6 +94,14 @@ export function useWorktrees({
|
|||||||
[projectPath, setCurrentWorktree]
|
[projectPath, setCurrentWorktree]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// fetchWorktrees for backward compatibility - now just triggers a refetch
|
||||||
|
const fetchWorktrees = useCallback(async () => {
|
||||||
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: queryKeys.worktrees.all(projectPath),
|
||||||
|
});
|
||||||
|
return refetch();
|
||||||
|
}, [projectPath, queryClient, refetch]);
|
||||||
|
|
||||||
const currentWorktreePath = currentWorktree?.path ?? null;
|
const currentWorktreePath = currentWorktree?.path ?? null;
|
||||||
const selectedWorktree = currentWorktreePath
|
const selectedWorktree = currentWorktreePath
|
||||||
? worktrees.find((w) => pathsEqual(w.path, currentWorktreePath))
|
? worktrees.find((w) => pathsEqual(w.path, currentWorktreePath))
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { cn, pathsEqual } from '@/lib/utils';
|
|||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { getHttpApiClient } from '@/lib/http-api-client';
|
import { getHttpApiClient } from '@/lib/http-api-client';
|
||||||
import { useIsMobile } from '@/hooks/use-media-query';
|
import { useIsMobile } from '@/hooks/use-media-query';
|
||||||
|
import { useWorktreeInitScript } from '@/hooks/queries';
|
||||||
import type { WorktreePanelProps, WorktreeInfo } from './types';
|
import type { WorktreePanelProps, WorktreeInfo } from './types';
|
||||||
import {
|
import {
|
||||||
useWorktrees,
|
useWorktrees,
|
||||||
@@ -79,42 +80,21 @@ export function WorktreePanel({
|
|||||||
handlePull,
|
handlePull,
|
||||||
handlePush,
|
handlePush,
|
||||||
handleOpenInEditor,
|
handleOpenInEditor,
|
||||||
} = useWorktreeActions({
|
} = useWorktreeActions();
|
||||||
fetchWorktrees,
|
|
||||||
fetchBranches,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { hasRunningFeatures } = useRunningFeatures({
|
const { hasRunningFeatures } = useRunningFeatures({
|
||||||
runningFeatureIds,
|
runningFeatureIds,
|
||||||
features,
|
features,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Track whether init script exists for the project
|
// Check if init script exists for the project using React Query
|
||||||
const [hasInitScript, setHasInitScript] = useState(false);
|
const { data: initScriptData } = useWorktreeInitScript(projectPath);
|
||||||
|
const hasInitScript = initScriptData?.exists ?? false;
|
||||||
|
|
||||||
// Log panel state management
|
// Log panel state management
|
||||||
const [logPanelOpen, setLogPanelOpen] = useState(false);
|
const [logPanelOpen, setLogPanelOpen] = useState(false);
|
||||||
const [logPanelWorktree, setLogPanelWorktree] = useState<WorktreeInfo | null>(null);
|
const [logPanelWorktree, setLogPanelWorktree] = useState<WorktreeInfo | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!projectPath) {
|
|
||||||
setHasInitScript(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const checkInitScript = async () => {
|
|
||||||
try {
|
|
||||||
const api = getHttpApiClient();
|
|
||||||
const result = await api.worktree.getInitScript(projectPath);
|
|
||||||
setHasInitScript(result.success && result.exists);
|
|
||||||
} catch {
|
|
||||||
setHasInitScript(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
checkInitScript();
|
|
||||||
}, [projectPath]);
|
|
||||||
|
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
// Periodic interval check (5 seconds) to detect branch changes on disk
|
// Periodic interval check (5 seconds) to detect branch changes on disk
|
||||||
|
|||||||
Reference in New Issue
Block a user