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