11import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema" ;
2- import { Plus , Trash2 } from "lucide-react" ;
32import { useEffect } from "react" ;
43import { useFieldArray , useForm } from "react-hook-form" ;
54import { toast } from "sonner" ;
@@ -20,25 +19,40 @@ import {
2019 FormLabel ,
2120 FormMessage ,
2221} from "@/components/ui/form" ;
23- import { Input } from "@/components/ui/input " ;
22+ import { RadioGroup , RadioGroupItem } from "@/components/ui/radio-group " ;
2423import { api } from "@/utils/api" ;
24+ import { CustomShellFields } from "./custom-shell-fields" ;
25+ import { SingleCommandFields } from "./single-command-fields" ;
2526
2627interface Props {
2728 applicationId : string ;
2829}
2930
30- const AddRedirectSchema = z . object ( {
31- command : z . string ( ) ,
32- args : z
33- . array (
34- z . object ( {
35- value : z . string ( ) . min ( 1 , "Argument cannot be empty" ) ,
36- } ) ,
37- )
38- . optional ( ) ,
39- } ) ;
31+ const AddCommandSchema = z
32+ . object ( {
33+ commandMode : z . enum ( [ "single" , "custom" ] ) ,
34+ command : z . string ( ) ,
35+ customCommand : z . string ( ) . max ( 20000 ) . optional ( ) ,
36+ customShell : z . enum ( [ "sh" , "bash" ] ) . optional ( ) ,
37+ args : z
38+ . array (
39+ z . object ( {
40+ value : z . string ( ) . min ( 1 , "Argument cannot be empty" ) ,
41+ } ) ,
42+ )
43+ . optional ( ) ,
44+ } )
45+ . superRefine ( ( data , ctx ) => {
46+ if ( data . commandMode === "custom" && ! data . customCommand ?. trim ( ) ) {
47+ ctx . addIssue ( {
48+ code : "custom" ,
49+ path : [ "customCommand" ] ,
50+ message : "Enter a script" ,
51+ } ) ;
52+ }
53+ } ) ;
4054
41- type AddCommand = z . infer < typeof AddRedirectSchema > ;
55+ export type AddCommandForm = z . infer < typeof AddCommandSchema > ;
4256
4357export const AddCommand = ( { applicationId } : Props ) => {
4458 const { data } = api . application . one . useQuery (
@@ -52,33 +66,48 @@ export const AddCommand = ({ applicationId }: Props) => {
5266
5367 const { mutateAsync, isPending } = api . application . update . useMutation ( ) ;
5468
55- const form = useForm < AddCommand > ( {
69+ const form = useForm < AddCommandForm > ( {
5670 defaultValues : {
71+ commandMode : "single" ,
5772 command : "" ,
73+ customCommand : "" ,
74+ customShell : "sh" ,
5875 args : [ ] ,
5976 } ,
60- resolver : zodResolver ( AddRedirectSchema ) ,
77+ resolver : zodResolver ( AddCommandSchema ) ,
6178 } ) ;
6279
6380 const { fields, append, remove } = useFieldArray ( {
6481 control : form . control ,
6582 name : "args" ,
6683 } ) ;
6784
85+ const commandMode = form . watch ( "commandMode" ) ;
86+ const customCommand = form . watch ( "customCommand" ) ;
87+
6888 useEffect ( ( ) => {
6989 if ( data ) {
7090 form . reset ( {
91+ commandMode : data ?. customCommand ? "custom" : "single" ,
7192 command : data ?. command || "" ,
93+ customCommand : data ?. customCommand || "" ,
94+ customShell : data ?. customShell === "bash" ? "bash" : "sh" ,
7295 args : data ?. args ?. map ( ( arg ) => ( { value : arg } ) ) || [ ] ,
7396 } ) ;
7497 }
7598 } , [ data , form ] ) ;
7699
77- const onSubmit = async ( data : AddCommand ) => {
100+ const onSubmit = async ( values : AddCommandForm ) => {
78101 await mutateAsync ( {
79102 applicationId,
80- command : data ?. command ,
81- args : data ?. args ?. map ( ( arg ) => arg . value ) . filter ( Boolean ) ,
103+ command : values ?. command ,
104+ args : values ?. args ?. map ( ( arg ) => arg . value ) . filter ( Boolean ) ,
105+ customCommand :
106+ values . commandMode === "custom"
107+ ? values . customCommand ?. trim ( ) || null
108+ : null ,
109+ customShell :
110+ values . commandMode === "custom" ? ( values . customShell ?? "sh" ) : null ,
82111 } )
83112 . then ( async ( ) => {
84113 toast . success ( "Command Updated" ) ;
@@ -111,73 +140,54 @@ export const AddCommand = ({ applicationId }: Props) => {
111140 < div className = "flex flex-col gap-4" >
112141 < FormField
113142 control = { form . control }
114- name = "command "
143+ name = "commandMode "
115144 render = { ( { field } ) => (
116- < FormItem >
117- < FormLabel > Command </ FormLabel >
145+ < FormItem className = "space-y-3" >
146+ < FormLabel > Mode </ FormLabel >
118147 < FormControl >
119- < Input placeholder = "/bin/sh" { ...field } />
148+ < RadioGroup
149+ onValueChange = { field . onChange }
150+ value = { field . value }
151+ className = "flex flex-row gap-6"
152+ >
153+ < FormItem className = "flex items-center space-x-2 space-y-0" >
154+ < FormControl >
155+ < RadioGroupItem value = "single" />
156+ </ FormControl >
157+ < FormLabel className = "font-normal" > Single</ FormLabel >
158+ </ FormItem >
159+ < FormItem className = "flex items-center space-x-2 space-y-0" >
160+ < FormControl >
161+ < RadioGroupItem value = "custom" />
162+ </ FormControl >
163+ < FormLabel className = "font-normal" >
164+ Custom shell
165+ </ FormLabel >
166+ </ FormItem >
167+ </ RadioGroup >
120168 </ FormControl >
121-
122169 < FormMessage />
123170 </ FormItem >
124171 ) }
125172 />
126-
127- < div className = "space-y-2" >
128- < div className = "flex items-center justify-between" >
129- < FormLabel > Arguments (Args)</ FormLabel >
130- < Button
131- type = "button"
132- variant = "outline"
133- size = "sm"
134- onClick = { ( ) => append ( { value : "" } ) }
135- >
136- < Plus className = "h-4 w-4 mr-1" />
137- Add Argument
138- </ Button >
139- </ div >
140-
141- { fields . length === 0 && (
142- < p className = "text-sm text-muted-foreground" >
143- No arguments added yet. Click "Add Argument" to add one.
144- </ p >
145- ) }
146-
147- { fields . map ( ( field , index ) => (
148- < FormField
149- key = { field . id }
150- control = { form . control }
151- name = { `args.${ index } .value` }
152- render = { ( { field } ) => (
153- < FormItem >
154- < div className = "flex gap-2" >
155- < FormControl >
156- < Input
157- placeholder = {
158- index === 0 ? "-c" : "echo Hello World"
159- }
160- { ...field }
161- />
162- </ FormControl >
163- < Button
164- type = "button"
165- variant = "destructive"
166- size = "icon"
167- onClick = { ( ) => remove ( index ) }
168- >
169- < Trash2 className = "h-4 w-4" />
170- </ Button >
171- </ div >
172- < FormMessage />
173- </ FormItem >
174- ) }
175- />
176- ) ) }
177- </ div >
173+ { commandMode === "custom" ? (
174+ < CustomShellFields control = { form . control } />
175+ ) : (
176+ < SingleCommandFields
177+ control = { form . control }
178+ fields = { fields }
179+ append = { append }
180+ remove = { remove }
181+ />
182+ ) }
178183 </ div >
179184 < div className = "flex justify-end" >
180- < Button isLoading = { isPending } type = "submit" className = "w-fit" >
185+ < Button
186+ isLoading = { isPending }
187+ type = "submit"
188+ className = "w-fit"
189+ disabled = { commandMode === "custom" && ! customCommand ?. trim ( ) }
190+ >
181191 Save
182192 </ Button >
183193 </ div >
0 commit comments