Skip to content

EditRolesForm

Change an associate's role on the evolve-feature-form pattern: a RAC Form whose validationErrors drive display, clean until the first submit then live, and a submit button that is never disabled.

import { EditRolesForm } from "@evolve-storefront/feature-organization/edit-roles-form";

Renders inside OrganizationProvider.

Design notes

Compound: this root owns the <form> and its state and seeds EditRolesFormContext, while EditRolesFormFields and EditRolesFormSubmit read it from there. That is what lets EditRolesDialog put cancel and submit in a DialogFooter without this form importing anything dialog-specific.

Preview

DefaultOpen in Storybook ↗

Usage

Nothing pre-selected — submitting marks the field invalid.

const update = useUpdateAssociateRoles();
<EditRolesForm
roles={unit.availableRoles}
onValidSubmit={({ roleId }) => update.mutate({ customerId: associate.id, roleIds: [roleId] })}
>
<EditRolesFormFields />
<EditRolesFormSubmit pendingLabel={t("saving")}>{t("save")}</EditRolesFormSubmit>
</EditRolesForm>;

Props

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

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

childrenReactNode

The composed pieces — typically <EditRolesFormFields /> and a footer with <EditRolesFormSubmit>. 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.

defaultRoleId?string
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 EditRolesForm to arrange its parts yourself. Every part is exported from @evolve-storefront/feature-organization/edit-roles-form.

const update = useUpdateAssociateRoles();
<EditRolesForm
roles={unit.availableRoles}
onValidSubmit={({ roleId }) => update.mutate({ customerId: associate.id, roleIds: [roleId] })}
>
<EditRolesFormFields />
<EditRolesFormSubmit pendingLabel={t("saving")}>{t("save")}</EditRolesFormSubmit>
</EditRolesForm>;
EditRolesDialogModal role editor.
PropTypeDescription
openboolean
onOpenChange(open: boolean) => void
associateBusinessUnitAssociate
rolesBusinessUnitRole[]
defaultOpen?boolean
EditRolesFormContextThe context EditRolesForm fills.

No props of its own.

EditRolesFormFieldsThe role field, seeded from EditRolesForm'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.

EditRolesFormSubmitThe 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.

Examples

SeededSeeded with the associate's current role.
// Seed the select with the role the associate holds today.
<EditRolesForm roles={roles} defaultRoleId={associate.roles[0]?.id} onValidSubmit={submit}>
<EditRolesFormFields />
<EditRolesFormSubmit pendingLabel={t("saving")}>{t("save")}</EditRolesFormSubmit>
</EditRolesForm>;
Open in Storybook ↗
PendingSubmitting: the button shows the pending label (never disabled).
// Straight off the mutation: the submit swaps its label and stays enabled.
<EditRolesForm roles={roles} isPending={update.isPending} onValidSubmit={submit}>
<EditRolesFormFields />
<EditRolesFormSubmit pendingLabel={t("saving")}>{t("save")}</EditRolesFormSubmit>
</EditRolesForm>;
Open in Storybook ↗
With ErrorA failed mutation surfaces a form-level error above the actions.
// A form-level error, e.g. from a failed mutation, reads above the actions.
<EditRolesForm roles={roles} error={error} serverErrors={serverErrors} onValidSubmit={submit}>
<EditRolesFormFields />
<EditRolesFormSubmit pendingLabel={t("saving")}>{t("save")}</EditRolesFormSubmit>
</EditRolesForm>;
Open in Storybook ↗