Graduate file editing from experimental (#599)
This commit is contained in:
@@ -2,11 +2,19 @@ import React, { useState, useRef, useEffect } from "react";
|
||||
import Editor, { OnMount } from "@monaco-editor/react";
|
||||
import { useLoadAppFile } from "@/hooks/useLoadAppFile";
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
import { ChevronRight, Circle } from "lucide-react";
|
||||
import { ChevronRight, Circle, Save } from "lucide-react";
|
||||
import "@/components/chat/monaco";
|
||||
import { IpcClient } from "@/ipc/ipc_client";
|
||||
import { showError, showSuccess, showWarning } from "@/lib/toast";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useSettings } from "@/hooks/useSettings";
|
||||
import { showError } from "@/lib/toast";
|
||||
import { useCheckProblems } from "@/hooks/useCheckProblems";
|
||||
|
||||
interface FileEditorProps {
|
||||
appId: number | null;
|
||||
@@ -16,9 +24,16 @@ interface FileEditorProps {
|
||||
interface BreadcrumbProps {
|
||||
path: string;
|
||||
hasUnsavedChanges: boolean;
|
||||
onSave: () => void;
|
||||
isSaving: boolean;
|
||||
}
|
||||
|
||||
const Breadcrumb: React.FC<BreadcrumbProps> = ({ path, hasUnsavedChanges }) => {
|
||||
const Breadcrumb: React.FC<BreadcrumbProps> = ({
|
||||
path,
|
||||
hasUnsavedChanges,
|
||||
onSave,
|
||||
isSaving,
|
||||
}) => {
|
||||
const segments = path.split("/").filter(Boolean);
|
||||
|
||||
return (
|
||||
@@ -39,7 +54,24 @@ const Breadcrumb: React.FC<BreadcrumbProps> = ({ path, hasUnsavedChanges }) => {
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex-shrink-0 ml-2">
|
||||
<div className="flex items-center gap-2 flex-shrink-0 ml-2">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onSave}
|
||||
disabled={!hasUnsavedChanges || isSaving}
|
||||
className="h-6 w-6 p-0"
|
||||
data-testid="save-file-button"
|
||||
>
|
||||
<Save size={12} />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{hasUnsavedChanges ? "Save changes" : "No unsaved changes"}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
{hasUnsavedChanges && (
|
||||
<Circle
|
||||
size={8}
|
||||
@@ -56,10 +88,10 @@ const Breadcrumb: React.FC<BreadcrumbProps> = ({ path, hasUnsavedChanges }) => {
|
||||
export const FileEditor = ({ appId, filePath }: FileEditorProps) => {
|
||||
const { content, loading, error } = useLoadAppFile(appId, filePath);
|
||||
const { theme } = useTheme();
|
||||
const { settings } = useSettings();
|
||||
const [value, setValue] = useState<string | undefined>(undefined);
|
||||
const [displayUnsavedChanges, setDisplayUnsavedChanges] = useState(false);
|
||||
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const { settings } = useSettings();
|
||||
// Use refs for values that need to be current in event handlers
|
||||
const originalValueRef = useRef<string | undefined>(undefined);
|
||||
const editorRef = useRef<any>(null);
|
||||
@@ -67,6 +99,9 @@ export const FileEditor = ({ appId, filePath }: FileEditorProps) => {
|
||||
const needsSaveRef = useRef<boolean>(false);
|
||||
const currentValueRef = useRef<string | undefined>(undefined);
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const { checkProblems } = useCheckProblems(appId);
|
||||
|
||||
// Update state when content loads
|
||||
useEffect(() => {
|
||||
if (content !== null) {
|
||||
@@ -75,6 +110,7 @@ export const FileEditor = ({ appId, filePath }: FileEditorProps) => {
|
||||
currentValueRef.current = content;
|
||||
needsSaveRef.current = false;
|
||||
setDisplayUnsavedChanges(false);
|
||||
setIsSaving(false);
|
||||
}
|
||||
}, [content, filePath]);
|
||||
|
||||
@@ -125,9 +161,23 @@ export const FileEditor = ({ appId, filePath }: FileEditorProps) => {
|
||||
|
||||
try {
|
||||
isSavingRef.current = true;
|
||||
setIsSaving(true);
|
||||
|
||||
const ipcClient = IpcClient.getInstance();
|
||||
await ipcClient.editAppFile(appId, filePath, currentValueRef.current);
|
||||
const { warning } = await ipcClient.editAppFile(
|
||||
appId,
|
||||
filePath,
|
||||
currentValueRef.current,
|
||||
);
|
||||
await queryClient.invalidateQueries({ queryKey: ["versions", appId] });
|
||||
if (settings?.enableAutoFixProblems) {
|
||||
checkProblems();
|
||||
}
|
||||
if (warning) {
|
||||
showWarning(warning);
|
||||
} else {
|
||||
showSuccess("File saved");
|
||||
}
|
||||
|
||||
originalValueRef.current = currentValueRef.current;
|
||||
needsSaveRef.current = false;
|
||||
@@ -136,6 +186,7 @@ export const FileEditor = ({ appId, filePath }: FileEditorProps) => {
|
||||
showError(error);
|
||||
} finally {
|
||||
isSavingRef.current = false;
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -182,7 +233,12 @@ export const FileEditor = ({ appId, filePath }: FileEditorProps) => {
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
<Breadcrumb path={filePath} hasUnsavedChanges={displayUnsavedChanges} />
|
||||
<Breadcrumb
|
||||
path={filePath}
|
||||
hasUnsavedChanges={displayUnsavedChanges}
|
||||
onSave={saveFile}
|
||||
isSaving={isSaving}
|
||||
/>
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<Editor
|
||||
height="100%"
|
||||
@@ -199,7 +255,6 @@ export const FileEditor = ({ appId, filePath }: FileEditorProps) => {
|
||||
fontFamily: "monospace",
|
||||
fontSize: 13,
|
||||
lineNumbers: "on",
|
||||
readOnly: !settings?.experiments?.enableFileEditing,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
CreateAppParams,
|
||||
RenameBranchParams,
|
||||
CopyAppParams,
|
||||
EditAppFileReturnType,
|
||||
} from "../ipc_types";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
@@ -32,7 +33,10 @@ import fixPath from "fix-path";
|
||||
import killPort from "kill-port";
|
||||
import util from "util";
|
||||
import log from "electron-log";
|
||||
import { getSupabaseProjectName } from "../../supabase_admin/supabase_management_client";
|
||||
import {
|
||||
deploySupabaseFunctions,
|
||||
getSupabaseProjectName,
|
||||
} from "../../supabase_admin/supabase_management_client";
|
||||
import { createLoggedHandler } from "./safe_handle";
|
||||
import { getLanguageModelProviders } from "../shared/language_model_helpers";
|
||||
import { startProxy } from "../utils/start_proxy_server";
|
||||
@@ -41,6 +45,7 @@ import { createFromTemplate } from "./createFromTemplate";
|
||||
import { gitCommit } from "../utils/git_utils";
|
||||
import { safeSend } from "../utils/safe_sender";
|
||||
import { normalizePath } from "../../../shared/normalizePath";
|
||||
import { isServerFunction } from "@/supabase_admin/supabase_utils";
|
||||
|
||||
async function copyDir(
|
||||
source: string,
|
||||
@@ -604,7 +609,7 @@ export function registerAppHandlers() {
|
||||
filePath,
|
||||
content,
|
||||
}: { appId: number; filePath: string; content: string },
|
||||
): Promise<void> => {
|
||||
): Promise<EditAppFileReturnType> => {
|
||||
const app = await db.query.apps.findFirst({
|
||||
where: eq(apps.id, appId),
|
||||
});
|
||||
@@ -641,12 +646,26 @@ export function registerAppHandlers() {
|
||||
message: `Updated ${filePath}`,
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
} catch (error: any) {
|
||||
logger.error(`Error writing file ${filePath} for app ${appId}:`, error);
|
||||
throw new Error(`Failed to write file: ${error.message}`);
|
||||
}
|
||||
|
||||
if (isServerFunction(filePath) && app.supabaseProjectId) {
|
||||
try {
|
||||
await deploySupabaseFunctions({
|
||||
supabaseProjectId: app.supabaseProjectId,
|
||||
functionName: path.basename(path.dirname(filePath)),
|
||||
content: content,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(`Error deploying Supabase function ${filePath}:`, error);
|
||||
return {
|
||||
warning: `File saved, but failed to deploy Supabase function: ${filePath}: ${error}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
return {};
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ import type {
|
||||
ComponentSelection,
|
||||
AppUpgrade,
|
||||
ProblemReport,
|
||||
EditAppFileReturnType,
|
||||
} from "./ipc_types";
|
||||
import type { AppChatContext, ProposalResult } from "@/lib/schemas";
|
||||
import { showError } from "@/lib/toast";
|
||||
@@ -220,8 +221,8 @@ export class IpcClient {
|
||||
appId: number,
|
||||
filePath: string,
|
||||
content: string,
|
||||
): Promise<void> {
|
||||
await this.ipcRenderer.invoke("edit-app-file", {
|
||||
): Promise<EditAppFileReturnType> {
|
||||
return this.ipcRenderer.invoke("edit-app-file", {
|
||||
appId,
|
||||
filePath,
|
||||
content,
|
||||
|
||||
@@ -248,3 +248,7 @@ export interface AppUpgrade {
|
||||
manualUpgradeUrl: string;
|
||||
isNeeded: boolean;
|
||||
}
|
||||
|
||||
export interface EditAppFileReturnType {
|
||||
warning?: string;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,9 @@ import path from "node:path";
|
||||
import fsExtra from "fs-extra";
|
||||
import { generateCuteAppName } from "../../lib/utils";
|
||||
|
||||
// Directories to exclude when scanning files
|
||||
const EXCLUDED_DIRS = ["node_modules", ".git", ".next"];
|
||||
|
||||
/**
|
||||
* Recursively gets all files in a directory, excluding node_modules and .git
|
||||
* @param dir The directory to scan
|
||||
@@ -22,8 +25,8 @@ export function getFilesRecursively(dir: string, baseDir: string): string[] {
|
||||
const res = path.join(dir, dirent.name);
|
||||
if (dirent.isDirectory()) {
|
||||
// For directories, concat the results of recursive call
|
||||
// Exclude node_modules and .git directories
|
||||
if (dirent.name !== "node_modules" && dirent.name !== ".git") {
|
||||
// Exclude specified directories
|
||||
if (!EXCLUDED_DIRS.includes(dirent.name)) {
|
||||
files.push(...getFilesRecursively(res, baseDir));
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -93,7 +93,7 @@ export type Supabase = z.infer<typeof SupabaseSchema>;
|
||||
export const ExperimentsSchema = z.object({
|
||||
// Deprecated
|
||||
enableSupabaseIntegration: z.boolean().describe("DEPRECATED").optional(),
|
||||
enableFileEditing: z.boolean().optional(),
|
||||
enableFileEditing: z.boolean().describe("DEPRECATED").optional(),
|
||||
});
|
||||
export type Experiments = z.infer<typeof ExperimentsSchema>;
|
||||
|
||||
|
||||
@@ -150,30 +150,6 @@ export default function SettingsPage() {
|
||||
.
|
||||
</div>
|
||||
</div>
|
||||
{/* Enable File Editing Experiment */}
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
id="enable-file-editing"
|
||||
checked={!!settings?.experiments?.enableFileEditing}
|
||||
onCheckedChange={(checked) =>
|
||||
updateSettings({
|
||||
experiments: {
|
||||
...settings?.experiments,
|
||||
enableFileEditing: checked,
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Label htmlFor="enable-file-editing">
|
||||
Enable File Editing
|
||||
</Label>
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
File editing is not reliable and requires you to manually
|
||||
commit changes and update Supabase edge functions.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user