feat(organization): allow setting a default role for new members

This commit is contained in:
Mauricio Siu
2026-08-11 02:21:33 -06:00
parent 1ccf15f331
commit aaf8d7e12f
10 changed files with 9241 additions and 9 deletions
@@ -106,6 +106,7 @@ export const AddInvitation = () => {
const { mutateAsync: createUserWithCredentials, isPending: isCreating } =
api.user.createUserWithCredentials.useMutation();
const { data: customRoles } = api.customRole.all.useQuery();
const { data: activeOrganization } = api.organization.active.useQuery();
const [error, setError] = useState<string | null>(null);
const form = useForm<AddInvitation>({
@@ -132,6 +133,16 @@ export const AddInvitation = () => {
}
}, [form, isCloud]);
useEffect(() => {
if (
activeOrganization?.defaultRole &&
activeOrganization.defaultRole !== "owner" &&
!form.formState.dirtyFields.role
) {
form.setValue("role", activeOrganization.defaultRole);
}
}, [form, activeOrganization?.defaultRole]);
const onSubmit = async (data: AddInvitation) => {
setError(null);
@@ -267,10 +278,7 @@ export const AddInvitation = () => {
return (
<FormItem>
<FormLabel>Role</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
>
<Select onValueChange={field.onChange} value={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a role" />
@@ -45,6 +45,13 @@ import {
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { api } from "@/utils/api";
@@ -780,6 +787,78 @@ function HandleCustomRole({
);
}
const DefaultRoleSection = ({ customRoles }: { customRoles: string[] }) => {
const utils = api.useUtils();
const { data: auth } = api.user.get.useQuery();
const { data: activeOrganization } = api.organization.active.useQuery();
const { mutateAsync: updateOrganization, isPending: isUpdating } =
api.organization.update.useMutation();
const [selectedRole, setSelectedRole] = useState<string>();
if (auth?.role !== "owner" || !activeOrganization) {
return null;
}
const currentRole = activeOrganization.defaultRole ?? "member";
const value = selectedRole ?? currentRole;
const onSave = async () => {
await updateOrganization({
organizationId: activeOrganization.id,
name: activeOrganization.name,
logo: activeOrganization.logo ?? undefined,
defaultRole: value,
})
.then(() => {
toast.success("Default role updated");
utils.organization.active.invalidate();
})
.catch((error) => {
toast.error(
error instanceof Error
? error.message
: "Error updating default role",
);
});
};
return (
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 rounded-lg border bg-muted/20 p-4">
<div className="space-y-0.5">
<p className="text-sm font-medium">Default role for new members</p>
<p className="text-xs text-muted-foreground">
Assigned automatically to users joining through SSO and preselected
when creating invitations.
</p>
</div>
<div className="flex items-center gap-2">
<Select value={value} onValueChange={setSelectedRole}>
<SelectTrigger className="w-40">
<SelectValue placeholder="Select a role" />
</SelectTrigger>
<SelectContent>
<SelectItem value="member">Member</SelectItem>
<SelectItem value="admin">Admin</SelectItem>
{customRoles.map((role) => (
<SelectItem key={role} value={role}>
{role}
</SelectItem>
))}
</SelectContent>
</Select>
<Button
size="sm"
isLoading={isUpdating}
disabled={value === currentRole}
onClick={onSave}
>
Save
</Button>
</div>
</div>
);
};
const CustomRolesContent = () => {
const {
data: customRoles,
@@ -821,6 +900,9 @@ const CustomRolesContent = () => {
return (
<div className="space-y-4">
<DefaultRoleSection
customRoles={customRoles?.map((role) => role.role) ?? []}
/>
<div className="flex justify-end">
<HandleCustomRole onSuccess={refetch} />
</div>