Skip to content
35 changes: 27 additions & 8 deletions app/components/form/SideModalForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*
* Copyright Oxide Computer Company
*/
import { useEffect, type ReactNode } from 'react'
import { useEffect, useId, type ReactNode } from 'react'
import type { FieldValues, UseFormReturn } from 'react-hook-form'
import { useNavigationType } from 'react-router-dom'

Expand All @@ -14,8 +14,19 @@ import type { ApiError } from '@oxide/api'
import { Button } from '~/ui/lib/Button'
import { SideModal } from '~/ui/lib/SideModal'

type CreateFormProps = {
formType: 'create'
/** Only needed if you need to override the default button text (`Create ${resourceName}`) */
submitLabel?: string
}

type EditFormProps = {
formType: 'edit'
/** Not permitted, as all edit form buttons should read `Update ${resourceName}` */
submitLabel?: never
}

type SideModalFormProps<TFieldValues extends FieldValues> = {
id: string
form: UseFormReturn<TFieldValues>
/**
* A function that returns the fields.
Expand All @@ -27,16 +38,17 @@ type SideModalFormProps<TFieldValues extends FieldValues> = {
*/
children: ReactNode
onDismiss: () => void
resourceName: string
/** Must be provided with a reason describing why it's disabled */
submitDisabled?: string
/** Error from the API call */
submitError: ApiError | null
loading?: boolean
title: string
/** Only needed if you need to override the default title (Create/Edit ${resourceName}) */
title?: string
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’d add a doc comment here explaining that you only need this when you want to override the default create or edit title. Same with submitLabel I think.

subtitle?: ReactNode
onSubmit?: (values: TFieldValues) => void
submitLabel?: string
}
} & (CreateFormProps | EditFormProps)

/**
* Only animate the modal in when we're navigating by a client-side click.
Expand All @@ -49,10 +61,11 @@ export function useShouldAnimateModal() {
}

export function SideModalForm<TFieldValues extends FieldValues>({
id,
form,
formType,
children,
onDismiss,
resourceName,
submitDisabled,
submitError,
title,
Expand All @@ -61,6 +74,7 @@ export function SideModalForm<TFieldValues extends FieldValues>({
loading,
subtitle,
}: SideModalFormProps<TFieldValues>) {
const id = useId()
const { isSubmitting } = form.formState

useEffect(() => {
Expand All @@ -70,11 +84,16 @@ export function SideModalForm<TFieldValues extends FieldValues>({
}
}, [submitError, form])

const label =
formType === 'edit'
? `Update ${resourceName}`
: submitLabel || title || `Create ${resourceName}`

return (
<SideModal
onDismiss={onDismiss}
isOpen
title={title}
title={title || `${formType === 'edit' ? 'Edit' : 'Create'} ${resourceName}`}
animate={useShouldAnimateModal()}
subtitle={subtitle}
errors={submitError ? [submitError.message] : []}
Expand Down Expand Up @@ -111,7 +130,7 @@ export function SideModalForm<TFieldValues extends FieldValues>({
loading={loading || isSubmitting}
form={id}
>
{submitLabel || title}
{label}
</Button>
)}
</SideModal.Footer>
Expand Down
1 change: 1 addition & 0 deletions app/forms/access-util.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export type AddRoleModalProps = {
}

export type EditRoleModalProps = AddRoleModalProps & {
name?: string
identityId: string
identityType: IdentityType
defaultValues: { roleName: RoleKey }
Expand Down
5 changes: 3 additions & 2 deletions app/forms/disk-attach.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,10 @@ export function AttachDiskSideModalForm({

return (
<SideModalForm
id="form-disk-attach"
title="Attach Disk"
form={form}
formType="create"
resourceName="disk"
title="Attach Disk"
onSubmit={onSubmit}
loading={loading}
submitError={submitError}
Expand Down
4 changes: 2 additions & 2 deletions app/forms/disk-create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,9 @@ export function CreateDiskSideModalForm({

return (
<SideModalForm
id="create-disk-form"
title="Create Disk"
form={form}
formType="create"
resourceName="disk"
onDismiss={() => onDismiss(navigate)}
onSubmit={({ size, ...rest }) => {
const body = { size: size * GiB, ...rest }
Expand Down
5 changes: 3 additions & 2 deletions app/forms/firewall-rules-create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -504,9 +504,10 @@ export function CreateFirewallRuleForm({

return (
<SideModalForm
id="create-firewall-rule-form"
title="Add firewall rule"
form={form}
formType="create"
resourceName="rule"
title="Add firewall rule"
onDismiss={onDismiss}
onSubmit={(values) => {
// TODO: this silently overwrites existing rules with the current name.
Expand Down
5 changes: 2 additions & 3 deletions app/forms/firewall-rules-edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,9 @@ export function EditFirewallRuleForm({

return (
<SideModalForm
id="create-firewall-rule-form"
title="Edit rule"
form={form}
formType="edit"
resourceName="rule"
onDismiss={onDismiss}
onSubmit={(values) => {
// note different filter logic from create: filter out the rule with the
Expand All @@ -86,7 +86,6 @@ export function EditFirewallRuleForm({
// validateOnBlur
loading={updateRules.isPending}
submitError={updateRules.error}
submitLabel="Update rule"
>
<CommonFields error={updateRules.error} control={form.control} />
</SideModalForm>
Expand Down
4 changes: 2 additions & 2 deletions app/forms/floating-ip-create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,9 @@ export function CreateFloatingIpSideModalForm() {

return (
<SideModalForm
id="create-floating-ip-form"
title="Create Floating IP"
form={form}
formType="create"
resourceName="floating IP"
onDismiss={() => navigate(pb.floatingIps(projectSelector))}
onSubmit={({ ip, ...rest }) => {
createFloatingIp.mutate({
Expand Down
5 changes: 2 additions & 3 deletions app/forms/floating-ip-edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,9 @@ export function EditFloatingIpSideModalForm() {

return (
<SideModalForm
id="edit-floating-ip-form"
form={form}
title="Edit floating IP"
formType="edit"
resourceName="floating IP"
onDismiss={onDismiss}
onSubmit={({ name, description }) => {
editFloatingIp.mutate({
Expand All @@ -68,7 +68,6 @@ export function EditFloatingIpSideModalForm() {
}}
loading={editFloatingIp.isPending}
submitError={editFloatingIp.error}
submitLabel="Save changes"
>
<NameField name="name" control={form.control} />
<DescriptionField name="description" control={form.control} />
Expand Down
4 changes: 2 additions & 2 deletions app/forms/idp/create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,9 @@ export function CreateIdpSideModalForm() {

return (
<SideModalForm
id="create-idp-form"
form={form}
title="Create identity provider"
formType="create"
resourceName="identity provider"
onDismiss={onDismiss}
onSubmit={async ({
signingKeypair,
Expand Down
3 changes: 2 additions & 1 deletion app/forms/idp/edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,9 @@ export function EditIdpSideModalForm() {

return (
<SideModalForm
id="edit-idp-form"
form={form}
formType="edit"
resourceName="identity provider"
title="Identity provider"
onDismiss={onDismiss}
subtitle={
Expand Down
4 changes: 2 additions & 2 deletions app/forms/image-edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,9 @@ export function EditImageSideModalForm({

return (
<SideModalForm
id="edit-project-image-form"
form={form}
title={`${type} image`}
formType="edit"
resourceName={type === 'Project' ? 'project image' : 'silo image'}
onDismiss={() => navigate(dismissLink)}
subtitle={
<ResourceLabel>
Expand Down
5 changes: 3 additions & 2 deletions app/forms/image-from-snapshot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,10 @@ export function CreateImageFromSnapshotSideModalForm() {

return (
<SideModalForm
id="create-image-from-snapshot-form"
form={form}
title={`Create image from snapshot`}
formType="create"
resourceName="image"
title="Create image from snapshot"
submitLabel="Create image"
onDismiss={onDismiss}
onSubmit={(body) =>
Expand Down
3 changes: 2 additions & 1 deletion app/forms/image-upload.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -471,8 +471,9 @@ export function CreateImageSideModalForm() {

return (
<SideModalForm
id="upload-image-form"
form={form}
formType="create"
resourceName="image"
title="Upload image"
onDismiss={backToImages}
onSubmit={async (values) => {
Expand Down
4 changes: 2 additions & 2 deletions app/forms/ip-pool-create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,9 @@ export function CreateIpPoolSideModalForm() {

return (
<SideModalForm
id="create-pool-form"
form={form}
title="Create IP pool"
formType="create"
resourceName="IP pool"
onDismiss={onDismiss}
onSubmit={({ name, description }) => {
createPool.mutate({ body: { name, description } })
Expand Down
5 changes: 2 additions & 3 deletions app/forms/ip-pool-edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,16 +49,15 @@ export function EditIpPoolSideModalForm() {

return (
<SideModalForm
id="edit-pool-form"
form={form}
title="Edit IP pool"
formType="edit"
resourceName="IP pool"
onDismiss={onDismiss}
onSubmit={({ name, description }) => {
editPool.mutate({ path: poolSelector, body: { name, description } })
}}
loading={editPool.isPending}
submitError={editPool.error}
submitLabel="Save changes"
>
<NameField name="name" control={form.control} />
<DescriptionField name="description" control={form.control} />
Expand Down
3 changes: 2 additions & 1 deletion app/forms/ip-pool-range-add.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,9 @@ export function IpPoolAddRangeSideModalForm() {

return (
<SideModalForm
id="add-ip-range-form"
form={form}
formType="create"
resourceName="IP range"
title="Add IP range"
onDismiss={onDismiss}
onSubmit={(body) => addRange.mutate({ path: { pool }, body })}
Expand Down
5 changes: 3 additions & 2 deletions app/forms/network-interface-create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,10 @@ export default function CreateNetworkInterfaceForm({

return (
<SideModalForm
id="create-network-interface-form"
title="Add network interface"
form={form}
formType="create"
resourceName="network interface"
title="Add network interface"
onDismiss={onDismiss}
onSubmit={onSubmit}
loading={loading}
Expand Down
5 changes: 2 additions & 3 deletions app/forms/network-interface-edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@ export default function EditNetworkInterfaceForm({

return (
<SideModalForm
id="edit-network-interface-form"
title="Edit network interface"
form={form}
formType="edit"
resourceName="network interface"
onDismiss={onDismiss}
onSubmit={(body) => {
const interfaceName = defaultValues.name
Expand All @@ -56,7 +56,6 @@ export default function EditNetworkInterfaceForm({
}}
loading={editNetworkInterface.isPending}
submitError={editNetworkInterface.error}
submitLabel="Save changes"
>
<NameField name="name" control={form.control} />
<DescriptionField name="description" control={form.control} />
Expand Down
10 changes: 6 additions & 4 deletions app/forms/project-access.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,9 @@ export function ProjectAccessAddUserSideModal({ onDismiss, policy }: AddRoleModa
return (
<SideModalForm
title="Add user or group"
id="project-access-add-user"
resourceName="role"
form={form}
formType="create"
onSubmit={({ identityId, roleName }) => {
// can't happen because roleName is validated not to be '', but TS
// wants to be sure
Expand Down Expand Up @@ -82,6 +83,7 @@ export function ProjectAccessAddUserSideModal({ onDismiss, policy }: AddRoleModa

export function ProjectAccessEditUserSideModal({
onDismiss,
name,
identityId,
identityType,
policy,
Expand All @@ -102,9 +104,10 @@ export function ProjectAccessEditUserSideModal({
return (
<SideModalForm
// TODO: show user name in header or SOMEWHERE
title="Change user role"
id="project-access-edit-user"
form={form}
formType="edit"
resourceName="role"
title={`Change role for ${name}`}
onSubmit={({ roleName }) => {
updatePolicy.mutate({
path: { project },
Expand All @@ -113,7 +116,6 @@ export function ProjectAccessEditUserSideModal({
}}
loading={updatePolicy.isPending}
submitError={updatePolicy.error}
submitLabel="Update role"
onDismiss={onDismiss}
>
<ListboxField
Expand Down
4 changes: 2 additions & 2 deletions app/forms/project-create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@ export function CreateProjectSideModalForm() {

return (
<SideModalForm
id="create-project-form"
form={form}
title="Create project"
formType="create"
resourceName="project"
onDismiss={onDismiss}
onSubmit={({ name, description }) => {
createProject.mutate({ body: { name, description } })
Expand Down
Loading