77 */
88import { createColumnHelper } from '@tanstack/react-table'
99import { useCallback , useMemo , useState } from 'react'
10- import { useForm , type FieldValues } from 'react-hook-form'
10+ import { useForm } from 'react-hook-form'
1111import { Outlet } from 'react-router'
1212
1313import {
@@ -24,6 +24,7 @@ import { DocsPopover } from '~/components/DocsPopover'
2424import { ComboboxField } from '~/components/form/fields/ComboboxField'
2525import { toImageComboboxItem } from '~/components/form/fields/ImageSelectField'
2626import { ListboxField } from '~/components/form/fields/ListboxField'
27+ import { ModalForm } from '~/components/form/ModalForm'
2728import { HL } from '~/components/HL'
2829import { confirmDelete } from '~/stores/confirm-delete'
2930import { addToast } from '~/stores/toast'
@@ -35,7 +36,6 @@ import { Button } from '~/ui/lib/Button'
3536import { toComboboxItems } from '~/ui/lib/Combobox'
3637import { EmptyMessage } from '~/ui/lib/EmptyMessage'
3738import { Message } from '~/ui/lib/Message'
38- import { Modal } from '~/ui/lib/Modal'
3939import { PageHeader , PageTitle } from '~/ui/lib/PageHeader'
4040import { TableActions } from '~/ui/lib/Table'
4141import { docLinks } from '~/util/links'
@@ -129,7 +129,7 @@ type Values = { project: string | null; image: string | null }
129129const defaultValues : Values = { project : null , image : null }
130130
131131const PromoteImageModal = ( { onDismiss } : { onDismiss : ( ) => void } ) => {
132- const { control , handleSubmit , watch , resetField } = useForm ( { defaultValues } )
132+ const form = useForm ( { defaultValues } )
133133
134134 const queryClient = useApiQueryClient ( )
135135
@@ -146,7 +146,7 @@ const PromoteImageModal = ({ onDismiss }: { onDismiss: () => void }) => {
146146
147147 const projects = useApiQuery ( 'projectList' , { } )
148148 const projectItems = useMemo ( ( ) => toComboboxItems ( projects . data ?. items ) , [ projects . data ] )
149- const selectedProject = watch ( 'project' )
149+ const selectedProject = form . watch ( 'project' )
150150
151151 // can only fetch images if a project is selected
152152 const images = useApiQuery (
@@ -159,74 +159,70 @@ const PromoteImageModal = ({ onDismiss }: { onDismiss: () => void }) => {
159159 [ images . data ]
160160 )
161161
162- const onSubmit = ( { image, project } : Values ) => {
163- if ( ! image || ! project ) return
164- promoteImage . mutate ( { path : { image } } )
165- }
166-
167162 return (
168- < Modal isOpen onDismiss = { onDismiss } title = "Promote image" >
169- < Modal . Body >
170- < Modal . Section >
171- < form autoComplete = "off" onSubmit = { handleSubmit ( onSubmit ) } className = "space-y-4" >
172- < ComboboxField
173- placeholder = "Select a project"
174- name = "project"
175- label = "Project"
176- items = { projectItems }
177- onChange = { ( ) => {
178- resetField ( 'image' ) // reset image field when the project changes
179- } }
180- isLoading = { projects . isPending }
181- required
182- control = { control }
183- />
184- < ListboxField
185- control = { control }
186- name = "image"
187- placeholder = "Select an image"
188- items = { imageItems }
189- isLoading = { images . isPending }
190- required
191- disabled = { ! selectedProject }
192- />
193- </ form >
194- < Message
195- variant = "info"
196- content = "Once an image has been promoted it is visible to all projects in a silo"
197- />
198- </ Modal . Section >
199- </ Modal . Body >
200- < Modal . Footer
201- onDismiss = { onDismiss }
202- onAction = { handleSubmit ( onSubmit ) }
203- actionText = "Promote"
163+ < ModalForm
164+ title = "Promote image"
165+ form = { form }
166+ loading = { promoteImage . isPending }
167+ submitError = { promoteImage . error }
168+ onSubmit = { ( { image, project } ) => {
169+ if ( ! image || ! project ) return // shouldn't happen because of validation
170+ promoteImage . mutate ( { path : { image } } )
171+ } }
172+ onDismiss = { onDismiss }
173+ submitLabel = "Promote"
174+ >
175+ < ComboboxField
176+ placeholder = "Select a project"
177+ name = "project"
178+ label = "Project"
179+ items = { projectItems }
180+ onChange = { ( ) => {
181+ form . resetField ( 'image' ) // reset image field when the project changes
182+ } }
183+ isLoading = { projects . isPending }
184+ required
185+ control = { form . control }
186+ />
187+ < ListboxField
188+ control = { form . control }
189+ name = "image"
190+ placeholder = "Select an image"
191+ items = { imageItems }
192+ isLoading = { images . isPending }
193+ required
194+ disabled = { ! selectedProject }
204195 />
205- </ Modal >
196+ < Message
197+ variant = "info"
198+ content = "Once an image has been promoted it is visible to all projects in a silo"
199+ />
200+ </ ModalForm >
206201 )
207202}
208203
204+ type DemoteFormValues = {
205+ project : string | undefined
206+ }
207+
209208const DemoteImageModal = ( {
210209 onDismiss,
211210 image,
212211} : {
213212 onDismiss : ( ) => void
214213 image : Image
215214} ) => {
216- const { control, handleSubmit, watch } = useForm ( )
215+ const defaultValues : DemoteFormValues = { project : undefined }
216+ const form = useForm ( { defaultValues } )
217217
218- const selectedProject : string | undefined = watch ( 'project' )
218+ const selectedProject : string | undefined = form . watch ( 'project' )
219219
220220 const queryClient = useApiQueryClient ( )
221221
222222 const demoteImage = useApiMutation ( 'imageDemote' , {
223223 onSuccess ( data ) {
224224 addToast ( {
225- content : (
226- < >
227- Image < HL > { data . name } </ HL > demoted
228- </ >
229- ) ,
225+ content : < > Image < HL > { data . name } </ HL > demoted</ > , // prettier-ignore
230226 cta : selectedProject
231227 ? {
232228 text : `View images in ${ selectedProject } ` ,
@@ -243,51 +239,40 @@ const DemoteImageModal = ({
243239 onSettled : onDismiss ,
244240 } )
245241
246- const onSubmit = ( data : FieldValues ) => {
247- demoteImage . mutate ( { path : { image : image . id } , query : { project : data . project } } )
248- }
249-
250242 const projects = useApiQuery ( 'projectList' , { } )
251243 const projectItems = useMemo ( ( ) => toComboboxItems ( projects . data ?. items ) , [ projects . data ] )
252244
253245 return (
254- < Modal isOpen onDismiss = { onDismiss } title = "Demote image" >
255- < Modal . Body >
256- < Modal . Section >
257- < form
258- autoComplete = "off"
259- onSubmit = { ( e ) => {
260- e . stopPropagation ( )
261- handleSubmit ( onSubmit ) ( e )
262- } }
263- className = "space-y-4"
264- >
265- < p >
266- Demoting: < span className = "text-sans-semi-md text-raise" > { image . name } </ span >
267- </ p >
246+ < ModalForm
247+ title = "Demote image"
248+ form = { form }
249+ loading = { demoteImage . isPending }
250+ submitError = { demoteImage . error }
251+ onSubmit = { ( { project } ) => {
252+ if ( ! project ) return // shouldn't happen because of validation
253+ demoteImage . mutate ( { path : { image : image . id } , query : { project } } )
254+ } }
255+ onDismiss = { onDismiss }
256+ submitLabel = "Demote"
257+ >
258+ < p >
259+ Demoting: < span className = "text-sans-semi-md text-raise" > { image . name } </ span >
260+ </ p >
268261
269- < Message
270- variant = "info"
271- content = "Once an image has been demoted it is only visible to the project that it is demoted into. This will not affect disks already created with the image."
272- />
262+ < Message
263+ variant = "info"
264+ content = "Once an image has been demoted it is only visible within the project that it is demoted into. This will not affect disks already created with the image."
265+ />
273266
274- < ComboboxField
275- placeholder = "Select project for image"
276- name = "project"
277- label = "Project"
278- items = { projectItems }
279- isLoading = { projects . isPending }
280- required
281- control = { control }
282- />
283- </ form >
284- </ Modal . Section >
285- </ Modal . Body >
286- < Modal . Footer
287- onDismiss = { onDismiss }
288- onAction = { handleSubmit ( onSubmit ) }
289- actionText = "Demote"
267+ < ComboboxField
268+ placeholder = "Select project for image"
269+ name = "project"
270+ label = "Project"
271+ items = { projectItems }
272+ isLoading = { projects . isPending }
273+ required
274+ control = { form . control }
290275 />
291- </ Modal >
276+ </ ModalForm >
292277 )
293278}
0 commit comments