Explorar el Código

[ALL-557] feat(frontend): Add save and discard actions to the editor (#4442)

Co-authored-by: mamoodi <mamoodiha@gmail.com>
sp.wack hace 1 año
padre
commit
6cb174b7d1

+ 62 - 0
frontend/src/components/editor-actions.tsx

@@ -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>
+  );
+}

+ 13 - 4
frontend/src/context/files.tsx

@@ -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,
     ],
   );
 

+ 37 - 2
frontend/src/routes/_oh.app._index/route.tsx

@@ -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>