refactor: organize frontend components into features/ directories
Moved 43 component files into 9 feature domains: - features/git/ — commit-dialog, commit-panel, file-editor, git-mount-editor, git-toolbar, merge-dialog - features/project/ — repositories-settings-tab, repository-create-dialog - features/terminal/ — special-keys-panel, special-keys-strip, terminal-session-tabs, terminal - features/workspace/ — workspace-card, workspace-create-form, workspace-header, workspace-instance-chips - features/session/ — create-session-form, session-card, session-list - features/tool/ — instance-list, manifest-editor, start-tool-fab, start-tool-modal, tool-starter, tools-bottom-sheet - features/notification/ — event-toast-bridge, notification-center, notification-item - features/settings/ — settings-tab-layout - features/mobile/ — mobile-action-sheet, mobile-detail-view, mobile-edit-view, mobile-fab, mobile-list-view, mobile-nav, mobile-page-header, mobile-terminal-header, mobile-terminal-wrapper Updated all imports across pages and components. Root components/ now only contains generic UI pieces: app-shell, code-editor, data-states, icon, protected-route, syntax-highlighter. Quality gates: verified no remaining old imports.
This commit is contained in:
@@ -0,0 +1,241 @@
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { apiClient } from "../api/client";
|
||||
import { useAuth } from "../state/auth";
|
||||
import { CodeEditor } from "../components/code-editor";
|
||||
import { CommitDialog } from "../components/features/git/commit-dialog";
|
||||
import { Icon } from "../components/icon";
|
||||
import { SyntaxHighlighter } from "../components/syntax-highlighter";
|
||||
import { detectLanguage } from "../utils/language";
|
||||
|
||||
interface FileEditorProps {
|
||||
projectId: string;
|
||||
repoId: string;
|
||||
}
|
||||
|
||||
export const FileEditor: React.FC<FileEditorProps> = ({
|
||||
projectId,
|
||||
repoId,
|
||||
}) => {
|
||||
const [searchParams] = useSearchParams();
|
||||
const { user } = useAuth();
|
||||
|
||||
const [mode, setMode] = useState<"view" | "edit">("view");
|
||||
const [content, setContent] = useState(">");
|
||||
const [originalContent, setOriginalContent] = useState(">");
|
||||
const [language, setLanguage] = useState("plaintext");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [showCommitDialog, setShowCommitDialog] = useState(false);
|
||||
const [isBinary, setIsBinary] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
const branch = searchParams.get("branch") || "main";
|
||||
const filePath = searchParams.get("file");
|
||||
|
||||
const loadFile = useCallback(async () => {
|
||||
if (!filePath) {
|
||||
setContent("");
|
||||
setOriginalContent("");
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await apiClient.get(
|
||||
`/projects/${projectId}/repositories/${repoId}/files/content`,
|
||||
{
|
||||
params: {
|
||||
branch,
|
||||
path: filePath,
|
||||
},
|
||||
}
|
||||
);
|
||||
const data = response.data;
|
||||
if (data.is_binary) {
|
||||
setIsBinary(true);
|
||||
setContent("Binary file - cannot display");
|
||||
setOriginalContent("");
|
||||
} else {
|
||||
setIsBinary(false);
|
||||
setContent(data.content);
|
||||
setOriginalContent(data.content);
|
||||
setLanguage(detectLanguage(filePath));
|
||||
}
|
||||
} catch {
|
||||
setError("Failed to load file");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [projectId, repoId, branch, filePath]);
|
||||
|
||||
useEffect(() => {
|
||||
void loadFile();
|
||||
}, [loadFile]);
|
||||
|
||||
const handleEdit = () => {
|
||||
if (isBinary) return;
|
||||
setMode("edit");
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setContent(originalContent);
|
||||
setMode("view");
|
||||
setShowCommitDialog(false);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
if (content === originalContent) {
|
||||
setMode("view");
|
||||
return;
|
||||
}
|
||||
setShowCommitDialog(true);
|
||||
};
|
||||
|
||||
const handleCommit = async (message: string) => {
|
||||
if (!filePath || !user) return;
|
||||
|
||||
setSaving(true);
|
||||
try {
|
||||
await apiClient.post(
|
||||
`/projects/${projectId}/repositories/${repoId}/files/content`,
|
||||
{
|
||||
path: filePath,
|
||||
branch,
|
||||
content,
|
||||
commit_message: message,
|
||||
author_name: user.name || "User",
|
||||
author_email: user.email || "user@example.com",
|
||||
}
|
||||
);
|
||||
setOriginalContent(content);
|
||||
setMode("view");
|
||||
setShowCommitDialog(false);
|
||||
} catch {
|
||||
setError("Failed to save changes");
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Keyboard shortcuts
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === "e") {
|
||||
e.preventDefault();
|
||||
if (mode === "view" && !isBinary) {
|
||||
handleEdit();
|
||||
} else if (mode === "edit") {
|
||||
handleCancel();
|
||||
}
|
||||
}
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === "s") {
|
||||
e.preventDefault();
|
||||
if (mode === "edit") {
|
||||
handleSave();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, [mode, isBinary, content, originalContent]);
|
||||
|
||||
if (!filePath) {
|
||||
return (
|
||||
<div className="file-viewer-empty">
|
||||
<p className="muted">Select a file to view its contents</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (loading) return <p className="muted">Loading file...</p>;
|
||||
if (error) return <p className="error-text">{error}</p>;
|
||||
|
||||
return (
|
||||
<div className="file-editor">
|
||||
<div className="file-editor-toolbar">
|
||||
<div className="file-breadcrumbs">
|
||||
{filePath.split("/").map((part, i, arr) => (
|
||||
<span key={i}>
|
||||
{part}
|
||||
{i < arr.length - 1 && (
|
||||
<span className="breadcrumb-sep">/</span>
|
||||
)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="file-actions">
|
||||
{mode === "view" && !isBinary && (
|
||||
<button
|
||||
className="btn-primary"
|
||||
onClick={handleEdit}
|
||||
type="button"
|
||||
>
|
||||
<Icon name="edit" size="sm" />
|
||||
Edit
|
||||
</button>
|
||||
)}
|
||||
{mode === "edit" && (
|
||||
<>
|
||||
<button
|
||||
className="btn-primary"
|
||||
onClick={handleSave}
|
||||
disabled={content === originalContent || saving}
|
||||
type="button"
|
||||
>
|
||||
{saving ? (
|
||||
<>
|
||||
<Icon name="loading" size="sm" />
|
||||
Saving...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Icon name="save" size="sm" />
|
||||
Save
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
className="btn-secondary"
|
||||
onClick={handleCancel}
|
||||
type="button"
|
||||
>
|
||||
<Icon name="cancel" size="sm" />
|
||||
Cancel
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="file-editor-content">
|
||||
{mode === "view" && (
|
||||
<SyntaxHighlighter
|
||||
code={content}
|
||||
language={language}
|
||||
showLineNumbers={!isBinary}
|
||||
/>
|
||||
)}
|
||||
|
||||
{mode === "edit" && (
|
||||
<CodeEditor
|
||||
value={content}
|
||||
onChange={setContent}
|
||||
language={language}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<CommitDialog
|
||||
isOpen={showCommitDialog}
|
||||
filePath={filePath}
|
||||
originalContent={originalContent}
|
||||
newContent={content}
|
||||
onCommit={handleCommit}
|
||||
onCancel={() => setShowCommitDialog(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user