open, interoperable sandbox platform for agents and humans 📦 ✨
pocketenv.io
claude-code
atproto
sandbox
openclaw
agent
1import { useEffect, useState } from "react";
2import { createPortal } from "react-dom";
3import { useForm } from "react-hook-form";
4import { z } from "zod";
5import { zodResolver } from "@hookform/resolvers/zod";
6import {
7 useAddVolumeMutation,
8 useUpdateVolumeMutation,
9 useVolumeQuery,
10} from "../../../hooks/useVolume";
11import { useNotyf } from "../../../hooks/useNotyf";
12
13const schema = z.object({
14 name: z.string().min(1, "Name is required"),
15 path: z.string().min(1, "Path is required"),
16});
17
18type FormValues = z.infer<typeof schema>;
19
20export type AddVolumeModalProps = {
21 isOpen: boolean;
22 onClose: () => void;
23 sandboxId: string;
24 volumeId?: string;
25};
26
27function AddVolumeModal({
28 isOpen,
29 onClose,
30 sandboxId,
31 volumeId,
32}: AddVolumeModalProps) {
33 const notyf = useNotyf();
34 const [isLoading, setIsLoading] = useState(false);
35 const { mutateAsync: addVolume } = useAddVolumeMutation();
36 const { mutateAsync: updateVolume } = useUpdateVolumeMutation();
37 const { data } = useVolumeQuery(volumeId!);
38 const {
39 register,
40 handleSubmit,
41 reset,
42 setValue,
43 formState: { errors },
44 } = useForm<FormValues>({
45 resolver: zodResolver(schema),
46 });
47
48 useEffect(() => {
49 if (data) {
50 setValue("name", data.volume.name);
51 setValue("path", data.volume.path);
52 }
53 }, [data, setValue]);
54
55 useEffect(() => {
56 const handleEscapeKey = (event: KeyboardEvent) => {
57 if (event.key === "Escape" && isOpen) {
58 reset();
59 onClose();
60 }
61 };
62
63 document.addEventListener("keydown", handleEscapeKey);
64 return () => {
65 document.removeEventListener("keydown", handleEscapeKey);
66 };
67 }, [isOpen, onClose, reset]);
68
69 const handleBackdropClick = (e: React.MouseEvent<HTMLDivElement>) => {
70 e.stopPropagation();
71 if (e.target === e.currentTarget) {
72 reset();
73 onClose();
74 }
75 };
76
77 const handleContentClick = (e: React.MouseEvent<HTMLDivElement>) => {
78 e.stopPropagation();
79 };
80
81 const handleCloseButton = (e: React.MouseEvent<HTMLButtonElement>) => {
82 e.stopPropagation();
83 reset();
84 onClose();
85 };
86
87 const onSubmit = async (data: FormValues) => {
88 setIsLoading(true);
89 try {
90 if (volumeId) {
91 await updateVolume({
92 id: volumeId,
93 name: data.name,
94 path: data.path,
95 sandboxId,
96 });
97 setIsLoading(false);
98 reset();
99 onClose();
100 notyf.open("primary", "Volume updated successfully!");
101 return;
102 }
103 await addVolume({
104 sandboxId,
105 name: data.name,
106 path: data.path,
107 });
108 setIsLoading(false);
109 reset();
110 onClose();
111 notyf.open("primary", "Volume added successfully");
112 } catch {
113 setIsLoading(false);
114 reset();
115 onClose();
116 notyf.open("error", "Failed to add volume");
117 }
118 };
119
120 if (!isOpen) return null;
121
122 return createPortal(
123 <>
124 <div
125 className="overlay modal modal-middle overlay-open:opacity-100 overlay-open:duration-300 open opened"
126 role="dialog"
127 style={{ outline: "none", zIndex: 80 }}
128 onClick={handleBackdropClick}
129 onMouseDown={handleBackdropClick}
130 >
131 <div
132 className={`overlay-animation-target modal-dialog overlay-open:duration-300 transition-all ease-out modal-dialog-lg overlay-open:mt-4 mt-12`}
133 onClick={handleContentClick}
134 onMouseDown={handleContentClick}
135 >
136 <div className="modal-content">
137 <div className="modal-header">
138 <div className="flex-1">
139 {volumeId ? "Edit Volume" : "Add Volume"}
140 </div>
141 <button
142 type="button"
143 className="btn btn-text btn-circle btn-sm absolute end-3 top-3"
144 aria-label="Close"
145 onClick={handleCloseButton}
146 onMouseDown={(e) => e.stopPropagation()}
147 >
148 <span className="icon-[tabler--x] size-4"></span>
149 </button>
150 </div>
151 <form onSubmit={handleSubmit(onSubmit)}>
152 <div className="modal-body" style={{ height: 300 }}>
153 <div className="form-control w-full">
154 <label className="label">
155 <span className="label-text font-bold mb-1 text-[14px]">
156 Name
157 </span>
158 </label>
159 <div
160 className={`input input-bordered w-full input-lg text-[15px] font-semibold bg-transparent`}
161 >
162 <input
163 type="text"
164 placeholder="Volume Name"
165 className={`grow`}
166 autoComplete="off"
167 data-1p-ignore
168 data-lpignore="true"
169 data-form-type="other"
170 style={{ fontFamily: "CaskaydiaNerdFontMonoRegular" }}
171 {...register("name")}
172 />
173 </div>
174 {errors.name && (
175 <span className="text-error text-[12px] mt-1">
176 {errors.name.message}
177 </span>
178 )}
179 <div className="mt-5">
180 <label className="label">
181 <span className="label-text font-bold mb-1 text-[14px]">
182 Path
183 </span>
184 </label>
185 <div
186 className={`input input-bordered w-full input-lg text-[15px] font-semibold bg-transparent`}
187 >
188 <input
189 type="text"
190 placeholder="Mount Path, e.g /data"
191 className={`grow`}
192 autoComplete="off"
193 data-1p-ignore
194 data-lpignore="true"
195 data-form-type="other"
196 style={{ fontFamily: "CaskaydiaNerdFontMonoRegular" }}
197 {...register("path")}
198 />
199 </div>
200 {errors.path && (
201 <span className="text-error text-[12px] mt-1 block">
202 {errors.path.message}
203 </span>
204 )}
205 </div>
206 </div>
207 </div>
208 <div className="modal-footer">
209 <button type="submit" className="btn btn-primary font-semibold">
210 {isLoading && (
211 <span className="loading loading-spinner loading-xs mr-1.5"></span>
212 )}
213 {volumeId ? "Save Changes" : "Add Volume"}
214 </button>
215 </div>
216 </form>
217 </div>
218 </div>
219 </div>
220
221 <div
222 data-overlay-backdrop-template=""
223 style={{ zIndex: 79 }}
224 className="overlay-backdrop transition duration-300 fixed inset-0 bg-base-300/60 overflow-y-auto opacity-75"
225 onClick={handleBackdropClick}
226 onMouseDown={(e) => e.stopPropagation()}
227 ></div>
228 </>,
229 document.body,
230 );
231}
232
233export default AddVolumeModal;