feat: remove license gate from workspace and task bulk actions (#22090)

Co-authored-by: blink-so[bot] <211532188+blink-so[bot]@users.noreply.github.com>
This commit is contained in:
blinkagent[bot]
2026-02-13 20:08:36 +05:00
committed by GitHub
parent 6d41d98b65
commit 00713385fb
8 changed files with 75 additions and 124 deletions
+1 -5
View File
@@ -102,11 +102,7 @@ manually updated the workspace.
## Bulk operations
> [!NOTE]
> Bulk operations are a Premium feature.
> [Learn more](https://coder.com/pricing#compare-plans).
Licensed admins may apply bulk operations (update, delete, start, stop) in the
Admins may apply bulk operations (update, delete, start, stop) in the
**Workspaces** tab. Select the workspaces you'd like to modify with the
checkboxes on the left, then use the top-right **Actions** dropdown to apply the
operation.
@@ -415,7 +415,6 @@ export const ResumeTask: Story = {
export const BatchActionsEnabled: Story = {
parameters: {
features: ["task_batch_actions"],
queries: [
{
key: ["tasks", { owner: MockUserOwner.username }],
@@ -431,7 +430,6 @@ export const BatchActionsEnabled: Story = {
export const BatchActionsSomeSelected: Story = {
parameters: {
features: ["task_batch_actions"],
queries: [
{
key: ["tasks", { owner: MockUserOwner.username }],
@@ -458,7 +456,6 @@ export const BatchActionsSomeSelected: Story = {
export const BatchActionsAllSelected: Story = {
parameters: {
features: ["task_batch_actions"],
queries: [
{
key: ["tasks", { owner: MockUserOwner.username }],
@@ -484,7 +481,6 @@ export const BatchActionsAllSelected: Story = {
export const BatchActionsDropdownOpen: Story = {
parameters: {
features: ["task_batch_actions"],
queries: [
{
key: ["tasks", { owner: MockUserOwner.username }],
-5
View File
@@ -30,7 +30,6 @@ import { TableToolbar } from "components/TableToolbar/TableToolbar";
import { useAuthenticated } from "hooks";
import { useSearchParamsKey } from "hooks/useSearchParamsKey";
import { ChevronDownIcon, TrashIcon } from "lucide-react";
import { useDashboard } from "modules/dashboard/useDashboard";
import {
isTaskNotification,
notificationIsDisabled,
@@ -102,9 +101,6 @@ const TasksPage: FC = () => {
await batchActions.delete(checkedTasks);
};
const { entitlements } = useDashboard();
const canCheckTasks = entitlements.features.task_batch_actions.enabled;
// Count workspaces that will be deleted with the selected tasks.
const workspaceCount = checkedTasks.filter(
(t) => t.workspace_id !== null,
@@ -277,7 +273,6 @@ const TasksPage: FC = () => {
onRetry={tasksQuery.refetch}
checkedTaskIds={checkedTaskIds}
onCheckChange={handleCheckChange}
canCheckTasks={canCheckTasks}
/>
</section>
)}
+33 -50
View File
@@ -47,7 +47,6 @@ type TasksTableProps = {
onRetry: () => void;
checkedTaskIds?: Set<string>;
onCheckChange?: (checkedTaskIds: Set<string>) => void;
canCheckTasks?: boolean;
};
export const TasksTable: FC<TasksTableProps> = ({
@@ -56,14 +55,13 @@ export const TasksTable: FC<TasksTableProps> = ({
onRetry,
checkedTaskIds = new Set(),
onCheckChange,
canCheckTasks = false,
}) => {
let body: ReactNode = null;
if (error) {
body = <TasksErrorBody error={error} onRetry={onRetry} />;
} else if (!tasks) {
body = <TasksSkeleton canCheckTasks={canCheckTasks} />;
body = <TasksSkeleton />;
} else if (tasks.length === 0) {
body = <TasksEmpty />;
} else {
@@ -84,7 +82,6 @@ export const TasksTable: FC<TasksTableProps> = ({
}
onCheckChange(newIds);
}}
canCheck={canCheckTasks}
/>
);
});
@@ -96,28 +93,26 @@ export const TasksTable: FC<TasksTableProps> = ({
<TableRow>
<TableHead className="w-1/3">
<div className="flex items-center gap-5">
{canCheckTasks && (
<Checkbox
disabled={!tasks || tasks.length === 0}
checked={
tasks &&
tasks.length > 0 &&
checkedTaskIds.size === tasks.length
<Checkbox
disabled={!tasks || tasks.length === 0}
checked={
tasks &&
tasks.length > 0 &&
checkedTaskIds.size === tasks.length
}
onCheckedChange={(checked) => {
if (!tasks || !onCheckChange) {
return;
}
onCheckedChange={(checked) => {
if (!tasks || !onCheckChange) {
return;
}
if (!checked) {
onCheckChange(new Set());
} else {
onCheckChange(new Set(tasks.map((t) => t.id)));
}
}}
aria-label="Select all tasks"
/>
)}
if (!checked) {
onCheckChange(new Set());
} else {
onCheckChange(new Set(tasks.map((t) => t.id)));
}
}}
aria-label="Select all tasks"
/>
Task
</div>
</TableHead>
@@ -182,15 +177,9 @@ type TaskRowProps = {
task: Task;
checked: boolean;
onCheckChange: (taskId: string, checked: boolean) => void;
canCheck: boolean;
};
const TaskRow: FC<TaskRowProps> = ({
task,
checked,
onCheckChange,
canCheck,
}) => {
const TaskRow: FC<TaskRowProps> = ({ task, checked, onCheckChange }) => {
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
const templateDisplayName = task.template_display_name ?? task.template_name;
const navigate = useNavigate();
@@ -228,19 +217,17 @@ const TaskRow: FC<TaskRowProps> = ({
>
<TableCell>
<div className="flex items-center gap-5">
{canCheck && (
<Checkbox
data-testid={`checkbox-${task.id}`}
checked={checked}
onClick={(e) => {
e.stopPropagation();
}}
onCheckedChange={(checked) => {
onCheckChange(task.id, Boolean(checked));
}}
aria-label={`Select task ${task.initial_prompt}`}
/>
)}
<Checkbox
data-testid={`checkbox-${task.id}`}
checked={checked}
onClick={(e) => {
e.stopPropagation();
}}
onCheckedChange={(checked) => {
onCheckChange(task.id, Boolean(checked));
}}
aria-label={`Select task ${task.initial_prompt}`}
/>
<AvatarData
title={
<span className="block max-w-[520px] truncate">
@@ -333,17 +320,13 @@ const TaskRow: FC<TaskRowProps> = ({
);
};
type TasksSkeletonProps = {
canCheckTasks: boolean;
};
const TasksSkeleton: FC<TasksSkeletonProps> = ({ canCheckTasks }) => {
const TasksSkeleton: FC = () => {
return (
<TableLoaderSkeleton>
<TableRowSkeleton>
<TableCell>
<div className="flex items-center gap-5">
{canCheckTasks && <Checkbox disabled />}
<Checkbox disabled />
<AvatarDataSkeleton />
</div>
</TableCell>
@@ -70,7 +70,6 @@ const WorkspacesPage: FC = () => {
},
});
const { permissions, user: me } = useAuthenticated();
const { entitlements } = useDashboard();
const templatesQuery = useQuery(templates());
const workspacePermissionsQuery = useQuery(
workspacePermissionsByOrganization(
@@ -129,8 +128,6 @@ const WorkspacesPage: FC = () => {
});
const [activeBatchAction, setActiveBatchAction] = useState<BatchAction>();
const canCheckWorkspaces =
entitlements.features.workspace_batch_actions.enabled;
const batchActions = useBatchActions({
onSuccess: async () => {
await refetch();
@@ -161,7 +158,6 @@ const WorkspacesPage: FC = () => {
return new Set(newIds);
});
}}
canCheckWorkspaces={canCheckWorkspaces}
templates={filteredTemplates}
templatesFetchStatus={templatesQuery.status}
workspaces={data?.workspaces}
@@ -168,7 +168,6 @@ const meta: Meta<typeof WorkspacesPageView> = {
limit: DEFAULT_RECORDS_PER_PAGE,
filterState: defaultFilterProps,
checkedWorkspaces: [],
canCheckWorkspaces: true,
templates: mockTemplates,
templatesFetchStatus: "success",
count: 13,
@@ -402,7 +401,6 @@ export const WithCheckedWorkspaces: Story = {
args: {
workspaces: allWorkspaces.slice(0, 5),
checkedWorkspaces: allWorkspaces.slice(0, 2),
canCheckWorkspaces: true,
count: 5,
},
};
@@ -60,7 +60,6 @@ interface WorkspacesPageViewProps {
onBatchUpdateTransition: () => void;
onBatchStartTransition: () => void;
onBatchStopTransition: () => void;
canCheckWorkspaces: boolean;
templatesFetchStatus: TemplateQuery["status"];
templates: TemplateQuery["data"];
canCreateTemplate: boolean;
@@ -84,7 +83,6 @@ export const WorkspacesPageView: FC<WorkspacesPageViewProps> = ({
onBatchStopTransition,
onBatchStartTransition,
isRunningBatchAction,
canCheckWorkspaces,
templates,
templatesFetchStatus,
canCreateTemplate,
@@ -231,7 +229,6 @@ export const WorkspacesPageView: FC<WorkspacesPageViewProps> = ({
isUsingFilter={filterState.filter.used}
checkedWorkspaces={checkedWorkspaces}
onCheckChange={onCheckChange}
canCheckWorkspaces={canCheckWorkspaces}
templates={templates}
onActionSuccess={onActionSuccess}
onActionError={onActionError}
@@ -92,7 +92,6 @@ interface WorkspacesTableProps {
error?: unknown;
isUsingFilter: boolean;
onCheckChange: (checkedWorkspaces: readonly Workspace[]) => void;
canCheckWorkspaces: boolean;
templates?: Template[];
canCreateTemplate: boolean;
onActionSuccess: () => Promise<void>;
@@ -104,7 +103,6 @@ export const WorkspacesTable: FC<WorkspacesTableProps> = ({
checkedWorkspaces,
isUsingFilter,
onCheckChange,
canCheckWorkspaces,
templates,
canCreateTemplate,
onActionSuccess,
@@ -118,28 +116,26 @@ export const WorkspacesTable: FC<WorkspacesTableProps> = ({
<TableRow>
<TableHead className="w-1/3">
<div className="flex items-center gap-5">
{canCheckWorkspaces && (
<Checkbox
disabled={!workspaces || workspaces.length === 0}
checked={
workspaces &&
workspaces.length > 0 &&
checkedWorkspaces.length === workspaces.length
<Checkbox
disabled={!workspaces || workspaces.length === 0}
checked={
workspaces &&
workspaces.length > 0 &&
checkedWorkspaces.length === workspaces.length
}
onCheckedChange={(checked) => {
if (!workspaces) {
return;
}
onCheckedChange={(checked) => {
if (!workspaces) {
return;
}
if (!checked) {
onCheckChange([]);
} else {
onCheckChange(workspaces);
}
}}
aria-label="Select all workspaces"
/>
)}
if (!checked) {
onCheckChange([]);
} else {
onCheckChange(workspaces);
}
}}
aria-label="Select all workspaces"
/>
Name
</div>
</TableHead>
@@ -151,7 +147,7 @@ export const WorkspacesTable: FC<WorkspacesTableProps> = ({
</TableRow>
</TableHeader>
<TableBody className="[&_td]:h-[72px]">
{!workspaces && <TableLoader canCheckWorkspaces={canCheckWorkspaces} />}
{!workspaces && <TableLoader />}
{workspaces && workspaces.length === 0 && (
<TableRow>
<TableCell colSpan={999}>
@@ -177,28 +173,26 @@ export const WorkspacesTable: FC<WorkspacesTableProps> = ({
>
<TableCell>
<div className="flex items-center gap-5">
{canCheckWorkspaces && (
<Checkbox
data-testid={`checkbox-${workspace.id}`}
disabled={cantBeChecked(workspace)}
checked={checked}
onClick={(e) => {
e.stopPropagation();
}}
onCheckedChange={(checked) => {
if (checked) {
onCheckChange([...checkedWorkspaces, workspace]);
} else {
onCheckChange(
checkedWorkspaces.filter(
(w) => w.id !== workspace.id,
),
);
}
}}
aria-label={`Select workspace ${workspace.name}`}
/>
)}
<Checkbox
data-testid={`checkbox-${workspace.id}`}
disabled={cantBeChecked(workspace)}
checked={checked}
onClick={(e) => {
e.stopPropagation();
}}
onCheckedChange={(checked) => {
if (checked) {
onCheckChange([...checkedWorkspaces, workspace]);
} else {
onCheckChange(
checkedWorkspaces.filter(
(w) => w.id !== workspace.id,
),
);
}
}}
aria-label={`Select workspace ${workspace.name}`}
/>
<AvatarData
title={
<Stack direction="row" spacing={0.5} alignItems="center">
@@ -333,17 +327,13 @@ const WorkspacesRow: FC<WorkspacesRowProps> = ({
);
};
interface TableLoaderProps {
canCheckWorkspaces?: boolean;
}
const TableLoader: FC<TableLoaderProps> = ({ canCheckWorkspaces }) => {
const TableLoader: FC = () => {
return (
<TableLoaderSkeleton>
<TableRowSkeleton>
<TableCell className="w-2/6">
<div className="flex items-center gap-5">
{canCheckWorkspaces && <Checkbox disabled />}
<Checkbox disabled />
<AvatarDataSkeleton />
</div>
</TableCell>