1
1
"use client"
2
2
3
- import React , { PropsWithChildren } from "react"
3
+ import React , { PropsWithChildren , useCallback } from "react"
4
4
import { SecretResponse , UpdateSecretParams } from "@/client"
5
5
import { zodResolver } from "@hookform/resolvers/zod"
6
6
import { DialogProps } from "@radix-ui/react-dialog"
7
- import { KeyRoundIcon , PlusCircle , Trash2Icon } from "lucide-react"
8
- import { ArrayPath , FieldPath , useFieldArray , useForm } from "react-hook-form"
7
+ import { PlusCircle , SaveIcon , Trash2Icon } from "lucide-react"
8
+ import { useFieldArray , useForm } from "react-hook-form"
9
+ import { z } from "zod"
9
10
10
- import { createSecretSchema } from "@/types/schemas"
11
11
import { useSecrets } from "@/lib/hooks"
12
12
import { Button } from "@/components/ui/button"
13
13
import {
@@ -23,7 +23,6 @@ import {
23
23
import {
24
24
Form ,
25
25
FormControl ,
26
- FormDescription ,
27
26
FormField ,
28
27
FormItem ,
29
28
FormLabel ,
@@ -40,79 +39,88 @@ interface EditCredentialsDialogProps
40
39
setSelectedSecret : ( selectedSecret : SecretResponse | null ) => void
41
40
}
42
41
42
+ export const updateSecretSchema = z . object ( {
43
+ name : z . string ( ) . optional ( ) ,
44
+ description : z . string ( ) . max ( 255 ) . optional ( ) ,
45
+ keys : z . array (
46
+ z . object ( {
47
+ key : z . string ( ) ,
48
+ value : z . string ( ) ,
49
+ } )
50
+ ) ,
51
+ } )
52
+
43
53
export function EditCredentialsDialog ( {
44
54
selectedSecret,
45
55
setSelectedSecret,
46
56
children,
47
57
className,
58
+ ...props
48
59
} : EditCredentialsDialogProps ) {
49
- const [ showDialog , setShowDialog ] = React . useState ( false )
50
60
const { updateSecretById } = useSecrets ( )
51
- console . log ( "EDIT SECRET DIALOG" , selectedSecret )
52
61
53
62
const methods = useForm < UpdateSecretParams > ( {
54
- resolver : zodResolver ( createSecretSchema ) ,
55
- values : {
56
- name : selectedSecret ?. name ?? undefined ,
57
- description : selectedSecret ?. description ?? undefined ,
58
- type : "custom" ,
59
- keys : selectedSecret ?. keys . map ( ( key ) => ( {
60
- key,
61
- value : "" ,
62
- } ) ) || [ { key : "" , value : "" } ] ,
63
+ resolver : zodResolver ( updateSecretSchema ) ,
64
+ defaultValues : {
65
+ name : "" ,
66
+ description : "" ,
67
+ keys : [ ] ,
63
68
} ,
64
69
} )
65
70
const { control, register } = methods
66
71
67
- const onSubmit = async ( values : UpdateSecretParams ) => {
68
- if ( ! selectedSecret ) {
69
- console . error ( "No secret selected" )
70
- return
71
- }
72
- console . log ( "Submitting edit secret" )
73
- try {
74
- await updateSecretById ( {
75
- secretId : selectedSecret . id ,
76
- params : values ,
77
- } )
78
- } catch ( error ) {
79
- console . error ( error )
80
- }
81
- methods . reset ( )
82
- }
72
+ const onSubmit = useCallback (
73
+ async ( values : UpdateSecretParams ) => {
74
+ if ( ! selectedSecret ) {
75
+ console . error ( "No secret selected" )
76
+ return
77
+ }
78
+ // Remove unset values from the params object
79
+ // We consider empty strings as unset values
80
+ const params = {
81
+ name : values . name || undefined ,
82
+ description : values . description || undefined ,
83
+ keys : values . keys || undefined ,
84
+ }
85
+ console . log ( "Submitting edit secret" , params )
86
+ try {
87
+ await updateSecretById ( {
88
+ secretId : selectedSecret . id ,
89
+ params,
90
+ } )
91
+ } catch ( error ) {
92
+ console . error ( error )
93
+ }
94
+ methods . reset ( )
95
+ setSelectedSecret ( null ) // Only unset the selected secret after the form has been submitted
96
+ } ,
97
+ [ selectedSecret , setSelectedSecret ]
98
+ )
83
99
84
- const onValidationFailed = ( ) => {
85
- console . error ( "Form validation failed" )
100
+ const onValidationFailed = ( errors : unknown ) => {
101
+ console . error ( "Form validation failed" , errors )
86
102
toast ( {
87
103
title : "Form validation failed" ,
88
104
description : "A validation error occurred while editing the secret." ,
89
105
} )
90
106
}
91
107
92
- const inputKey = "keys"
93
- const typedKey = inputKey as FieldPath < UpdateSecretParams >
94
108
const { fields, append, remove } = useFieldArray < UpdateSecretParams > ( {
95
109
control,
96
- name : inputKey as ArrayPath < UpdateSecretParams > ,
110
+ name : "keys" ,
97
111
} )
98
112
99
113
return (
100
- < Dialog
101
- open = { showDialog }
102
- onOpenChange = { ( open ) => {
103
- if ( ! open ) {
104
- setSelectedSecret ( null )
105
- }
106
- setShowDialog ( open )
107
- } }
108
- >
114
+ < Dialog { ...props } >
109
115
{ children }
110
116
< DialogContent className = { className } >
111
117
< DialogHeader >
112
118
< DialogTitle > Edit secret</ DialogTitle >
113
- < DialogDescription >
114
- < b className = "inline-block" > NOTE</ b > : This feature is a work in
115
- progress.
119
+ < DialogDescription className = "flex flex-col" >
120
+ < span >
121
+ Leave a field blank to keep its existing value. You must update
122
+ all keys at once.
123
+ </ span >
116
124
</ DialogDescription >
117
125
</ DialogHeader >
118
126
< Form { ...methods } >
@@ -125,14 +133,18 @@ export function EditCredentialsDialog({
125
133
render = { ( ) => (
126
134
< FormItem >
127
135
< FormLabel className = "text-sm" > Name</ FormLabel >
136
+
128
137
< FormControl >
129
138
< Input
130
139
className = "text-sm"
131
- placeholder = "Name (snake case)"
140
+ placeholder = { selectedSecret ?. name || "Name" }
132
141
{ ...register ( "name" ) }
133
142
/>
134
143
</ FormControl >
135
144
< FormMessage />
145
+ < span className = "text-xs text-foreground/50" >
146
+ { ! methods . watch ( "name" ) && "Name will be left unchanged." }
147
+ </ span >
136
148
</ FormItem >
137
149
) }
138
150
/>
@@ -143,92 +155,101 @@ export function EditCredentialsDialog({
143
155
render = { ( ) => (
144
156
< FormItem >
145
157
< FormLabel className = "text-sm" > Description</ FormLabel >
146
- < FormDescription className = "text-sm" >
147
- A description for this secret.
148
- </ FormDescription >
149
158
< FormControl >
150
159
< Input
151
160
className = "text-sm"
152
- placeholder = "Description"
161
+ placeholder = {
162
+ selectedSecret ?. description || "Description"
163
+ }
153
164
{ ...register ( "description" ) }
154
165
/>
155
166
</ FormControl >
156
167
< FormMessage />
168
+ < span className = "text-xs text-foreground/50" >
169
+ { ! methods . watch ( "description" ) &&
170
+ "Description will be left unchanged." }
171
+ </ span >
157
172
</ FormItem >
158
173
) }
159
174
/>
160
- < FormField
161
- key = { inputKey }
162
- control = { control }
163
- name = { typedKey }
164
- render = { ( ) => (
165
- < FormItem >
166
- < FormLabel className = "text-sm" > Keys</ FormLabel >
167
- < div className = "flex flex-col space-y-2" >
168
- { fields . map ( ( field , index ) => {
169
- return (
170
- < div
171
- key = { `${ field . id } .${ index } ` }
172
- className = "flex w-full items-center gap-2"
173
- >
175
+ < FormItem >
176
+ < FormLabel className = "text-sm" > Keys</ FormLabel >
177
+
178
+ { fields . length > 0 &&
179
+ fields . map ( ( keysItem , index ) => (
180
+ < div
181
+ key = { keysItem . id }
182
+ className = "flex items-center justify-between"
183
+ >
184
+ < FormField
185
+ key = { `keys.${ index } .key` }
186
+ control = { control }
187
+ name = { `keys.${ index } .key` }
188
+ render = { ( { field } ) => (
189
+ < FormItem >
174
190
< FormControl >
175
191
< Input
176
192
id = { `key-${ index } ` }
177
193
className = "text-sm"
178
- { ...register (
179
- `${ inputKey } .${ index } .key` as const ,
180
- {
181
- required : true ,
182
- }
183
- ) }
184
- placeholder = "Key"
185
- />
186
- </ FormControl >
187
- < FormControl >
188
- < Input
189
- id = { `value-${ index } ` }
190
- className = "text-sm"
191
- { ...register (
192
- `${ inputKey } .${ index } .value` as const ,
193
- {
194
- required : true ,
195
- }
196
- ) }
197
- placeholder = "••••••••••••••••"
198
- type = "password"
194
+ placeholder = { "Key" }
195
+ { ...field }
199
196
/>
200
197
</ FormControl >
198
+ < FormMessage />
199
+ </ FormItem >
200
+ ) }
201
+ />
201
202
202
- < Button
203
- type = "button"
204
- variant = "ghost"
205
- onClick = { ( ) => remove ( index ) }
206
- disabled = { fields . length === 1 }
207
- >
208
- < Trash2Icon className = "size-3.5" />
209
- </ Button >
210
- </ div >
211
- )
212
- } ) }
203
+ < FormField
204
+ key = { `keys.${ index } .value` }
205
+ control = { control }
206
+ name = { `keys.${ index } .value` }
207
+ render = { ( { field } ) => (
208
+ < FormItem >
209
+ < div className = "flex flex-col space-y-2" >
210
+ < FormControl >
211
+ < Input
212
+ id = { `value-${ index } ` }
213
+ className = "text-sm"
214
+ placeholder = "••••••••••••••••"
215
+ type = "password"
216
+ { ...field }
217
+ />
218
+ </ FormControl >
219
+ </ div >
220
+ < FormMessage />
221
+ </ FormItem >
222
+ ) }
223
+ />
213
224
< Button
214
225
type = "button"
215
- variant = "outline"
216
- onClick = { ( ) => append ( { key : "" , value : "" } ) }
217
- className = "space-x-2 text-xs"
226
+ variant = "ghost"
227
+ onClick = { ( ) => remove ( index ) }
218
228
>
219
- < PlusCircle className = "mr-2 size-4" />
220
- Add Item
229
+ < Trash2Icon className = "size-3.5" />
221
230
</ Button >
222
231
</ div >
223
- < FormMessage />
224
- </ FormItem >
225
- ) }
226
- />
232
+ ) ) }
233
+ </ FormItem >
234
+ < Button
235
+ type = "button"
236
+ variant = "outline"
237
+ onClick = { ( ) => append ( { key : "" , value : "" } ) }
238
+ className = "w-full space-x-2 text-xs text-foreground/80"
239
+ >
240
+ < PlusCircle className = "mr-2 size-4" />
241
+ Add Item
242
+ </ Button >
243
+ { fields . length === 0 && (
244
+ < span className = "text-xs text-foreground/50" >
245
+ Secrets will be left unchanged.
246
+ </ span >
247
+ ) }
227
248
< DialogFooter >
228
249
< DialogClose asChild >
229
250
< Button className = "ml-auto space-x-2" type = "submit" >
230
- < KeyRoundIcon className = "mr-2 size-4" />
231
- Create Secret
251
+ < SaveIcon className = "mr-2 size-4" />
252
+ Save
232
253
</ Button >
233
254
</ DialogClose >
234
255
</ DialogFooter >
0 commit comments