open, interoperable sandbox platform for agents and humans 📦 ✨ pocketenv.io
claude-code atproto sandbox openclaw agent
at main 243 lines 7.6 kB view raw
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;