mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat(site): show non-assignable roles as disabled (#24846)
Previously, non-assignable roles were filtered out of the role selector entirely. Now all roles are shown, with non-assignable roles rendered as disabled (dimmed, `cursor-not-allowed`, checkbox disabled) so users can see they exist but cannot toggle them. <img width="672" height="392" alt="Screenshot 2026-04-30 at 1 40 44 PM" src="https://github.com/user-attachments/assets/9d17c06a-fcf0-4a49-9557-99cbddb18cba" />
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { action } from "storybook/actions";
|
||||
import {
|
||||
assignableRole,
|
||||
MockAuditorRole,
|
||||
MockOwnerRole,
|
||||
MockTemplateAdminRole,
|
||||
MockUserAdminRole,
|
||||
mockApiError,
|
||||
} from "#/testHelpers/entities";
|
||||
import { RoleSelector } from "./RoleSelector";
|
||||
|
||||
const meta: Meta<typeof RoleSelector> = {
|
||||
title: "pages/CreateUserPage/RoleSelector",
|
||||
component: RoleSelector,
|
||||
args: {
|
||||
onChange: action("change"),
|
||||
selectedRoles: [],
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof RoleSelector>;
|
||||
|
||||
const allAssignable = [
|
||||
assignableRole(MockOwnerRole, true),
|
||||
assignableRole(MockUserAdminRole, true),
|
||||
assignableRole(MockTemplateAdminRole, true),
|
||||
assignableRole(MockAuditorRole, true),
|
||||
];
|
||||
|
||||
const someNonAssignable = [
|
||||
assignableRole(MockOwnerRole, false),
|
||||
assignableRole(MockUserAdminRole, true),
|
||||
assignableRole(MockTemplateAdminRole, false),
|
||||
assignableRole(MockAuditorRole, true),
|
||||
];
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
roles: allAssignable,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithSelections: Story = {
|
||||
args: {
|
||||
roles: allAssignable,
|
||||
selectedRoles: [MockUserAdminRole.name, MockAuditorRole.name],
|
||||
},
|
||||
};
|
||||
|
||||
export const WithNonAssignableRoles: Story = {
|
||||
args: {
|
||||
roles: someNonAssignable,
|
||||
},
|
||||
};
|
||||
|
||||
export const Loading: Story = {
|
||||
args: {
|
||||
roles: [],
|
||||
loading: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithError: Story = {
|
||||
args: {
|
||||
roles: [],
|
||||
error: mockApiError({ message: "Failed to fetch assignable roles." }),
|
||||
},
|
||||
};
|
||||
@@ -5,6 +5,7 @@ import type { AssignableRoles } from "#/api/typesGenerated";
|
||||
import { Alert, AlertTitle } from "#/components/Alert/Alert";
|
||||
import { Checkbox } from "#/components/Checkbox/Checkbox";
|
||||
import { Skeleton } from "#/components/Skeleton/Skeleton";
|
||||
import { cn } from "#/utils/cn";
|
||||
|
||||
const roleDescriptions: Record<string, string> = {
|
||||
owner:
|
||||
@@ -33,9 +34,7 @@ export const RoleSelector: FC<RoleSelectorProps> = ({
|
||||
error,
|
||||
}) => {
|
||||
const baseId = useId();
|
||||
const selectableRoles = roles.filter(
|
||||
(r) => r.assignable && r.name !== "member",
|
||||
);
|
||||
const selectableRoles = roles.filter((r) => r.name !== "member");
|
||||
|
||||
const handleToggle = (roleName: string) => {
|
||||
if (selectedRoles.includes(roleName)) {
|
||||
@@ -98,12 +97,18 @@ export const RoleSelector: FC<RoleSelectorProps> = ({
|
||||
<label
|
||||
key={role.name}
|
||||
htmlFor={checkboxId}
|
||||
className="flex items-start gap-2 cursor-pointer"
|
||||
className={cn(
|
||||
"flex items-start gap-2",
|
||||
role.assignable
|
||||
? "cursor-pointer"
|
||||
: "cursor-not-allowed opacity-50",
|
||||
)}
|
||||
>
|
||||
<Checkbox
|
||||
id={checkboxId}
|
||||
checked={selectedRoles.includes(role.name)}
|
||||
onCheckedChange={() => handleToggle(role.name)}
|
||||
disabled={!role.assignable}
|
||||
className="mt-1 shrink-0"
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
|
||||
Reference in New Issue
Block a user