Skip to content

Commit 7581cf4

Browse files
committed
merge modalform thing into this one
2 parents b1dc605 + f17a1ac commit 7581cf4

File tree

8 files changed

+146
-198
lines changed

8 files changed

+146
-198
lines changed

app/components/form/ModalForm.tsx

Lines changed: 34 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -16,90 +16,67 @@ import { Modal, type ModalProps } from '~/ui/lib/Modal'
1616

1717
type ModalFormProps<TFieldValues extends FieldValues> = {
1818
form: UseFormReturn<TFieldValues>
19-
/**
20-
* A function that returns the fields.
21-
*
22-
* Implemented as a function so we can pass `control` to the fields in the
23-
* calling code. We could do that internally with `cloneElement` instead, but
24-
* then in the calling code, the field would not infer `TFieldValues` and
25-
* constrain the `name` prop to paths in the values object.
26-
*/
2719
children: ReactNode
28-
resourceName: string
2920
/** Must be provided with a reason describing why it's disabled */
3021
submitDisabled?: string
31-
22+
onSubmit: (values: TFieldValues) => void
23+
submitLabel: string
3224
// require loading and error so we can't forget to hook them up. there are a
3325
// few forms that don't need them, so we'll use dummy values
3426

3527
/** Error from the API call */
3628
submitError: ApiError | null
3729
loading: boolean
38-
39-
/** Only needed if you need to override the default title (Create/Edit ${resourceName}) */
40-
subtitle?: ReactNode
41-
onSubmit: (values: TFieldValues) => void
42-
43-
submitLabel?: string
4430
} & Omit<ModalProps, 'isOpen'>
4531

4632
export function ModalForm<TFieldValues extends FieldValues>({
4733
form,
4834
children,
4935
onDismiss,
50-
resourceName,
5136
submitDisabled,
5237
submitError,
5338
title,
5439
onSubmit,
5540
submitLabel = 'Save',
5641
loading,
57-
subtitle,
5842
width = 'medium',
5943
overlay = true,
6044
}: ModalFormProps<TFieldValues>) {
6145
const id = useId()
62-
6346
const { isSubmitting } = form.formState
64-
65-
const modalTitle = title || `Create ${resourceName}`
66-
6747
return (
68-
<>
69-
<Modal
70-
isOpen
48+
<Modal isOpen onDismiss={onDismiss} title={title} width={width} overlay={overlay}>
49+
<Modal.Body>
50+
<Modal.Section>
51+
{submitError && (
52+
<Message variant="error" title="Error" content={submitError.message} />
53+
)}
54+
<form
55+
id={id}
56+
className="ox-form"
57+
autoComplete="off"
58+
onSubmit={(e) => {
59+
if (!onSubmit) return
60+
// This modal being in a portal doesn't prevent the submit event
61+
// from bubbling up out of the portal. Normally that's not a
62+
// problem, but sometimes (e.g., instance create) we render the
63+
// SideModalForm from inside another form, in which case submitting
64+
// the inner form submits the outer form unless we stop propagation
65+
e.stopPropagation()
66+
form.handleSubmit(onSubmit)(e)
67+
}}
68+
>
69+
{children}
70+
</form>
71+
</Modal.Section>
72+
</Modal.Body>
73+
<Modal.Footer
7174
onDismiss={onDismiss}
72-
title={modalTitle}
73-
width={width}
74-
overlay={overlay}
75-
>
76-
<Modal.Body>
77-
<Modal.Section>
78-
{subtitle && <div className="mb-4">{subtitle}</div>}
79-
{submitError && (
80-
<Message variant="error" title="Error" content={submitError.message} />
81-
)}
82-
<form
83-
id={id}
84-
className="ox-form"
85-
autoComplete="off"
86-
onSubmit={(e) => {
87-
if (!onSubmit) return
88-
form.handleSubmit(onSubmit)(e)
89-
}}
90-
>
91-
{children}
92-
</form>
93-
</Modal.Section>
94-
</Modal.Body>
95-
<Modal.Footer
96-
onDismiss={onDismiss}
97-
formId={id}
98-
actionText={submitLabel}
99-
disabled={!!submitDisabled}
100-
actionLoading={loading || isSubmitting}
101-
/>
102-
</Modal>
103-
</>
75+
formId={id}
76+
actionText={submitLabel}
77+
disabled={!!submitDisabled}
78+
actionLoading={loading || isSubmitting}
79+
/>
80+
</Modal>
10481
)
10582
}

app/components/form/SideModalForm.tsx

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,6 @@ type EditFormProps = {
3030

3131
type SideModalFormProps<TFieldValues extends FieldValues> = {
3232
form: UseFormReturn<TFieldValues>
33-
/**
34-
* A function that returns the fields.
35-
*
36-
* Implemented as a function so we can pass `control` to the fields in the
37-
* calling code. We could do that internally with `cloneElement` instead, but
38-
* then in the calling code, the field would not infer `TFieldValues` and
39-
* constrain the `name` prop to paths in the values object.
40-
*/
4133
children: ReactNode
4234
onDismiss: () => void
4335
resourceName: string

app/forms/disk-attach.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,6 @@ export function AttachDiskModalForm({
6363
<ModalForm
6464
form={form}
6565
onDismiss={onDismiss}
66-
resourceName="disk"
6766
submitLabel="Attach disk"
6867
submitError={submitError}
6968
loading={loading}

app/pages/SiloImagesPage.tsx

Lines changed: 77 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88
import { createColumnHelper } from '@tanstack/react-table'
99
import { useCallback, useMemo, useState } from 'react'
10-
import { useForm, type FieldValues } from 'react-hook-form'
10+
import { useForm } from 'react-hook-form'
1111
import { Outlet } from 'react-router'
1212

1313
import {
@@ -24,6 +24,7 @@ import { DocsPopover } from '~/components/DocsPopover'
2424
import { ComboboxField } from '~/components/form/fields/ComboboxField'
2525
import { toImageComboboxItem } from '~/components/form/fields/ImageSelectField'
2626
import { ListboxField } from '~/components/form/fields/ListboxField'
27+
import { ModalForm } from '~/components/form/ModalForm'
2728
import { HL } from '~/components/HL'
2829
import { confirmDelete } from '~/stores/confirm-delete'
2930
import { addToast } from '~/stores/toast'
@@ -35,7 +36,6 @@ import { Button } from '~/ui/lib/Button'
3536
import { toComboboxItems } from '~/ui/lib/Combobox'
3637
import { EmptyMessage } from '~/ui/lib/EmptyMessage'
3738
import { Message } from '~/ui/lib/Message'
38-
import { Modal } from '~/ui/lib/Modal'
3939
import { PageHeader, PageTitle } from '~/ui/lib/PageHeader'
4040
import { TableActions } from '~/ui/lib/Table'
4141
import { docLinks } from '~/util/links'
@@ -129,7 +129,7 @@ type Values = { project: string | null; image: string | null }
129129
const defaultValues: Values = { project: null, image: null }
130130

131131
const 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+
209208
const 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

Comments
 (0)