mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-06-11 16:23:03 +08:00
Refactor Settings, FileTree, GitPanel, Shell, and CodeEditor components (#402)
This commit is contained in:
44
src/components/file-tree/hooks/useExpandedDirectories.ts
Normal file
44
src/components/file-tree/hooks/useExpandedDirectories.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
type UseExpandedDirectoriesResult = {
|
||||
expandedDirs: Set<string>;
|
||||
toggleDirectory: (path: string) => void;
|
||||
expandDirectories: (paths: string[]) => void;
|
||||
};
|
||||
|
||||
export function useExpandedDirectories(): UseExpandedDirectoriesResult {
|
||||
const [expandedDirs, setExpandedDirs] = useState<Set<string>>(() => new Set());
|
||||
|
||||
const toggleDirectory = useCallback((path: string) => {
|
||||
setExpandedDirs((previous) => {
|
||||
const next = new Set(previous);
|
||||
|
||||
if (next.has(path)) {
|
||||
next.delete(path);
|
||||
} else {
|
||||
next.add(path);
|
||||
}
|
||||
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const expandDirectories = useCallback((paths: string[]) => {
|
||||
if (paths.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
setExpandedDirs((previous) => {
|
||||
const next = new Set(previous);
|
||||
paths.forEach((path) => next.add(path));
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
return {
|
||||
expandedDirs,
|
||||
toggleDirectory,
|
||||
expandDirectories,
|
||||
};
|
||||
}
|
||||
|
||||
76
src/components/file-tree/hooks/useFileTreeData.ts
Normal file
76
src/components/file-tree/hooks/useFileTreeData.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { api } from '../../../utils/api';
|
||||
import type { Project } from '../../../types/app';
|
||||
import type { FileTreeNode } from '../types/types';
|
||||
|
||||
type UseFileTreeDataResult = {
|
||||
files: FileTreeNode[];
|
||||
loading: boolean;
|
||||
};
|
||||
|
||||
export function useFileTreeData(selectedProject: Project | null): UseFileTreeDataResult {
|
||||
const [files, setFiles] = useState<FileTreeNode[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const projectName = selectedProject?.name;
|
||||
|
||||
if (!projectName) {
|
||||
setFiles([]);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const abortController = new AbortController();
|
||||
// Track mount state so aborted or late responses do not enqueue stale state updates.
|
||||
let isActive = true;
|
||||
|
||||
const fetchFiles = async () => {
|
||||
if (isActive) {
|
||||
setLoading(true);
|
||||
}
|
||||
try {
|
||||
const response = await api.getFiles(projectName, { signal: abortController.signal });
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error('File fetch failed:', response.status, errorText);
|
||||
if (isActive) {
|
||||
setFiles([]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const data = (await response.json()) as FileTreeNode[];
|
||||
if (isActive) {
|
||||
setFiles(data);
|
||||
}
|
||||
} catch (error) {
|
||||
if ((error as { name?: string }).name === 'AbortError') {
|
||||
return;
|
||||
}
|
||||
|
||||
console.error('Error fetching files:', error);
|
||||
if (isActive) {
|
||||
setFiles([]);
|
||||
}
|
||||
} finally {
|
||||
if (isActive) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
void fetchFiles();
|
||||
|
||||
return () => {
|
||||
isActive = false;
|
||||
abortController.abort();
|
||||
};
|
||||
}, [selectedProject?.name]);
|
||||
|
||||
return {
|
||||
files,
|
||||
loading,
|
||||
};
|
||||
}
|
||||
42
src/components/file-tree/hooks/useFileTreeSearch.ts
Normal file
42
src/components/file-tree/hooks/useFileTreeSearch.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { collectExpandedDirectoryPaths, filterFileTree } from '../utils/fileTreeUtils';
|
||||
import type { FileTreeNode } from '../types/types';
|
||||
|
||||
type UseFileTreeSearchArgs = {
|
||||
files: FileTreeNode[];
|
||||
expandDirectories: (paths: string[]) => void;
|
||||
};
|
||||
|
||||
type UseFileTreeSearchResult = {
|
||||
searchQuery: string;
|
||||
setSearchQuery: (query: string) => void;
|
||||
filteredFiles: FileTreeNode[];
|
||||
};
|
||||
|
||||
export function useFileTreeSearch({
|
||||
files,
|
||||
expandDirectories,
|
||||
}: UseFileTreeSearchArgs): UseFileTreeSearchResult {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [filteredFiles, setFilteredFiles] = useState<FileTreeNode[]>(files);
|
||||
|
||||
useEffect(() => {
|
||||
const query = searchQuery.trim().toLowerCase();
|
||||
|
||||
if (!query) {
|
||||
setFilteredFiles(files);
|
||||
return;
|
||||
}
|
||||
|
||||
const filtered = filterFileTree(files, query);
|
||||
setFilteredFiles(filtered);
|
||||
// Keep search results visible by opening every matching ancestor directory once per query update.
|
||||
expandDirectories(collectExpandedDirectoryPaths(filtered));
|
||||
}, [files, searchQuery, expandDirectories]);
|
||||
|
||||
return {
|
||||
searchQuery,
|
||||
setSearchQuery,
|
||||
filteredFiles,
|
||||
};
|
||||
}
|
||||
43
src/components/file-tree/hooks/useFileTreeViewMode.ts
Normal file
43
src/components/file-tree/hooks/useFileTreeViewMode.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import {
|
||||
FILE_TREE_DEFAULT_VIEW_MODE,
|
||||
FILE_TREE_VIEW_MODES,
|
||||
FILE_TREE_VIEW_MODE_STORAGE_KEY,
|
||||
} from '../constants/constants';
|
||||
import type { FileTreeViewMode } from '../types/types';
|
||||
|
||||
type UseFileTreeViewModeResult = {
|
||||
viewMode: FileTreeViewMode;
|
||||
changeViewMode: (mode: FileTreeViewMode) => void;
|
||||
};
|
||||
|
||||
export function useFileTreeViewMode(): UseFileTreeViewModeResult {
|
||||
const [viewMode, setViewMode] = useState<FileTreeViewMode>(FILE_TREE_DEFAULT_VIEW_MODE);
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
const savedViewMode = localStorage.getItem(FILE_TREE_VIEW_MODE_STORAGE_KEY);
|
||||
if (savedViewMode && FILE_TREE_VIEW_MODES.includes(savedViewMode as FileTreeViewMode)) {
|
||||
setViewMode(savedViewMode as FileTreeViewMode);
|
||||
}
|
||||
} catch {
|
||||
// Keep default view mode when storage is unavailable.
|
||||
}
|
||||
}, []);
|
||||
|
||||
const changeViewMode = useCallback((mode: FileTreeViewMode) => {
|
||||
setViewMode(mode);
|
||||
|
||||
try {
|
||||
localStorage.setItem(FILE_TREE_VIEW_MODE_STORAGE_KEY, mode);
|
||||
} catch {
|
||||
// Keep runtime state even when persistence fails.
|
||||
}
|
||||
}, []);
|
||||
|
||||
return {
|
||||
viewMode,
|
||||
changeViewMode,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user