Files
supabase/apps/studio/components/interfaces/Organization/NewOrg/OrganizationDetailsFields.tsx
Danny White 51ecfbb86d studio(chore): reuse org details fields in AWS marketplace flow (#46087)
## What kind of change does this PR introduce?

Refactor. Resolves FE-3216.

## What is the current behavior?

The AWS Marketplace create-organisation dialog owns its own copy of the
organisation name, type, and company-size form fields. It’s duplicative
and has drifted from the normal `/new` organisation form, making the AWS
flow harder to keep aligned.

This is stacked on #46058.

## What is the new behavior?

- Extracts the shared organisation details schema, defaults, option
constants, and fields from the normal `NewOrgForm`
- Reuses those shared fields in both the full organisation creation form
and the AWS Marketplace create-and-link dialog
- Keeps the AWS Marketplace flow anchored in the onboarding interstitial
rather than routing through `/new`
- Keeps the AWS-specific buyer ID, AWS-managed organisation endpoint,
create-and-link success state, and modal dismissal behaviour

| Before | After |
| --- | --- |
| <img width="1024" height="759" alt="Link AWS Marketplace
Supabase-3742FEDF-53BD-4E80-926D-498B2EA94773"
src="https://github.com/user-attachments/assets/617ee422-1cf0-4858-801b-a4ee5ee402c9"
/> | <img width="1024" height="759" alt="Link AWS Marketplace
Supabase-0FEE2292-CB9F-43AA-B131-B6A549890970"
src="https://github.com/user-attachments/assets/ff017468-f8ac-469a-bb17-eea07842306f"
/> |

## Additional context

The shared field extraction is intentionally limited to organisation
details. Billing, plan selection, spend cap, Stripe, captcha, and the
`/new/[slug]` redirect behaviour stay owned by `NewOrgForm`; AWS
Marketplace keeps its separate create-and-link container because AWS
owns the billing contract.

## Validation

- `pnpm --filter studio exec vitest --run
tests/pages/aws-marketplace-onboarding.test.tsx`
- `pnpm --filter studio lint:ratchet --rule no-restricted-exports`
- `git diff --check`

Full Studio typecheck was also run, but it currently fails on existing
unrelated repo-wide React/implicit-any errors outside this diff.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Improved AWS Marketplace organization creation dialog to prevent
accidental closure while the creation process is in progress.

* **Improvements**
* Standardized organization details form handling across different
organization creation flows for improved consistency and user
experience.

* **Tests**
* Added comprehensive test coverage for the AWS Marketplace organization
creation workflow, including form submission, validation, and state
transitions.

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/supabase/supabase/pull/46087?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)

<!-- review_stack_entry_end -->

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-05-22 14:10:07 +10:00

171 lines
4.6 KiB
TypeScript

import type { ReactNode } from 'react'
import type { Control, FieldPath, FieldValues } from 'react-hook-form'
import {
FormControl,
FormField,
Input,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { z } from 'zod'
export const ORG_KIND_TYPES = {
PERSONAL: 'Personal',
EDUCATIONAL: 'Educational',
STARTUP: 'Startup',
AGENCY: 'Agency',
COMPANY: 'Company',
UNDISCLOSED: 'N/A',
}
export const ORG_KIND_DEFAULT = 'PERSONAL'
export const ORG_SIZE_TYPES = {
'1': '1 - 10',
'10': '10 - 49',
'50': '50 - 99',
'100': '100 - 299',
'300': 'More than 300',
}
export const ORG_SIZE_DEFAULT = '1'
export type OrgKind = keyof typeof ORG_KIND_TYPES
export type OrgSize = keyof typeof ORG_SIZE_TYPES
const orgKindValues = [
'PERSONAL',
'EDUCATIONAL',
'STARTUP',
'AGENCY',
'COMPANY',
'UNDISCLOSED',
] as const
const orgSizeValues = ['1', '10', '50', '100', '300'] as const
export const organizationDetailsSchema = z.object({
name: z.string().trim().min(1, 'Organization name is required'),
kind: z
.string()
.transform((val) => val.toUpperCase())
.pipe(z.enum(orgKindValues)),
size: z.enum(orgSizeValues),
})
export type OrganizationDetailsFormValues = z.infer<typeof organizationDetailsSchema>
interface OrganizationDetailsFieldsProps<TFieldValues extends FieldValues> {
control: Control<TFieldValues>
kind: string
nameField?: FieldPath<TFieldValues>
kindField?: FieldPath<TFieldValues>
sizeField?: FieldPath<TFieldValues>
renderFieldWrapper?: (children: ReactNode, field: 'name' | 'kind' | 'size') => ReactNode
}
export const OrganizationDetailsFields = <TFieldValues extends FieldValues>({
control,
kind,
nameField = 'name' as FieldPath<TFieldValues>,
kindField = 'kind' as FieldPath<TFieldValues>,
sizeField = 'size' as FieldPath<TFieldValues>,
renderFieldWrapper = (children) => children,
}: OrganizationDetailsFieldsProps<TFieldValues>) => (
<>
{renderFieldWrapper(
<FormField
control={control}
name={nameField}
render={({ field }) => (
<FormItemLayout
label="Name"
layout="horizontal"
description="What's the name of your company or team? You can change this later."
>
<FormControl>
<Input
autoFocus
type="text"
placeholder="Organization name"
data-1p-ignore
data-lpignore="true"
data-form-type="other"
data-bwignore
{...field}
/>
</FormControl>
</FormItemLayout>
)}
/>,
'name'
)}
{renderFieldWrapper(
<FormField
control={control}
name={kindField}
render={({ field }) => (
<FormItemLayout
label="Type"
layout="horizontal"
description="What best describes your organization?"
>
<FormControl>
<Select value={field.value} onValueChange={field.onChange}>
<SelectTrigger className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
{Object.entries(ORG_KIND_TYPES).map(([key, label]) => (
<SelectItem key={key} value={key}>
{label}
</SelectItem>
))}
</SelectContent>
</Select>
</FormControl>
</FormItemLayout>
)}
/>,
'kind'
)}
{kind === 'COMPANY' &&
renderFieldWrapper(
<FormField
control={control}
name={sizeField}
render={({ field }) => (
<FormItemLayout
label="Company size"
layout="horizontal"
description="How many people are in your company?"
>
<FormControl>
<Select value={field.value} onValueChange={field.onChange}>
<SelectTrigger className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
{Object.entries(ORG_SIZE_TYPES).map(([key, label]) => (
<SelectItem key={key} value={key}>
{label}
</SelectItem>
))}
</SelectContent>
</Select>
</FormControl>
</FormItemLayout>
)}
/>,
'size'
)}
</>
)