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.
pnpm dev:example) to see it.Add the components
terminal npx shadcn add button empty field input selectInstall the glue
Install the block with
npx better-supabase add teams. The glue items are copied into your app, so edit them as you need.None: the page calls the block's client on the server.
Write the component
React and shadcn
View in the exampleteam-picker.tsx /** * 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> ); }