mirror of
https://github.com/OpenHands/OpenHands.git
synced 2026-03-22 05:37:20 +08:00
[ALL-557] feat(frontend): Add save and discard actions to the editor (#4442)
Co-authored-by: mamoodi <mamoodiha@gmail.com>
This commit is contained in:
62
frontend/src/components/editor-actions.tsx
Normal file
62
frontend/src/components/editor-actions.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import { cn } from "@nextui-org/react";
|
||||
import { HTMLAttributes } from "react";
|
||||
|
||||
interface EditorActionButtonProps {
|
||||
onClick: () => void;
|
||||
disabled: boolean;
|
||||
className: HTMLAttributes<HTMLButtonElement>["className"];
|
||||
}
|
||||
|
||||
function EditorActionButton({
|
||||
onClick,
|
||||
disabled,
|
||||
className,
|
||||
children,
|
||||
}: React.PropsWithChildren<EditorActionButtonProps>) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
"text-sm py-0.5 rounded w-20",
|
||||
"hover:bg-neutral-700 disabled:opacity-50 disabled:cursor-not-allowed",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
interface EditorActionsProps {
|
||||
onSave: () => void;
|
||||
onDiscard: () => void;
|
||||
isDisabled: boolean;
|
||||
}
|
||||
|
||||
export function EditorActions({
|
||||
onSave,
|
||||
onDiscard,
|
||||
isDisabled,
|
||||
}: EditorActionsProps) {
|
||||
return (
|
||||
<div className="flex gap-2">
|
||||
<EditorActionButton
|
||||
onClick={onSave}
|
||||
disabled={isDisabled}
|
||||
className="bg-neutral-800 disabled:hover:bg-neutral-800"
|
||||
>
|
||||
Save
|
||||
</EditorActionButton>
|
||||
|
||||
<EditorActionButton
|
||||
onClick={onDiscard}
|
||||
disabled={isDisabled}
|
||||
className="border border-neutral-800 disabled:hover:bg-transparent"
|
||||
>
|
||||
Discard
|
||||
</EditorActionButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -27,6 +27,7 @@ interface FilesContextType {
|
||||
modifiedFiles: Record<string, string>;
|
||||
modifyFileContent: (path: string, content: string) => void;
|
||||
saveFileContent: (path: string) => string | undefined;
|
||||
discardChanges: (path: string) => void;
|
||||
}
|
||||
|
||||
const FilesContext = React.createContext<FilesContextType | undefined>(
|
||||
@@ -62,19 +63,25 @@ function FilesProvider({ children }: FilesProviderProps) {
|
||||
[files, modifiedFiles],
|
||||
);
|
||||
|
||||
const discardChanges = React.useCallback((path: string) => {
|
||||
setModifiedFiles((prev) => {
|
||||
const newModifiedFiles = { ...prev };
|
||||
delete newModifiedFiles[path];
|
||||
return newModifiedFiles;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const saveFileContent = React.useCallback(
|
||||
(path: string): string | undefined => {
|
||||
const content = modifiedFiles[path];
|
||||
if (content) {
|
||||
setFiles((prev) => ({ ...prev, [path]: content }));
|
||||
const newModifiedFiles = { ...modifiedFiles };
|
||||
delete newModifiedFiles[path];
|
||||
setModifiedFiles(newModifiedFiles);
|
||||
discardChanges(path);
|
||||
}
|
||||
|
||||
return content;
|
||||
},
|
||||
[files, modifiedFiles, selectedPath],
|
||||
[files, modifiedFiles, selectedPath, discardChanges],
|
||||
);
|
||||
|
||||
const value = React.useMemo(
|
||||
@@ -88,6 +95,7 @@ function FilesProvider({ children }: FilesProviderProps) {
|
||||
modifiedFiles,
|
||||
modifyFileContent,
|
||||
saveFileContent,
|
||||
discardChanges,
|
||||
}),
|
||||
[
|
||||
paths,
|
||||
@@ -99,6 +107,7 @@ function FilesProvider({ children }: FilesProviderProps) {
|
||||
modifiedFiles,
|
||||
modifyFileContent,
|
||||
saveFileContent,
|
||||
discardChanges,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import OpenHands from "#/api/open-hands";
|
||||
import { useSocket } from "#/context/socket";
|
||||
import CodeEditorCompoonent from "./code-editor-component";
|
||||
import { useFiles } from "#/context/files";
|
||||
import { EditorActions } from "#/components/editor-actions";
|
||||
|
||||
export const clientLoader = async () => {
|
||||
const token = localStorage.getItem("token");
|
||||
@@ -48,7 +49,13 @@ export function ErrorBoundary() {
|
||||
function CodeEditor() {
|
||||
const { token } = useLoaderData<typeof clientLoader>();
|
||||
const { runtimeActive } = useSocket();
|
||||
const { setPaths } = useFiles();
|
||||
const {
|
||||
setPaths,
|
||||
selectedPath,
|
||||
modifiedFiles,
|
||||
saveFileContent: saveNewFileContent,
|
||||
discardChanges,
|
||||
} = useFiles();
|
||||
|
||||
const agentState = useSelector(
|
||||
(state: RootState) => state.agent.curAgentState,
|
||||
@@ -68,10 +75,38 @@ function CodeEditor() {
|
||||
[agentState],
|
||||
);
|
||||
|
||||
const handleSave = async () => {
|
||||
if (selectedPath) {
|
||||
const content = saveNewFileContent(selectedPath);
|
||||
|
||||
if (content && token) {
|
||||
try {
|
||||
await OpenHands.saveFile(token, selectedPath, content);
|
||||
} catch (error) {
|
||||
// handle error
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleDiscard = () => {
|
||||
if (selectedPath) discardChanges(selectedPath);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full bg-neutral-900 relative">
|
||||
<FileExplorer />
|
||||
<div className="flex flex-col min-h-0 w-full pt-3">
|
||||
<div className="flex flex-col min-h-0 w-full">
|
||||
{selectedPath && (
|
||||
<div className="flex w-full items-center justify-between self-end p-2">
|
||||
<span className="text-sm text-neutral-500">{selectedPath}</span>
|
||||
<EditorActions
|
||||
onSave={handleSave}
|
||||
onDiscard={handleDiscard}
|
||||
isDisabled={!isEditingAllowed || !modifiedFiles[selectedPath]}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex grow items-center justify-center">
|
||||
<CodeEditorCompoonent isReadOnly={!isEditingAllowed} />
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user