mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
fix(site): update bulk action checkbox style for workspace and task lists (#21535)
## Summary Updates the bulk action checkbox style in workspace and task lists to use the new Shadcn Checkbox component that aligns with the Coder design system (as specified in [Figma](https://www.figma.com/design/WfqIgsTFN2BscBSSyXWF8/Coder-kit?node-id=489-4187&t=KRtpi391rVPHRXJI-1)). ## Changes - **WorkspacesTable.tsx**: Replace MUI Checkbox with Shadcn Checkbox component - **TasksTable.tsx**: Replace MUI Checkbox with Shadcn Checkbox component - **WorkspacesPageView.stories.tsx**: Add `WithCheckedWorkspaces` story to showcase the new design ## Key Improvements The new checkbox design features: - ✨ Consistent 20px × 20px sizing (vs. old larger MUI checkbox) - 🎨 Clean inverted color scheme (light background unchecked, dark when checked) - ✅ Proper indeterminate state support for "select all" functionality - 🎯 Smooth hover and focus states with proper ring indicators - 📐 Better alignment with Coder design language from Figma ## API Changes Updated from MUI Checkbox API to Radix UI Checkbox API: - `onChange={(e) => ...}` → `onCheckedChange={(checked) => ...}` - Removed MUI-specific `size` props (`xsmall`, `small`) - Updated `checked` prop to support boolean | "indeterminate" ## Testing ### Storybook The checkbox changes can be reviewed in Storybook: 1. **Checkbox Component** - `components/Checkbox` - Base component examples 2. **Workspaces Page** - `pages/WorkspacesPage/WithCheckedWorkspaces` - Shows workspace list with selections 3. **Tasks Page** - `pages/TasksPage/BatchActionsSomeSelected` - Shows task list with selections ### Feature Flag Requirement **Note**: The bulk action checkboxes require the `workspace_batch_actions` or `task_batch_actions` feature flags to be enabled (Premium feature). To test in a live environment, you'll need a valid license that includes these features. ## Screenshots Before: Old MUI checkbox with prominent blue styling and larger size After: New Shadcn checkbox with refined design matching Coder's design system _(Screenshots can be viewed in Storybook at the URLs above)_ Closes #21444 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com> Co-authored-by: Jake Howell <jake@hwll.me> Co-authored-by: Jaayden Halko <jaayden@coder.com>
This commit is contained in:
@@ -17,7 +17,7 @@ export const Checkbox = React.forwardRef<
|
||||
<CheckboxPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
`peer h-5 w-5 shrink-0 rounded-sm border border-border border-solid
|
||||
`peer size-[18px] shrink-0 rounded-sm border border-border border-solid
|
||||
focus-visible:outline-none focus-visible:ring-2
|
||||
focus-visible:ring-content-link focus-visible:ring-offset-4 focus-visible:ring-offset-surface-primary
|
||||
disabled:cursor-not-allowed disabled:bg-surface-primary disabled:data-[state=checked]:bg-surface-tertiary
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import Checkbox from "@mui/material/Checkbox";
|
||||
import { getErrorDetail, getErrorMessage } from "api/errors";
|
||||
import type { Task } from "api/typesGenerated";
|
||||
import { Avatar } from "components/Avatar/Avatar";
|
||||
import { AvatarData } from "components/Avatar/AvatarData";
|
||||
import { AvatarDataSkeleton } from "components/Avatar/AvatarDataSkeleton";
|
||||
import { Button } from "components/Button/Button";
|
||||
import { Checkbox } from "components/Checkbox/Checkbox";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -93,18 +93,16 @@ export const TasksTable: FC<TasksTableProps> = ({
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-1/3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-5">
|
||||
{canCheckTasks && (
|
||||
<Checkbox
|
||||
className="-my-[9px]"
|
||||
disabled={!tasks || tasks.length === 0}
|
||||
checked={
|
||||
tasks &&
|
||||
tasks.length > 0 &&
|
||||
checkedTaskIds.size === tasks.length
|
||||
}
|
||||
size="xsmall"
|
||||
onChange={(_, checked) => {
|
||||
onCheckedChange={(checked) => {
|
||||
if (!tasks || !onCheckChange) {
|
||||
return;
|
||||
}
|
||||
@@ -209,17 +207,16 @@ const TaskRow: FC<TaskRowProps> = ({
|
||||
{...clickableRowProps}
|
||||
>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-5">
|
||||
{canCheck && (
|
||||
<Checkbox
|
||||
data-testid={`checkbox-${task.id}`}
|
||||
size="xsmall"
|
||||
checked={checked}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
onChange={(e) => {
|
||||
onCheckChange(task.id, e.currentTarget.checked);
|
||||
onCheckedChange={(checked) => {
|
||||
onCheckChange(task.id, Boolean(checked));
|
||||
}}
|
||||
aria-label={`Select task ${task.initial_prompt}`}
|
||||
/>
|
||||
@@ -320,8 +317,8 @@ const TasksSkeleton: FC<TasksSkeletonProps> = ({ canCheckTasks }) => {
|
||||
<TableLoaderSkeleton>
|
||||
<TableRowSkeleton>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
{canCheckTasks && <Checkbox size="small" disabled />}
|
||||
<div className="flex items-center gap-5">
|
||||
{canCheckTasks && <Checkbox disabled />}
|
||||
<AvatarDataSkeleton />
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
@@ -360,9 +360,7 @@ describe("WorkspacesPage", () => {
|
||||
});
|
||||
|
||||
const getWorkspaceCheckbox = (workspace: Workspace) => {
|
||||
return within(screen.getByTestId(`checkbox-${workspace.id}`)).getByRole(
|
||||
"checkbox",
|
||||
);
|
||||
return screen.getByTestId(`checkbox-${workspace.id}`);
|
||||
};
|
||||
|
||||
describe("WorkspaceApps filtering", () => {
|
||||
|
||||
@@ -397,3 +397,12 @@ export const ShowWorkspaceTasks: Story = {
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const WithCheckedWorkspaces: Story = {
|
||||
args: {
|
||||
workspaces: allWorkspaces.slice(0, 5),
|
||||
checkedWorkspaces: allWorkspaces.slice(0, 2),
|
||||
canCheckWorkspaces: true,
|
||||
count: 5,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import Checkbox from "@mui/material/Checkbox";
|
||||
import Skeleton from "@mui/material/Skeleton";
|
||||
import { templateVersion } from "api/queries/templates";
|
||||
import { apiKey } from "api/queries/users";
|
||||
@@ -19,6 +18,7 @@ import { AvatarData } from "components/Avatar/AvatarData";
|
||||
import { AvatarDataSkeleton } from "components/Avatar/AvatarDataSkeleton";
|
||||
import { Badge } from "components/Badge/Badge";
|
||||
import { Button } from "components/Button/Button";
|
||||
import { Checkbox } from "components/Checkbox/Checkbox";
|
||||
import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog";
|
||||
import { ExternalImage } from "components/ExternalImage/ExternalImage";
|
||||
import { VSCodeIcon } from "components/Icons/VSCodeIcon";
|
||||
@@ -117,14 +117,16 @@ export const WorkspacesTable: FC<WorkspacesTableProps> = ({
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-1/3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-5">
|
||||
{canCheckWorkspaces && (
|
||||
<Checkbox
|
||||
className="-my-[9px]"
|
||||
disabled={!workspaces || workspaces.length === 0}
|
||||
checked={checkedWorkspaces.length === workspaces?.length}
|
||||
size="xsmall"
|
||||
onChange={(_, checked) => {
|
||||
checked={
|
||||
workspaces &&
|
||||
workspaces.length > 0 &&
|
||||
checkedWorkspaces.length === workspaces.length
|
||||
}
|
||||
onCheckedChange={(checked) => {
|
||||
if (!workspaces) {
|
||||
return;
|
||||
}
|
||||
@@ -135,6 +137,7 @@ export const WorkspacesTable: FC<WorkspacesTableProps> = ({
|
||||
onCheckChange(workspaces);
|
||||
}
|
||||
}}
|
||||
aria-label="Select all workspaces"
|
||||
/>
|
||||
)}
|
||||
Name
|
||||
@@ -173,18 +176,17 @@ export const WorkspacesTable: FC<WorkspacesTableProps> = ({
|
||||
checked={checked}
|
||||
>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-5">
|
||||
{canCheckWorkspaces && (
|
||||
<Checkbox
|
||||
data-testid={`checkbox-${workspace.id}`}
|
||||
size="xsmall"
|
||||
disabled={cantBeChecked(workspace)}
|
||||
checked={checked}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
onChange={(e) => {
|
||||
if (e.currentTarget.checked) {
|
||||
onCheckedChange={(checked) => {
|
||||
if (checked) {
|
||||
onCheckChange([...checkedWorkspaces, workspace]);
|
||||
} else {
|
||||
onCheckChange(
|
||||
@@ -194,6 +196,7 @@ export const WorkspacesTable: FC<WorkspacesTableProps> = ({
|
||||
);
|
||||
}
|
||||
}}
|
||||
aria-label={`Select workspace ${workspace.name}`}
|
||||
/>
|
||||
)}
|
||||
<AvatarData
|
||||
@@ -339,8 +342,8 @@ const TableLoader: FC<TableLoaderProps> = ({ canCheckWorkspaces }) => {
|
||||
<TableLoaderSkeleton>
|
||||
<TableRowSkeleton>
|
||||
<TableCell className="w-2/6">
|
||||
<div className="flex items-center gap-2">
|
||||
{canCheckWorkspaces && <Checkbox size="small" disabled />}
|
||||
<div className="flex items-center gap-5">
|
||||
{canCheckWorkspaces && <Checkbox disabled />}
|
||||
<AvatarDataSkeleton />
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
Reference in New Issue
Block a user