Skip to content

InviteAssociateForm

Invite a colleague by email and one or more roles on the evolve-feature-form pattern: validation clean until the first submit, then live; the submit is never disabled. This root owns the <form> and its state, and InviteAssociateFormFields and InviteAssociateFormSubmit read it from context, so InviteAssociateDialog can put the actions in its own DialogFooter.

import { InviteAssociateForm } from "@evolve-storefront/feature-organization/invite-associate-form";

Renders inside OrganizationProvider.

Preview

DefaultOpen in Storybook ↗

Usage

Submitting empty marks the email invalid and asks for a role.

const invite = useInviteAssociate();
<InviteAssociateForm
roles={unit.availableRoles}
onValidSubmit={(values) => invite.mutate(values)}
>
<InviteAssociateFormFields />
<InviteAssociateFormSubmit pendingLabel={t("sending")}>{t("invite")}</InviteAssociateFormSubmit>
</InviteAssociateForm>;

Props

PropTypeDescription
rolesBusinessUnitRole[]
onValidSubmit(values: { email: string; roleIds: string[]; }) => void

Runs with the validated values on a valid submit — where the mutation goes.

childrenReactNode

The composed pieces — typically <InviteAssociateFormFields /> and a footer with <InviteAssociateFormSubmit>. The form owns state and the <form> element; the caller owns the layout, so a dialog keeps its own DialogFooter.

validationBehavior?"aria" | "native"

Default "aria": mark fields via ARIA rather than blocking native submit. The form is then noValidate, so an input's own constraint (type="email") never stops the submit with a browser tooltip that the field's error does not match.

isPending?boolean
error?string | null

Form-level error (e.g. a failed mutation), shown above the actions.

serverErrors?FormValidationErrors

Server-provided field errors, merged into the same display path.

Any other props go to the root element, like on a plain HTML element.

Change the layout

Pass children to InviteAssociateForm to arrange its parts yourself. Every part is exported from @evolve-storefront/feature-organization/invite-associate-form.

const invite = useInviteAssociate();
<InviteAssociateForm
roles={unit.availableRoles}
onValidSubmit={(values) => invite.mutate(values)}
>
<InviteAssociateFormFields />
<InviteAssociateFormSubmit pendingLabel={t("sending")}>{t("invite")}</InviteAssociateFormSubmit>
</InviteAssociateForm>;
InviteAssociateDialogModal invite form.
PropTypeDescription
openboolean
onOpenChange(open: boolean) => void
rolesBusinessUnitRole[]

The company's available roles (from useBusinessUnit().data.availableRoles).

defaultOpen?boolean
InviteAssociateFormContextThe context InviteAssociateForm fills.

No props of its own.

InviteAssociateFormFieldsThe email field and a checkbox per role, each with what that role can do, seeded from InviteAssociateForm's context.

The form-level error sits at the end of this part rather than in the footer, so it reads above the actions wherever the caller places them.

No props of its own.

InviteAssociateFormSubmitThe form's submit button — never disabled (per the evolve-feature-form pattern: submission is gated by validity in the form's onSubmit, not by a disabled control).
PropTypeDescription
pendingLabelReactNode

Shown while a submit is in flight.

isPending?boolean

The action is running. The button keeps its label, so its width and accessible name hold, shows a spinner and is aria-busy. It stays focusable rather than disabled, so focus is not dropped mid-save; clicks and the form submit it would trigger are ignored. Not supported with asChild.

variant?ButtonVariant
tone?ButtonTone
isIconOnly?boolean

The button has no visible label, so it is square. Structural rather than a size: it says the button has nothing to be wide for, and says nothing about how big — the brand's control tokens still decide that.

More

Not inferred from the children. A kebab menu is as often a glyph or an SVG as an <Icon>, and a visually-hidden label — the accessible thing to add — would break any :only-child predicate silently.

asChild?boolean
data-component?undefined

A button's identity is not a caller's to rename — see buttonProps. Typed away so a call site renaming it fails to compile rather than passing an attribute that is silently discarded.

Any other props go to the root element, like on a plain HTML element.

RoleCapabilitiesWhat a role lets an associate do, as one sentence read from its capabilities.
PropTypeDescription
capabilitiesstring[]

The role's capabilities, as BusinessUnitRole.capabilities holds them.

Any other props go to the root element, like on a plain HTML element.

Examples

PendingSubmitting: the button shows the pending label (never disabled).
// Straight off the mutation: the submit swaps its label and stays enabled.
<InviteAssociateForm roles={roles} isPending={invite.isPending} onValidSubmit={submit}>
<InviteAssociateFormFields />
<InviteAssociateFormSubmit pendingLabel={t("sending")}>{t("invite")}</InviteAssociateFormSubmit>
</InviteAssociateForm>;
Open in Storybook ↗
With ErrorA failed mutation surfaces a form-level error at the end of the fields.
// A form-level error, e.g. from a failed mutation, reads above the actions.
<InviteAssociateForm roles={roles} error={error} serverErrors={serverErrors} onValidSubmit={submit}>
<InviteAssociateFormFields />
<InviteAssociateFormSubmit pendingLabel={t("sending")}>{t("invite")}</InviteAssociateFormSubmit>
</InviteAssociateForm>;
Open in Storybook ↗