feat(site): show org default roles in member role editor (#26107)

Surfaces the org's `default_org_member_roles` inside the org members role editor. These roles are implied, not physically assigned to any member. Just like the `member` role.
This commit is contained in:
Steven Masley
2026-06-05 20:51:20 -05:00
committed by GitHub
parent fa56224eda
commit 69f9b0e535
4 changed files with 83 additions and 8 deletions
@@ -89,3 +89,13 @@ export const OrganizationMemberRoles: Story = {
availableRoles: orgMemberRoles,
},
};
export const WithAdditionalImpliedRoles: Story = {
args: {
availableRoles: orgMemberRoles,
additionalImpliedRoles: [
assignableRole(MockAgentsAccessRole, true),
assignableRole(MockOrganizationAuditorRole, true),
],
},
};
+44 -6
View File
@@ -16,6 +16,7 @@ type RoleSelectorProps = {
loading?: boolean;
error?: unknown;
availableRoles?: AssignableRoles[];
additionalImpliedRoles?: AssignableRoles[];
selectedRoles: Set<string>;
onChange: (roles: Set<string>) => void;
};
@@ -25,6 +26,7 @@ export const RoleSelector: FC<RoleSelectorProps> = ({
loading,
error,
availableRoles = [],
additionalImpliedRoles = [],
selectedRoles,
onChange,
}) => {
@@ -32,7 +34,7 @@ export const RoleSelector: FC<RoleSelectorProps> = ({
return (
<RoleSelectorLayout>
<RoleSelectorSkeleton />
<MemberRole />
<ImpliedRolesList additionalImpliedRoles={additionalImpliedRoles} />
</RoleSelectorLayout>
);
}
@@ -49,8 +51,11 @@ export const RoleSelector: FC<RoleSelectorProps> = ({
);
}
const impliedRoleNames = new Set(additionalImpliedRoles.map((r) => r.name));
const { selectableRoles = [], advancedRoles = [] } = Object.groupBy(
availableRoles.filter((r) => r.name !== "member"),
availableRoles.filter(
(r) => r.name !== "member" && !impliedRoleNames.has(r.name),
),
(it) =>
advancedRoleNames.includes(it.name) ? "advancedRoles" : "selectableRoles",
);
@@ -80,7 +85,7 @@ export const RoleSelector: FC<RoleSelectorProps> = ({
/>
)}
<MemberRole />
<ImpliedRolesList additionalImpliedRoles={additionalImpliedRoles} />
</RoleSelectorLayout>
);
};
@@ -182,13 +187,46 @@ const RoleSelectorLayout: React.FC<RoleSelectorLayoutProps> = ({
);
};
const MemberRole: React.FC = () => {
type ImpliedRolesListProps = {
additionalImpliedRoles: AssignableRoles[];
};
const ImpliedRolesList: React.FC<ImpliedRolesListProps> = ({
additionalImpliedRoles,
}) => {
return (
<>
<ImpliedRoleRow title="Member" description={roleDescriptions.member} />
{additionalImpliedRoles.map((role) => (
<ImpliedRoleRow
key={role.name}
title={role.display_name || role.name}
description={roleDescriptions[role.name] ?? ""}
caption="Sourced from organization default roles"
/>
))}
</>
);
};
type ImpliedRoleRowProps = {
title: string;
description: string;
caption?: string;
};
const ImpliedRoleRow: React.FC<ImpliedRoleRowProps> = ({
title,
description,
caption,
}) => {
return (
<div className="border-t border-border py-2 flex items-start gap-2 text-content-disabled">
<UserIcon className="size-4 mt-1 shrink-0" />
<div className="flex flex-col">
<span className="text-sm font-medium">Member</span>
<span className="text-sm">{roleDescriptions.member}</span>
<span className="text-sm font-medium">{title}</span>
{description && <span className="text-sm">{description}</span>}
{caption && <span className="text-xs italic">{caption}</span>}
</div>
</div>
);
@@ -20,6 +20,7 @@ type RoleSelectorDialogProps = {
user?: ThingWithRoles;
/** The roles available in this context that can be given or removed from the user */
availableRoles?: AssignableRoles[];
additionalImpliedRoles?: AssignableRoles[];
onCancel: () => void;
onUpdateRoles: (roles: string[]) => Promise<void>;
@@ -36,6 +37,7 @@ type ThingWithRoles = {
export const RoleSelectorDialog: React.FC<RoleSelectorDialogProps> = ({
user,
availableRoles = [],
additionalImpliedRoles = [],
onCancel,
onUpdateRoles,
isUpdatingRoles,
@@ -48,6 +50,7 @@ export const RoleSelectorDialog: React.FC<RoleSelectorDialogProps> = ({
<ActiveRoleSelectorDialog
user={user}
availableRoles={availableRoles}
additionalImpliedRoles={additionalImpliedRoles}
onCancel={onCancel}
onUpdateRoles={onUpdateRoles}
isUpdatingRoles={isUpdatingRoles}
@@ -58,6 +61,7 @@ export const RoleSelectorDialog: React.FC<RoleSelectorDialogProps> = ({
const ActiveRoleSelectorDialog: React.FC<Required<RoleSelectorDialogProps>> = ({
user,
availableRoles,
additionalImpliedRoles,
onCancel,
onUpdateRoles,
isUpdatingRoles,
@@ -89,6 +93,7 @@ const ActiveRoleSelectorDialog: React.FC<Required<RoleSelectorDialogProps>> = ({
<RoleSelector
hideLabel
availableRoles={availableRoles}
additionalImpliedRoles={additionalImpliedRoles}
selectedRoles={selectedRoles}
onChange={setSelectedRoles}
/>
@@ -1,4 +1,4 @@
import { type FC, useState } from "react";
import { type FC, useMemo, useState } from "react";
import { useMutation, useQuery, useQueryClient } from "react-query";
import { useParams, useSearchParams } from "react-router";
import { toast } from "sonner";
@@ -12,6 +12,7 @@ import {
} from "#/api/queries/organizations";
import { organizationRoles } from "#/api/queries/roles";
import type {
AssignableRoles,
OrganizationMemberWithUserData,
User,
} from "#/api/typesGenerated";
@@ -35,9 +36,10 @@ const OrganizationMembersPage: FC = () => {
organization: string;
};
const { organization, organizationPermissions } = useOrganizationSettings();
const { entitlements } = useDashboard();
const { entitlements, experiments } = useDashboard();
const searchParamsResult = useSearchParams();
const showAISeatColumn = shouldShowAISeatColumn(entitlements);
const defaultRolesEnabled = experiments.includes("minimum-implicit-member");
const organizationRolesQuery = useQuery(organizationRoles(organizationName));
const groupsByUserIdQuery = useQuery(
@@ -76,6 +78,25 @@ const OrganizationMembersPage: FC = () => {
removeOrganizationMember(queryClient, organizationName),
);
// Resolve the org's default member role names against the assignable
// roles list so the dialog can show full display names + descriptions.
const defaultMemberImpliedRoles = useMemo<AssignableRoles[]>(() => {
if (!defaultRolesEnabled) {
return [];
}
const available = organizationRolesQuery.data;
if (!available) {
return [];
}
return (organization?.default_org_member_roles ?? [])
.map((name) => available.find((r) => r.name === name))
.filter((r): r is AssignableRoles => r !== undefined);
}, [
defaultRolesEnabled,
organization?.default_org_member_roles,
organizationRolesQuery.data,
]);
if (!organization) {
return <EmptyState message="Organization not found" />;
}
@@ -133,6 +154,7 @@ const OrganizationMembersPage: FC = () => {
key={memberToEditRoles?.username}
user={memberToEditRoles}
availableRoles={organizationRolesQuery.data}
additionalImpliedRoles={defaultMemberImpliedRoles}
onCancel={() => setMemberToEditRoles(undefined)}
onUpdateRoles={async (roles) => {
try {