mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-03-11 00:47:52 +00:00
214 lines
7.3 KiB
TypeScript
214 lines
7.3 KiB
TypeScript
import { GitBranch, GitCommit, RefreshCw } from 'lucide-react';
|
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
import type { ConfirmationRequest, FileStatusCode, GitDiffMap, GitStatusResponse } from '../../types/types';
|
|
import { getAllChangedFiles, hasChangedFiles } from '../../utils/gitPanelUtils';
|
|
import CommitComposer from './CommitComposer';
|
|
import FileChangeList from './FileChangeList';
|
|
import FileSelectionControls from './FileSelectionControls';
|
|
import FileStatusLegend from './FileStatusLegend';
|
|
|
|
type ChangesViewProps = {
|
|
isMobile: boolean;
|
|
gitStatus: GitStatusResponse | null;
|
|
gitDiff: GitDiffMap;
|
|
isLoading: boolean;
|
|
wrapText: boolean;
|
|
isCreatingInitialCommit: boolean;
|
|
onWrapTextChange: (wrapText: boolean) => void;
|
|
onCreateInitialCommit: () => Promise<boolean>;
|
|
onOpenFile: (filePath: string) => Promise<void>;
|
|
onDiscardFile: (filePath: string) => Promise<void>;
|
|
onDeleteFile: (filePath: string) => Promise<void>;
|
|
onCommitChanges: (message: string, files: string[]) => Promise<boolean>;
|
|
onGenerateCommitMessage: (files: string[]) => Promise<string | null>;
|
|
onRequestConfirmation: (request: ConfirmationRequest) => void;
|
|
onExpandedFilesChange: (hasExpandedFiles: boolean) => void;
|
|
};
|
|
|
|
export default function ChangesView({
|
|
isMobile,
|
|
gitStatus,
|
|
gitDiff,
|
|
isLoading,
|
|
wrapText,
|
|
isCreatingInitialCommit,
|
|
onWrapTextChange,
|
|
onCreateInitialCommit,
|
|
onOpenFile,
|
|
onDiscardFile,
|
|
onDeleteFile,
|
|
onCommitChanges,
|
|
onGenerateCommitMessage,
|
|
onRequestConfirmation,
|
|
onExpandedFilesChange,
|
|
}: ChangesViewProps) {
|
|
const [expandedFiles, setExpandedFiles] = useState<Set<string>>(new Set());
|
|
const [selectedFiles, setSelectedFiles] = useState<Set<string>>(new Set());
|
|
|
|
const changedFiles = useMemo(() => getAllChangedFiles(gitStatus), [gitStatus]);
|
|
const hasExpandedFiles = expandedFiles.size > 0;
|
|
|
|
useEffect(() => {
|
|
if (!gitStatus || gitStatus.error) {
|
|
setSelectedFiles(new Set());
|
|
return;
|
|
}
|
|
|
|
// Preserve previous behavior: every fresh status snapshot reselects changed files.
|
|
setSelectedFiles(new Set(getAllChangedFiles(gitStatus)));
|
|
}, [gitStatus]);
|
|
|
|
useEffect(() => {
|
|
onExpandedFilesChange(hasExpandedFiles);
|
|
}, [hasExpandedFiles, onExpandedFilesChange]);
|
|
|
|
useEffect(() => {
|
|
return () => {
|
|
onExpandedFilesChange(false);
|
|
};
|
|
}, [onExpandedFilesChange]);
|
|
|
|
const toggleFileExpanded = useCallback((filePath: string) => {
|
|
setExpandedFiles((previous) => {
|
|
const next = new Set(previous);
|
|
if (next.has(filePath)) {
|
|
next.delete(filePath);
|
|
} else {
|
|
next.add(filePath);
|
|
}
|
|
return next;
|
|
});
|
|
}, []);
|
|
|
|
const toggleFileSelected = useCallback((filePath: string) => {
|
|
setSelectedFiles((previous) => {
|
|
const next = new Set(previous);
|
|
if (next.has(filePath)) {
|
|
next.delete(filePath);
|
|
} else {
|
|
next.add(filePath);
|
|
}
|
|
return next;
|
|
});
|
|
}, []);
|
|
|
|
const requestFileAction = useCallback(
|
|
(filePath: string, status: FileStatusCode) => {
|
|
if (status === 'U') {
|
|
onRequestConfirmation({
|
|
type: 'delete',
|
|
message: `Delete untracked file "${filePath}"? This action cannot be undone.`,
|
|
onConfirm: async () => {
|
|
await onDeleteFile(filePath);
|
|
},
|
|
});
|
|
return;
|
|
}
|
|
|
|
onRequestConfirmation({
|
|
type: 'discard',
|
|
message: `Discard all changes to "${filePath}"? This action cannot be undone.`,
|
|
onConfirm: async () => {
|
|
await onDiscardFile(filePath);
|
|
},
|
|
});
|
|
},
|
|
[onDeleteFile, onDiscardFile, onRequestConfirmation],
|
|
);
|
|
|
|
const commitSelectedFiles = useCallback(
|
|
(message: string) => {
|
|
return onCommitChanges(message, Array.from(selectedFiles));
|
|
},
|
|
[onCommitChanges, selectedFiles],
|
|
);
|
|
|
|
const generateMessageForSelection = useCallback(() => {
|
|
return onGenerateCommitMessage(Array.from(selectedFiles));
|
|
}, [onGenerateCommitMessage, selectedFiles]);
|
|
|
|
return (
|
|
<>
|
|
<CommitComposer
|
|
isMobile={isMobile}
|
|
selectedFileCount={selectedFiles.size}
|
|
isHidden={hasExpandedFiles}
|
|
onCommit={commitSelectedFiles}
|
|
onGenerateMessage={generateMessageForSelection}
|
|
onRequestConfirmation={onRequestConfirmation}
|
|
/>
|
|
|
|
{gitStatus && !gitStatus.error && (
|
|
<FileSelectionControls
|
|
isMobile={isMobile}
|
|
selectedCount={selectedFiles.size}
|
|
totalCount={changedFiles.length}
|
|
isHidden={hasExpandedFiles}
|
|
onSelectAll={() => setSelectedFiles(new Set(changedFiles))}
|
|
onDeselectAll={() => setSelectedFiles(new Set())}
|
|
/>
|
|
)}
|
|
|
|
{!gitStatus?.error && <FileStatusLegend isMobile={isMobile} />}
|
|
|
|
<div className={`flex-1 overflow-y-auto ${isMobile ? 'pb-mobile-nav' : ''}`}>
|
|
{isLoading ? (
|
|
<div className="flex items-center justify-center h-32">
|
|
<RefreshCw className="w-5 h-5 animate-spin text-muted-foreground" />
|
|
</div>
|
|
) : gitStatus?.hasCommits === false ? (
|
|
<div className="flex flex-col items-center justify-center p-8 text-center">
|
|
<div className="w-14 h-14 rounded-2xl bg-muted/50 flex items-center justify-center mb-4">
|
|
<GitBranch className="w-7 h-7 text-muted-foreground/50" />
|
|
</div>
|
|
<h3 className="text-lg font-medium mb-2 text-foreground">No commits yet</h3>
|
|
<p className="text-sm text-muted-foreground mb-6 max-w-md">
|
|
This repository doesn't have any commits yet. Create your first commit to start tracking changes.
|
|
</p>
|
|
<button
|
|
onClick={() => void onCreateInitialCommit()}
|
|
disabled={isCreatingInitialCommit}
|
|
className="px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2 transition-colors"
|
|
>
|
|
{isCreatingInitialCommit ? (
|
|
<>
|
|
<RefreshCw className="w-4 h-4 animate-spin" />
|
|
<span>Creating Initial Commit...</span>
|
|
</>
|
|
) : (
|
|
<>
|
|
<GitCommit className="w-4 h-4" />
|
|
<span>Create Initial Commit</span>
|
|
</>
|
|
)}
|
|
</button>
|
|
</div>
|
|
) : !gitStatus || !hasChangedFiles(gitStatus) ? (
|
|
<div className="flex flex-col items-center justify-center h-32 text-muted-foreground">
|
|
<GitCommit className="w-10 h-10 mb-2 opacity-40" />
|
|
<p className="text-sm">No changes detected</p>
|
|
</div>
|
|
) : (
|
|
<div className={isMobile ? 'pb-4' : ''}>
|
|
<FileChangeList
|
|
gitStatus={gitStatus}
|
|
gitDiff={gitDiff}
|
|
expandedFiles={expandedFiles}
|
|
selectedFiles={selectedFiles}
|
|
isMobile={isMobile}
|
|
wrapText={wrapText}
|
|
onToggleSelected={toggleFileSelected}
|
|
onToggleExpanded={toggleFileExpanded}
|
|
onOpenFile={(filePath) => {
|
|
void onOpenFile(filePath);
|
|
}}
|
|
onToggleWrapText={() => onWrapTextChange(!wrapText)}
|
|
onRequestFileAction={requestFileAction}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</>
|
|
);
|
|
}
|