RegistryUI recipes
Team picker
A select of the caller's teams in the active organization, to scope a page or assign work.
It loads its data on the server through the block's client; the teams block must be installed (npx better-supabase add teams). Its page on bettersupabase.com is /recipes/team-picker.
Web, with shadcn components:
npx shadcn add button empty field input select/**
* Scopes the page to one of the organization's teams through `?team=`. With no
* teams yet, it offers to create one when the caller holds `teams.manage`.
*/
export function TeamPicker({
teams,
teamId,
canManage,
}: {
teams: readonly TeamOption[];
teamId: string | null;
canManage: boolean;
}) {
const t = useExtracted("work");
const fieldId = useId();
const router = useRouter();
const pathname = usePathname();
const errorMessage = useErrorMessage();
const [switching, startTransition] = useTransition();
const form = useActionForm(createTeam, {
onSuccess: () => {
toast.success(t("Team created"));
},
});
if (teams.length === 0) {
const error = form.error === undefined ? null : errorMessage(form.error);
return (
<Empty className="border" data-recipe="team-picker">
<EmptyHeader>
<EmptyMedia variant="icon">
<UsersIcon />
</EmptyMedia>
<EmptyTitle>{t("No teams yet")}</EmptyTitle>
<EmptyDescription>
{canManage
? t("Create a team to group work and filter the board by it.")
: t("An admin can create teams to group work.")}
</EmptyDescription>
</EmptyHeader>
{canManage ? (
<EmptyContent>
<form {...form.formProps} className="flex w-full items-end gap-2">
<Field data-invalid={error ? true : undefined}>
<FieldLabel htmlFor={`${fieldId}-team-name`}>
{t("Team name")}
</FieldLabel>
<Input
id={`${fieldId}-team-name`}
name="name"
required
maxLength={80}
autoComplete="off"
/>
{error ? <FieldError>{error}</FieldError> : null}
</Field>
<Button type="submit" disabled={form.pending}>
{t("Create team")}
</Button>
</form>
</EmptyContent>
) : null}
</Empty>
);
}
const items = [
{ value: ALL_TEAMS, label: t("All teams") },
...teams.map((team) => ({ value: team.id, label: team.name })),
];
return (
<div className="flex items-center gap-2" data-recipe="team-picker">
<span className="text-sm text-muted-foreground" id={`${fieldId}-label`}>
{t("Team")}
</span>
<Select
value={teamId ?? ALL_TEAMS}
items={items}
disabled={switching}
onValueChange={(value) => {
const next = value ?? ALL_TEAMS;
startTransition(() => {
router.replace(
next === ALL_TEAMS
? pathname
: `${pathname}?team=${encodeURIComponent(next)}`,
);
});
}}
>
<SelectTrigger className="w-48" aria-labelledby={`${fieldId}-label`}>
<SelectValue />
</SelectTrigger>
<SelectContent>
{items.map((item) => (
<SelectItem key={item.value} value={item.value}>
{item.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
);
}Last updated on