refactor(patch-editor): streamline patch saving and loading logic

- Removed the local state for saving status and integrated loading state directly from the mutation hook.
- Updated the saveAsPatch function to handle success and error notifications more cleanly.
- Adjusted the file content loading to ensure it retrieves the latest data, improving user experience and consistency in the PatchEditor component.
- Refactored API calls in the patch router to unify patch retrieval logic for applications and composes, enhancing maintainability.
This commit is contained in:
Mauricio Siu
2026-02-17 00:41:46 -06:00
parent 88f387dd83
commit 20320639ce
3 changed files with 36 additions and 76 deletions

View File

@@ -41,7 +41,6 @@ export const PatchEditor = ({ id, type, repoPath, onClose }: Props) => {
const [expandedFolders, setExpandedFolders] = useState<Set<string>>(
new Set(),
);
const [isSaving, setIsSaving] = useState(false);
const { data: directories, isLoading: isDirLoading } =
api.patch.readRepoDirectories.useQuery(
@@ -49,27 +48,13 @@ export const PatchEditor = ({ id, type, repoPath, onClose }: Props) => {
{ enabled: !!repoPath },
);
const saveAsPatch = api.patch.saveFileAsPatch.useMutation({
onSuccess: (result) => {
setIsSaving(false);
if (result.deleted) {
toast.success("No changes - patch removed");
} else {
toast.success("Patch saved");
}
setOriginalContent(fileContent);
},
onError: () => {
setIsSaving(false);
toast.error("Failed to save patch");
},
});
const { mutateAsync: saveAsPatch, isLoading: isSavingPatch } =
api.patch.saveFileAsPatch.useMutation();
// Read file content when selected
const { data: fileData, isFetching: isFileLoading } =
api.patch.readRepoFile.useQuery(
{
id: id || "",
id,
type,
repoPath,
filePath: selectedFile || "",
@@ -77,8 +62,6 @@ export const PatchEditor = ({ id, type, repoPath, onClose }: Props) => {
{
enabled: !!selectedFile,
onSuccess: (data) => {
setFileContent(data.content);
setOriginalContent(data.content);
if (data.patchError) {
toast.error(data.patchErrorMessage || "Failed to apply patch");
}
@@ -104,14 +87,19 @@ export const PatchEditor = ({ id, type, repoPath, onClose }: Props) => {
const handleSave = () => {
if (!selectedFile) return;
setIsSaving(true);
saveAsPatch.mutate({
saveAsPatch({
id,
type,
repoPath,
filePath: selectedFile,
content: fileContent,
});
})
.then(() => {
toast.success("Patch saved");
})
.catch(() => {
toast.error("Failed to save patch");
});
};
const hasChanges = fileContent !== originalContent;
@@ -192,8 +180,8 @@ export const PatchEditor = ({ id, type, repoPath, onClose }: Props) => {
</div>
</div>
{selectedFile && (
<Button onClick={handleSave} disabled={isSaving || !hasChanges}>
{isSaving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
<Button onClick={handleSave} disabled={isSavingPatch || !hasChanges}>
{isSavingPatch && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
<Save className="mr-2 h-4 w-4" />
Save Patch
</Button>
@@ -201,7 +189,6 @@ export const PatchEditor = ({ id, type, repoPath, onClose }: Props) => {
</CardHeader>
<CardContent className="p-0">
<div className="grid grid-cols-[250px_1fr] border-t h-[600px]">
{/* File Tree */}
<div className="border-r h-full overflow-hidden">
<ScrollArea className="h-full">
<div className="p-2">
@@ -219,7 +206,6 @@ export const PatchEditor = ({ id, type, repoPath, onClose }: Props) => {
</div>
</ScrollArea>
</div>
{/* Editor */}
<div className="h-full overflow-hidden relative">
{isFileLoading ? (
<div className="flex items-center justify-center h-full">
@@ -227,7 +213,7 @@ export const PatchEditor = ({ id, type, repoPath, onClose }: Props) => {
</div>
) : selectedFile ? (
<CodeEditor
value={fileContent}
value={fileData?.content || ""}
onChange={(value) => setFileContent(value || "")}
className="h-full w-full"
wrapperClassName="h-full"

View File

@@ -51,8 +51,8 @@ const getApplicationGitConfig = (
};
case "gitea":
return {
gitUrl: app.gitea?.gitUrl
? `${app.gitea.gitUrl}/${app.giteaOwner}/${app.giteaRepository}.git`
gitUrl: app.gitea?.giteaUrl
? `${app.gitea.giteaUrl}/${app.giteaOwner}/${app.giteaRepository}.git`
: "",
gitBranch: app.giteaBranch || "main",
sshKeyId: null,
@@ -93,8 +93,8 @@ const getComposeGitConfig = (
};
case "gitea":
return {
gitUrl: compose.gitea?.gitUrl
? `${compose.gitea.gitUrl}/${compose.giteaOwner}/${compose.giteaRepository}.git`
gitUrl: compose.gitea?.giteaUrl
? `${compose.gitea.giteaUrl}/${compose.giteaOwner}/${compose.giteaRepository}.git`
: "",
gitBranch: compose.giteaBranch || "main",
sshKeyId: null,
@@ -343,7 +343,6 @@ export const patchRouter = createTRPCRouter({
)
.query(async ({ input, ctx }) => {
let serverId: string | null = null;
let patchContent: string | undefined;
if (input.type === "application") {
const app = await findApplicationById(input.id);
@@ -357,16 +356,6 @@ export const patchRouter = createTRPCRouter({
});
}
serverId = app.serverId;
// Check if patch exists for this file
const existingPatch = await findPatchByFilePath(
input.filePath,
input.id,
undefined,
);
if (existingPatch?.enabled) {
patchContent = existingPatch.content;
}
} else if (input.type === "compose") {
const compose = await findComposeById(input.id);
if (
@@ -379,27 +368,22 @@ export const patchRouter = createTRPCRouter({
});
}
serverId = compose.serverId;
// Check if patch exists for this file
const existingPatch = await findPatchByFilePath(
input.filePath,
undefined,
input.id,
);
if (existingPatch?.enabled) {
patchContent = existingPatch.content;
}
} else {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Either applicationId or composeId must be provided",
});
}
const existingPatch = await findPatchByFilePath(
input.filePath,
input.id,
input.type,
);
return await readPatchRepoFile(
input.repoPath,
input.filePath,
patchContent,
existingPatch?.enabled ? existingPatch?.content : undefined,
serverId,
);
}),
@@ -461,7 +445,7 @@ export const patchRouter = createTRPCRouter({
const existingPatch = await findPatchByFilePath(
input.filePath,
input.id,
input.id,
input.type,
);
if (existingPatch) {
await deletePatch(existingPatch.patchId);
@@ -473,22 +457,20 @@ export const patchRouter = createTRPCRouter({
const existingPatch = await findPatchByFilePath(
input.filePath,
input.id,
input.id,
input.type,
);
if (existingPatch) {
// Update existing patch
await updatePatch(existingPatch.patchId, { content: patchContent });
return { deleted: false, patchId: existingPatch.patchId };
}
// Create new patch
const newPatch = await createPatch({
filePath: input.filePath,
content: patchContent,
enabled: true,
applicationId: input.id,
composeId: input.id,
applicationId: input.type === "application" ? input.id : undefined,
composeId: input.type === "compose" ? input.id : undefined,
});
return { deleted: false, patchId: newPatch.patchId };

View File

@@ -76,23 +76,15 @@ export const findPatchesByComposeId = async (composeId: string) => {
export const findPatchByFilePath = async (
filePath: string,
applicationId?: string,
composeId?: string,
id: string,
type: "application" | "compose",
) => {
if (applicationId) {
return await db.query.patch.findFirst({
where: and(
eq(patch.filePath, filePath),
eq(patch.applicationId, applicationId),
),
});
}
if (composeId) {
return await db.query.patch.findFirst({
where: and(eq(patch.filePath, filePath), eq(patch.composeId, composeId)),
});
}
return null;
return await db.query.patch.findFirst({
where:
type === "application"
? and(eq(patch.filePath, filePath), eq(patch.applicationId, id))
: and(eq(patch.filePath, filePath), eq(patch.composeId, id)),
});
};
export const updatePatch = async (patchId: string, data: Partial<Patch>) => {