the universal sandbox runtime for agents and humans. pocketenv.io
sandbox openclaw agent claude-code vercel-sandbox deno-sandbox cloudflare-sandbox atproto sprites daytona
7
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add ConfirmDelete modal and edit mode support

Add ConfirmDelete component and index export. Update
AddFile/AddSecret/AddVariable/AddVolume
modals to accept optional id props and toggle between Add/Edit. Enforce
UPPER_SNAKE_CASE for
secret/variable names and auto-transform input. Wire confirm-delete
modal into Files, Secrets,
Variables and Volumes pages (state + placeholder handler). Minor
UI/class cleanup and copy tweak.

+334 -45
+119
apps/web/src/components/confirmdelete/ConfirmDelete.tsx
··· 1 + import { useEffect, useState } from "react"; 2 + import { createPortal } from "react-dom"; 3 + 4 + export type ConfirmDeleteModalProps = { 5 + isOpen: boolean; 6 + onClose: () => void; 7 + onConfirm: () => Promise<void>; 8 + subject: string; 9 + title?: string; 10 + }; 11 + 12 + function ConfirmDeleteModal({ 13 + isOpen, 14 + onClose, 15 + onConfirm, 16 + subject, 17 + title, 18 + }: ConfirmDeleteModalProps) { 19 + const [isLoading, setIsLoading] = useState(false); 20 + useEffect(() => { 21 + const handleEscapeKey = (event: KeyboardEvent) => { 22 + if (event.key === "Escape" && isOpen) { 23 + onClose(); 24 + } 25 + }; 26 + 27 + document.addEventListener("keydown", handleEscapeKey); 28 + return () => { 29 + document.removeEventListener("keydown", handleEscapeKey); 30 + }; 31 + }, [isOpen, onClose]); 32 + 33 + const onDelete = async (e: React.MouseEvent<HTMLButtonElement>) => { 34 + setIsLoading(true); 35 + await onConfirm(); 36 + setIsLoading(false); 37 + e.stopPropagation(); 38 + onClose(); 39 + }; 40 + 41 + const handleBackdropClick = (e: React.MouseEvent<HTMLDivElement>) => { 42 + e.stopPropagation(); 43 + if (e.target === e.currentTarget) { 44 + onClose(); 45 + } 46 + }; 47 + 48 + const handleContentClick = (e: React.MouseEvent<HTMLDivElement>) => { 49 + e.stopPropagation(); 50 + }; 51 + 52 + const handleCloseButton = (e: React.MouseEvent<HTMLButtonElement>) => { 53 + e.stopPropagation(); 54 + onClose(); 55 + }; 56 + 57 + if (!isOpen) return null; 58 + 59 + return createPortal( 60 + <> 61 + <div 62 + className="overlay modal modal-middle overlay-open:opacity-100 overlay-open:duration-300 open opened" 63 + role="dialog" 64 + style={{ outline: "none", zIndex: 80 }} 65 + onClick={handleBackdropClick} 66 + onMouseDown={handleBackdropClick} 67 + > 68 + <div 69 + className={`overlay-animation-target modal-dialog overlay-open:duration-300 transition-all ease-out modal-dialog-md overlay-open:mt-4 mt-12`} 70 + onClick={handleContentClick} 71 + onMouseDown={handleContentClick} 72 + > 73 + <div className="modal-content"> 74 + <div className="modal-header pb-0"> 75 + <div className="flex-1">Delete {title}</div> 76 + <button 77 + type="button" 78 + className="btn btn-text btn-circle btn-sm absolute end-3 top-3" 79 + aria-label="Close" 80 + onClick={handleCloseButton} 81 + onMouseDown={(e) => e.stopPropagation()} 82 + > 83 + <span className="icon-[tabler--x] size-4"></span> 84 + </button> 85 + </div> 86 + <div className="modal-body p-0 pl-2 h-[100px] flex flex-col justify-center"> 87 + <p className="font-semibold text-center"> 88 + Are you sure you want to delete this {subject}? 89 + </p> 90 + <p className="text-center ">This action cannot be undone.</p> 91 + </div> 92 + <div className="modal-footer"> 93 + <button 94 + className="btn btn-error font-semibold" 95 + onClick={onDelete} 96 + > 97 + {isLoading && ( 98 + <span className="loading loading-spinner loading-xs mr-1.5"></span> 99 + )} 100 + Yes, delete 101 + </button> 102 + </div> 103 + </div> 104 + </div> 105 + </div> 106 + 107 + <div 108 + data-overlay-backdrop-template="" 109 + style={{ zIndex: 79 }} 110 + className="overlay-backdrop transition duration-300 fixed inset-0 bg-base-300/60 overflow-y-auto opacity-75" 111 + onClick={handleBackdropClick} 112 + onMouseDown={(e) => e.stopPropagation()} 113 + ></div> 114 + </>, 115 + document.body, 116 + ); 117 + } 118 + 119 + export default ConfirmDeleteModal;
+2
apps/web/src/components/confirmdelete/index.tsx
··· 1 + import ConfirmDelete from "./ConfirmDelete"; 2 + export default ConfirmDelete;
+12 -6
apps/web/src/components/contextmenu/AddFileModal/AddFileModal.tsx
··· 19 19 isOpen: boolean; 20 20 onClose: () => void; 21 21 sandboxId: string; 22 + fileId?: string; 22 23 }; 23 24 24 - function AddFileModal({ isOpen, onClose, sandboxId }: AddFileModalProps) { 25 + function AddFileModal({ 26 + isOpen, 27 + onClose, 28 + sandboxId, 29 + fileId, 30 + }: AddFileModalProps) { 25 31 const sodium = useSodium(); 26 32 const notyf = useNotyf(); 27 33 const [isLoading, setIsLoading] = useState(false); ··· 113 119 > 114 120 <div className="modal-content"> 115 121 <div className="modal-header"> 116 - <div className="flex-1">Add File</div> 122 + <div className="flex-1">{fileId ? "Edit File" : "Add File"}</div> 117 123 <button 118 124 type="button" 119 125 className="btn btn-text btn-circle btn-sm absolute end-3 top-3" ··· 135 141 <input 136 142 type="text" 137 143 placeholder="File Mount Path, e.g /root/.openclaw/openclaw.json" 138 - className={`grow ${errors.path ? "is-invalid" : ""}`} 144 + className={`grow`} 139 145 autoComplete="off" 140 146 data-1p-ignore 141 147 data-lpignore="true" ··· 156 162 </span> 157 163 </label> 158 164 <textarea 159 - className={`textarea max-w-full h-[250px] text-[14px] font-semibold ${errors.content ? "is-invalid" : ""}`} 165 + className={`textarea max-w-full h-[250px] text-[14px] font-semibold`} 160 166 aria-label="Textarea" 161 167 placeholder="File Content" 162 168 style={{ fontFamily: "CaskaydiaNerdFontMonoRegular" }} 163 169 {...register("content")} 164 170 ></textarea> 165 171 {errors.content && ( 166 - <span className="helper-text text-[12px] mt-1 block"> 172 + <span className="text-error text-[12px] mt-1 block"> 167 173 {errors.content.message} 168 174 </span> 169 175 )} ··· 177 183 {isLoading && ( 178 184 <span className="loading loading-spinner loading-xs mr-1.5"></span> 179 185 )} 180 - Add File 186 + {fileId ? "Save Changes" : "Add File"} 181 187 </button> 182 188 </div> 183 189 </form>
+34 -9
apps/web/src/components/contextmenu/AddSecretModal/AddSecretModal.tsx
··· 8 8 import { PUBLIC_KEY } from "../../../consts"; 9 9 import { useNotyf } from "../../../hooks/useNotyf"; 10 10 11 + const UPPER_SNAKE_CASE_REGEX = /^[A-Z][A-Z0-9_]*$/; 12 + 11 13 const schema = z.object({ 12 - name: z.string().min(1, "Name is required"), 14 + name: z 15 + .string() 16 + .min(1, "Name is required") 17 + .regex( 18 + UPPER_SNAKE_CASE_REGEX, 19 + "Name must be in UPPER_SNAKE_CASE (e.g. MY_SECRET)", 20 + ), 13 21 value: z.string().min(1, "Value is required"), 14 22 }); 15 23 ··· 19 27 isOpen: boolean; 20 28 onClose: () => void; 21 29 sandboxId: string; 30 + secretId?: string; 22 31 }; 23 32 24 - function AddSecretModal({ isOpen, onClose, sandboxId }: AddSecretModalProps) { 33 + function AddSecretModal({ 34 + isOpen, 35 + onClose, 36 + sandboxId, 37 + secretId, 38 + }: AddSecretModalProps) { 25 39 const sodium = useSodium(); 26 40 const notyf = useNotyf(); 27 41 const [isLoading, setIsLoading] = useState(false); ··· 30 44 register, 31 45 handleSubmit, 32 46 reset, 47 + setValue, 33 48 formState: { errors }, 34 49 } = useForm<FormValues>({ 35 50 resolver: zodResolver(schema), 36 51 }); 37 52 53 + const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => { 54 + const transformed = e.target.value 55 + .toUpperCase() 56 + .replace(/\s+/g, "_") 57 + .replace(/[^A-Z0-9_]/g, ""); 58 + setValue("name", transformed, { shouldValidate: true }); 59 + }; 60 + 38 61 useEffect(() => { 39 62 const handleEscapeKey = (event: KeyboardEvent) => { 40 63 if (event.key === "Escape" && isOpen) { ··· 112 135 > 113 136 <div className="modal-content"> 114 137 <div className="modal-header"> 115 - <div className="flex-1">Add Secret</div> 138 + <div className="flex-1"> 139 + {secretId ? "Edit Secret" : "Add Secret"} 140 + </div> 116 141 <button 117 142 type="button" 118 143 className="btn btn-text btn-circle btn-sm absolute end-3 top-3" ··· 137 162 <input 138 163 type="text" 139 164 placeholder="YOUR_SECRET_NAME" 140 - className={`grow ${errors.name ? "is-invalid" : ""}`} 165 + className={`grow`} 141 166 autoComplete="off" 142 167 data-1p-ignore 143 168 data-lpignore="true" 144 169 data-form-type="other" 145 170 style={{ fontFamily: "CaskaydiaNerdFontMonoRegular" }} 146 - {...register("name")} 171 + {...register("name", { onChange: handleNameChange })} 147 172 /> 148 173 </div> 149 174 {errors.name && ( 150 - <span className="helper-text text-[12px] mt-1"> 175 + <span className="text-error text-[12px] mt-1"> 151 176 {errors.name.message} 152 177 </span> 153 178 )} ··· 158 183 </span> 159 184 </label> 160 185 <textarea 161 - className={`textarea max-w-full h-[250px] text-[14px] font-semibold ${errors.value ? "is-invalid" : ""}`} 186 + className={`textarea max-w-full h-[250px] text-[14px] font-semibold`} 162 187 aria-label="Textarea" 163 188 placeholder="Secret Value" 164 189 style={{ fontFamily: "CaskaydiaNerdFontMonoRegular" }} 165 190 {...register("value")} 166 191 ></textarea> 167 192 {errors.value && ( 168 - <span className="helper-text text-[12px] mt-1 block"> 193 + <span className="text-error text-[12px] mt-1 block"> 169 194 {errors.value.message} 170 195 </span> 171 196 )} ··· 177 202 {isLoading && ( 178 203 <span className="loading loading-spinner loading-xs mr-1.5"></span> 179 204 )} 180 - Add Secret 205 + {secretId ? "Save Changes" : "Add Secret"} 181 206 </button> 182 207 </div> 183 208 </form>
+29 -8
apps/web/src/components/contextmenu/AddVariableModal/AddVariableModal.tsx
··· 6 6 import { useAddVariableMutation } from "../../../hooks/useVariable"; 7 7 import { useNotyf } from "../../../hooks/useNotyf"; 8 8 9 + const UPPER_SNAKE_CASE_REGEX = /^[A-Z][A-Z0-9_]*$/; 10 + 9 11 const schema = z.object({ 10 - name: z.string().min(1, "Name is required"), 12 + name: z 13 + .string() 14 + .min(1, "Name is required") 15 + .regex( 16 + UPPER_SNAKE_CASE_REGEX, 17 + "Name must be in UPPER_SNAKE_CASE (e.g. MY_VARIABLE)", 18 + ), 11 19 value: z.string().min(1, "Value is required"), 12 20 }); 13 21 ··· 17 25 isOpen: boolean; 18 26 onClose: () => void; 19 27 sandboxId: string; 28 + variableId?: string; 20 29 }; 21 30 22 31 function AddEnvironmentVariableModal({ 23 32 isOpen, 24 33 onClose, 25 34 sandboxId, 35 + variableId, 26 36 }: AddEnvironmentVariableModalProps) { 27 37 const [isLoading, setIsLoading] = useState(false); 28 38 const notyf = useNotyf(); ··· 31 41 register, 32 42 handleSubmit, 33 43 reset, 44 + setValue, 34 45 formState: { errors }, 35 46 } = useForm<FormValues>({ 36 47 resolver: zodResolver(schema), 37 48 }); 38 49 50 + const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => { 51 + const transformed = e.target.value 52 + .toUpperCase() 53 + .replace(/\s+/g, "_") 54 + .replace(/[^A-Z0-9_]/g, ""); 55 + setValue("name", transformed, { shouldValidate: true }); 56 + }; 57 + 39 58 useEffect(() => { 40 59 const handleEscapeKey = (event: KeyboardEvent) => { 41 60 if (event.key === "Escape" && isOpen) { ··· 106 125 > 107 126 <div className="modal-content"> 108 127 <div className="modal-header"> 109 - <div className="flex-1">Add Variable</div> 128 + <div className="flex-1"> 129 + {variableId ? "Edit Variable" : "Add Variable"} 130 + </div> 110 131 <button 111 132 type="button" 112 133 className="btn btn-text btn-circle btn-sm absolute end-3 top-3" ··· 129 150 <input 130 151 type="text" 131 152 placeholder="YOUR_VARIABLE_NAME" 132 - className={`grow ${errors.name ? "is-invalid" : ""}`} 153 + className={`grow`} 133 154 autoComplete="off" 134 155 data-1p-ignore 135 156 data-lpignore="true" 136 157 data-form-type="other" 137 158 style={{ fontFamily: "CaskaydiaNerdFontMonoRegular" }} 138 - {...register("name")} 159 + {...register("name", { onChange: handleNameChange })} 139 160 /> 140 161 </div> 141 162 {errors.name && ( 142 - <span className="helper-text text-[12px] mt-1"> 163 + <span className="text-error text-[12px] mt-1"> 143 164 {errors.name.message} 144 165 </span> 145 166 )} ··· 150 171 </span> 151 172 </label> 152 173 <textarea 153 - className={`textarea max-w-full h-[250px] text-[14px] font-semibold ${errors.value ? "is-invalid" : ""}`} 174 + className={`textarea max-w-full h-[250px] text-[14px] font-semibold`} 154 175 aria-label="Textarea" 155 176 placeholder="Variable Value" 156 177 style={{ fontFamily: "CaskaydiaNerdFontMonoRegular" }} 157 178 {...register("value")} 158 179 ></textarea> 159 180 {errors.value && ( 160 - <span className="helper-text text-[12px] mt-1 block"> 181 + <span className="text-error text-[12px] mt-1 block"> 161 182 {errors.value.message} 162 183 </span> 163 184 )} ··· 172 193 {isLoading && ( 173 194 <span className="loading loading-spinner loading-xs mr-1.5"></span> 174 195 )} 175 - Add Variable 196 + {variableId ? "Save Changes" : "Add Variable"} 176 197 </button> 177 198 </div> 178 199 </form>
+16 -8
apps/web/src/components/contextmenu/AddVolumeModal/AddVolumeModal.tsx
··· 17 17 isOpen: boolean; 18 18 onClose: () => void; 19 19 sandboxId: string; 20 + volumeId?: string; 20 21 }; 21 22 22 - function AddVolumeModal({ isOpen, onClose, sandboxId }: AddVolumeModalProps) { 23 + function AddVolumeModal({ 24 + isOpen, 25 + onClose, 26 + sandboxId, 27 + volumeId, 28 + }: AddVolumeModalProps) { 23 29 const notyf = useNotyf(); 24 30 const [isLoading, setIsLoading] = useState(false); 25 31 const { mutateAsync: addVolume } = useAddVolumeMutation(); ··· 102 108 > 103 109 <div className="modal-content"> 104 110 <div className="modal-header"> 105 - <div className="flex-1">Add Volume</div> 111 + <div className="flex-1"> 112 + {volumeId ? "Edit Volume" : "Add Volume"} 113 + </div> 106 114 <button 107 115 type="button" 108 116 className="btn btn-text btn-circle btn-sm absolute end-3 top-3" ··· 122 130 </span> 123 131 </label> 124 132 <div 125 - className={`input input-bordered w-full input-lg text-[15px] font-semibold bg-transparent ${errors.name ? "is-invalid" : ""}`} 133 + className={`input input-bordered w-full input-lg text-[15px] font-semibold bg-transparent`} 126 134 > 127 135 <input 128 136 type="text" 129 137 placeholder="Volume Name" 130 - className={`grow ${errors.name ? "is-invalid" : ""}`} 138 + className={`grow`} 131 139 autoComplete="off" 132 140 data-1p-ignore 133 141 data-lpignore="true" ··· 148 156 </span> 149 157 </label> 150 158 <div 151 - className={`input input-bordered w-full input-lg text-[15px] font-semibold bg-transparent ${errors.path ? "is-invalid" : ""}`} 159 + className={`input input-bordered w-full input-lg text-[15px] font-semibold bg-transparent`} 152 160 > 153 161 <input 154 162 type="text" 155 163 placeholder="Mount Path, e.g /data" 156 - className={`grow ${errors.path ? "is-invalid" : ""}`} 164 + className={`grow`} 157 165 autoComplete="off" 158 166 data-1p-ignore 159 167 data-lpignore="true" ··· 163 171 /> 164 172 </div> 165 173 {errors.path && ( 166 - <span className="helper-text text-[12px] mt-1 block"> 174 + <span className="text-error text-[12px] mt-1 block"> 167 175 {errors.path.message} 168 176 </span> 169 177 )} ··· 175 183 {isLoading && ( 176 184 <span className="loading loading-spinner loading-xs mr-1.5"></span> 177 185 )} 178 - Add Volume 186 + {volumeId ? "Save Changes" : "Add Volume"} 179 187 </button> 180 188 </div> 181 189 </form>
+32 -5
apps/web/src/pages/settings/files/Files.tsx
··· 8 8 import { useFilesQuery } from "../../../hooks/useFile"; 9 9 import dayjs from "dayjs"; 10 10 import Pagination from "../../../components/pagination"; 11 + import ConfirmDelete from "../../../components/ConfirmDelete"; 11 12 12 13 const PAGE_SIZE = 12; 13 14 const SKELETON_ROWS = 8; ··· 32 33 ); 33 34 34 35 function Files() { 36 + const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false); 37 + const [selectedFileId, setSelectedFileId] = useState<string | undefined>( 38 + undefined, 39 + ); 35 40 const [isOpen, setIsOpen] = useState(false); 36 41 const [currentPage, setCurrentPage] = useState(1); 37 42 const offset = (currentPage - 1) * PAGE_SIZE; ··· 53 58 setCurrentPage(page); 54 59 }; 55 60 61 + const handleConfirmDelete = async () => {}; 62 + 56 63 return ( 57 64 <Main 58 65 sidebar={<Sidebar />} ··· 71 78 </button> 72 79 </div> 73 80 <p className="opacity-60 mb-5"> 74 - Files (encrypted) that are automatically injected into the sandbox 75 - filesystem. 81 + Files (will be encrypted) that are automatically injected into the 82 + sandbox filesystem. 76 83 </p> 77 84 <div className="w-full overflow-x-auto"> 78 85 <table className="table mb-20"> ··· 104 111 </td> 105 112 <td className="normal-case text-[14px] text-right"> 106 113 <div className="join"> 107 - <button className="btn btn-outline join-item"> 114 + <button 115 + className="btn btn-outline join-item" 116 + onClick={() => { 117 + setSelectedFileId(file.id); 118 + setIsOpen(true); 119 + }} 120 + > 108 121 Edit 109 122 </button> 110 - <button className="btn btn-outline join-item"> 123 + <button 124 + className="btn btn-outline join-item" 125 + onClick={() => setConfirmDeleteOpen(true)} 126 + > 111 127 Delete 112 128 </button> 113 129 </div> ··· 129 145 </div> 130 146 <AddFileModal 131 147 isOpen={isOpen} 132 - onClose={() => setIsOpen(false)} 148 + onClose={() => { 149 + setIsOpen(false); 150 + setSelectedFileId(undefined); 151 + }} 133 152 sandboxId={data?.sandbox?.id ?? ""} 153 + fileId={selectedFileId} 154 + /> 155 + <ConfirmDelete 156 + isOpen={confirmDeleteOpen} 157 + onClose={() => setConfirmDeleteOpen(false)} 158 + onConfirm={handleConfirmDelete} 159 + subject={"file"} 160 + title={"file?"} 134 161 /> 135 162 </> 136 163 </Main>
+30 -3
apps/web/src/pages/settings/secrets/Secrets.tsx
··· 8 8 import { useSecretsQuery } from "../../../hooks/useSecret"; 9 9 import dayjs from "dayjs"; 10 10 import Pagination from "../../../components/pagination"; 11 + import ConfirmDelete from "../../../components/confirmdelete/ConfirmDelete"; 11 12 12 13 const PAGE_SIZE = 12; 13 14 const SKELETON_ROWS = 8; ··· 32 33 ); 33 34 34 35 function Secrets() { 36 + const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false); 37 + const [selectedSecretId, setSelectedSecretId] = useState<string | undefined>( 38 + undefined, 39 + ); 35 40 const [isOpen, setIsOpen] = useState(false); 36 41 const [currentPage, setCurrentPage] = useState(1); 37 42 const offset = (currentPage - 1) * PAGE_SIZE; ··· 52 57 const onPageChange = (page: number) => { 53 58 setCurrentPage(page); 54 59 }; 60 + 61 + const handleConfirmDelete = async () => {}; 55 62 56 63 return ( 57 64 <Main ··· 109 116 </td> 110 117 <td className="normal-case text-[14px] text-right"> 111 118 <div className="join"> 112 - <button className="btn btn-outline join-item"> 119 + <button 120 + className="btn btn-outline join-item" 121 + onClick={() => { 122 + setSelectedSecretId(secret.id); 123 + setIsOpen(true); 124 + }} 125 + > 113 126 Edit 114 127 </button> 115 - <button className="btn btn-outline join-item"> 128 + <button 129 + className="btn btn-outline join-item" 130 + onClick={() => setConfirmDeleteOpen(true)} 131 + > 116 132 Delete 117 133 </button> 118 134 </div> ··· 134 150 </div> 135 151 <AddSecretModal 136 152 isOpen={isOpen} 137 - onClose={() => setIsOpen(false)} 153 + onClose={() => { 154 + setIsOpen(false); 155 + setSelectedSecretId(undefined); 156 + }} 138 157 sandboxId={data?.sandbox?.id ?? ""} 158 + secretId={selectedSecretId} 159 + /> 160 + <ConfirmDelete 161 + isOpen={confirmDeleteOpen} 162 + onClose={() => setConfirmDeleteOpen(false)} 163 + onConfirm={handleConfirmDelete} 164 + subject={"secret"} 165 + title={"secret?"} 139 166 /> 140 167 </> 141 168 </Main>
+30 -3
apps/web/src/pages/settings/variables/Variables.tsx
··· 8 8 import { useVariablesQuery } from "../../../hooks/useVariable"; 9 9 import dayjs from "dayjs"; 10 10 import Pagination from "../../../components/pagination"; 11 + import ConfirmDelete from "../../../components/confirmdelete"; 11 12 12 13 const PAGE_SIZE = 10; 13 14 const SKELETON_ROWS = 8; ··· 34 35 ); 35 36 36 37 function Variables() { 38 + const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false); 39 + const [selectedVariableId, setSelectedVariableId] = useState< 40 + string | undefined 41 + >(undefined); 37 42 const [isOpen, setIsOpen] = useState(false); 38 43 const [currentPage, setCurrentPage] = useState(1); 39 44 const offset = (currentPage - 1) * PAGE_SIZE; ··· 56 61 const onPageChange = (page: number) => { 57 62 setCurrentPage(page); 58 63 }; 64 + 65 + const handleConfirmDelete = async () => {}; 59 66 60 67 return ( 61 68 <Main ··· 116 123 </td> 117 124 <td className="normal-case text-[14px] text-right"> 118 125 <div className="join"> 119 - <button className="btn btn-outline join-item"> 126 + <button 127 + className="btn btn-outline join-item" 128 + onClick={() => { 129 + setSelectedVariableId(variable.id); 130 + setIsOpen(true); 131 + }} 132 + > 120 133 Edit 121 134 </button> 122 - <button className="btn btn-outline join-item"> 135 + <button 136 + className="btn btn-outline join-item" 137 + onClick={() => setConfirmDeleteOpen(true)} 138 + > 123 139 Delete 124 140 </button> 125 141 </div> ··· 141 157 </div> 142 158 <AddVariableModal 143 159 isOpen={isOpen} 144 - onClose={() => setIsOpen(false)} 160 + onClose={() => { 161 + setIsOpen(false); 162 + setSelectedVariableId(undefined); 163 + }} 145 164 sandboxId={data?.sandbox?.id ?? ""} 165 + variableId={selectedVariableId} 166 + /> 167 + <ConfirmDelete 168 + isOpen={confirmDeleteOpen} 169 + onClose={() => setConfirmDeleteOpen(false)} 170 + onConfirm={handleConfirmDelete} 171 + subject={"variable"} 172 + title={"variable?"} 146 173 /> 147 174 </> 148 175 </Main>
+30 -3
apps/web/src/pages/settings/volumes/Volumes.tsx
··· 8 8 import { useVolumesQuery } from "../../../hooks/useVolume"; 9 9 import dayjs from "dayjs"; 10 10 import Pagination from "../../../components/pagination"; 11 + import ConfirmDelete from "../../../components/confirmdelete/ConfirmDelete"; 11 12 12 13 const PAGE_SIZE = 12; 13 14 const SKELETON_ROWS = 8; ··· 34 35 ); 35 36 36 37 function Volumes() { 38 + const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false); 39 + const [selectedVolumeId, setSelectedVolumeId] = useState<string | undefined>( 40 + undefined, 41 + ); 37 42 const [isOpen, setIsOpen] = useState(false); 38 43 const [currentPage, setCurrentPage] = useState(1); 39 44 const offset = (currentPage - 1) * PAGE_SIZE; ··· 54 59 const onPageChange = (page: number) => { 55 60 setCurrentPage(page); 56 61 }; 62 + 63 + const handleConfirmDelete = async () => {}; 57 64 58 65 return ( 59 66 <Main ··· 111 118 </td> 112 119 <td className="normal-case text-[14px] text-right"> 113 120 <div className="join"> 114 - <button className="btn btn-outline join-item"> 121 + <button 122 + className="btn btn-outline join-item" 123 + onClick={() => { 124 + setSelectedVolumeId(volume.id); 125 + setIsOpen(true); 126 + }} 127 + > 115 128 Edit 116 129 </button> 117 - <button className="btn btn-outline join-item"> 130 + <button 131 + className="btn btn-outline join-item" 132 + onClick={() => setConfirmDeleteOpen(true)} 133 + > 118 134 Delete 119 135 </button> 120 136 </div> ··· 136 152 </div> 137 153 <AddVolumeModal 138 154 isOpen={isOpen} 139 - onClose={() => setIsOpen(false)} 155 + onClose={() => { 156 + setIsOpen(false); 157 + setSelectedVolumeId(undefined); 158 + }} 140 159 sandboxId={data?.sandbox?.id ?? ""} 160 + volumeId={selectedVolumeId} 161 + /> 162 + <ConfirmDelete 163 + isOpen={confirmDeleteOpen} 164 + onClose={() => setConfirmDeleteOpen(false)} 165 + onConfirm={handleConfirmDelete} 166 + subject={"volume"} 167 + title={"volume?"} 141 168 /> 142 169 </> 143 170 </Main>