Skip to content

Commit c98548f

Browse files
authored
Merge pull request #3574 from ivanpadavan/patches-impl
feat: Add patches functionality
2 parents b0c6b13 + 0d4d609 commit c98548f

File tree

31 files changed

+11653
-1267
lines changed

31 files changed

+11653
-1267
lines changed

apps/dokploy/__test__/deploy/application.command.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ vi.mock("@dokploy/server/db", () => {
2828
applications: {
2929
findFirst: vi.fn(),
3030
},
31+
patch: {
32+
findMany: vi.fn().mockResolvedValue([]),
33+
},
3134
},
3235
},
3336
};

apps/dokploy/__test__/deploy/application.real.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ vi.mock("@dokploy/server/db", () => {
2929
applications: {
3030
findFirst: vi.fn(),
3131
},
32+
patch: {
33+
findMany: vi.fn().mockResolvedValue([]),
34+
},
3235
},
3336
},
3437
};
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { FilePlus } from "lucide-react";
2+
import { useState } from "react";
3+
import { CodeEditor } from "@/components/shared/code-editor";
4+
import { Button } from "@/components/ui/button";
5+
import {
6+
Dialog,
7+
DialogClose,
8+
DialogContent,
9+
DialogDescription,
10+
DialogFooter,
11+
DialogHeader,
12+
DialogTitle,
13+
DialogTrigger,
14+
} from "@/components/ui/dialog";
15+
import { Input } from "@/components/ui/input";
16+
import { Label } from "@/components/ui/label";
17+
18+
interface Props {
19+
folderPath: string;
20+
onCreate: (filename: string, content: string) => void;
21+
onOpenChange: (open: boolean) => void;
22+
alwaysVisible?: boolean;
23+
}
24+
25+
export const CreateFileDialog = ({
26+
folderPath,
27+
onCreate,
28+
onOpenChange,
29+
alwaysVisible = false,
30+
}: Props) => {
31+
const [filename, setFilename] = useState("");
32+
const [content, setContent] = useState("");
33+
34+
const handleCreate = () => {
35+
if (!filename.trim()) return;
36+
onCreate(filename.trim(), content);
37+
setFilename("");
38+
setContent("");
39+
onOpenChange(false);
40+
};
41+
42+
return (
43+
<Dialog>
44+
<DialogTrigger asChild>
45+
<Button
46+
variant="ghost"
47+
size="icon"
48+
type="button"
49+
className={`h-6 w-6 ${alwaysVisible ? "" : "opacity-0 group-hover:opacity-100"}`}
50+
title="Create file"
51+
>
52+
<FilePlus className="h-3 w-3" />
53+
</Button>
54+
</DialogTrigger>
55+
<DialogContent className="sm:max-w-2xl">
56+
<form
57+
onSubmit={(e) => {
58+
e.preventDefault();
59+
handleCreate();
60+
}}
61+
>
62+
<DialogHeader>
63+
<DialogTitle>Create file</DialogTitle>
64+
<DialogDescription>
65+
{folderPath ? `New file in ${folderPath}/` : "New file in root"}
66+
</DialogDescription>
67+
</DialogHeader>
68+
<div className="space-y-4 py-4">
69+
<div className="space-y-2">
70+
<Label htmlFor="filename">Filename</Label>
71+
<Input
72+
id="filename"
73+
placeholder="e.g. .env.example"
74+
value={filename}
75+
onChange={(e) => setFilename(e.target.value)}
76+
/>
77+
</div>
78+
<div className="space-y-2">
79+
<Label>Content</Label>
80+
<div className="h-[200px] rounded-md border">
81+
<CodeEditor
82+
value={content}
83+
onChange={(v) => setContent(v ?? "")}
84+
className="h-full"
85+
wrapperClassName="h-[200px]"
86+
lineWrapping
87+
/>
88+
</div>
89+
</div>
90+
</div>
91+
<DialogFooter>
92+
<DialogClose asChild>
93+
<Button variant="outline" type="button">
94+
Cancel
95+
</Button>
96+
</DialogClose>
97+
<DialogClose asChild>
98+
<Button type="submit" disabled={!filename.trim()}>
99+
Create
100+
</Button>
101+
</DialogClose>
102+
</DialogFooter>
103+
</form>
104+
</DialogContent>
105+
</Dialog>
106+
);
107+
};
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { Loader2, Pencil } from "lucide-react";
2+
import { useEffect, useState } from "react";
3+
import { toast } from "sonner";
4+
import { CodeEditor } from "@/components/shared/code-editor";
5+
import { Button } from "@/components/ui/button";
6+
import {
7+
Dialog,
8+
DialogClose,
9+
DialogContent,
10+
DialogDescription,
11+
DialogFooter,
12+
DialogHeader,
13+
DialogTitle,
14+
DialogTrigger,
15+
} from "@/components/ui/dialog";
16+
import { api } from "@/utils/api";
17+
18+
interface Props {
19+
patchId: string;
20+
entityId: string;
21+
type: "application" | "compose";
22+
onSuccess?: () => void;
23+
}
24+
25+
export const EditPatchDialog = ({
26+
patchId,
27+
entityId,
28+
type,
29+
onSuccess,
30+
}: Props) => {
31+
const { data: patch, isLoading: isPatchLoading } = api.patch.one.useQuery(
32+
{ patchId },
33+
{ enabled: !!patchId },
34+
);
35+
const [content, setContent] = useState("");
36+
37+
useEffect(() => {
38+
if (patch) {
39+
setContent(patch.content);
40+
}
41+
}, [patch]);
42+
43+
const utils = api.useUtils();
44+
const updatePatch = api.patch.update.useMutation();
45+
46+
const handleSave = () => {
47+
updatePatch
48+
.mutateAsync({ patchId, content })
49+
.then(() => {
50+
toast.success("Patch saved");
51+
utils.patch.byEntityId.invalidate({ id: entityId, type });
52+
onSuccess?.();
53+
})
54+
.catch((err) => {
55+
toast.error(err.message);
56+
});
57+
};
58+
59+
return (
60+
<Dialog>
61+
<DialogTrigger asChild>
62+
<Button variant="ghost" size="icon" title="Edit patch">
63+
<Pencil className="h-4 w-4" />
64+
</Button>
65+
</DialogTrigger>
66+
<DialogContent className="sm:max-w-4xl max-h-[85vh] flex flex-col p-0">
67+
<DialogHeader className="px-6 pt-6 pb-4">
68+
<DialogTitle>Edit Patch</DialogTitle>
69+
<DialogDescription>
70+
{patch ? `Editing: ${patch.filePath}` : "Loading patch..."}
71+
</DialogDescription>
72+
</DialogHeader>
73+
{isPatchLoading ? (
74+
<div className="flex flex-1 items-center justify-center px-6 py-12">
75+
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
76+
</div>
77+
) : (
78+
<div className="flex-1 min-h-0 px-6 overflow-hidden flex flex-col">
79+
<CodeEditor
80+
value={content}
81+
onChange={(value) => setContent(value ?? "")}
82+
className="h-[400px] w-full"
83+
wrapperClassName="h-[400px]"
84+
lineWrapping
85+
/>
86+
</div>
87+
)}
88+
<DialogFooter className="px-6 ">
89+
<DialogClose asChild>
90+
<Button variant="outline">Cancel</Button>
91+
</DialogClose>
92+
<Button onClick={handleSave} isLoading={updatePatch.isLoading}>
93+
{updatePatch.isPending && (
94+
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
95+
)}
96+
Save
97+
</Button>
98+
</DialogFooter>
99+
</DialogContent>
100+
</Dialog>
101+
);
102+
};
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from "./show-patches";
2+
export * from "./patch-editor";

0 commit comments

Comments
 (0)