-
Notifications
You must be signed in to change notification settings - Fork 1.1k
feat(site): add bulk delete for tasks #20905
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+676
−31
Merged
Changes from 4 commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
e218dde
feat(site): add bulk delete for tasks
mafredri 7e1a09c
add feature flag
mafredri 41ea0de
fix: change emotion to Tailwind
mafredri 89dd770
last created -> last updated
mafredri dff4101
fix button
mafredri be24577
update testdata
mafredri 4c830ec
add table batch actions test
mafredri af188b1
remove row highlight on select
mafredri 294312b
pr feedback
mafredri 7ec75ae
pr feedback
mafredri c4827db
remove useEffect per feedback
mafredri aff10ce
Merge branch 'main' into bulk-delete-feature-plan-
mafredri File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
82 changes: 82 additions & 0 deletions
82
site/src/pages/TasksPage/BatchDeleteConfirmation.stories.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| import { chromatic } from "testHelpers/chromatic"; | ||
| import { MockTask } from "testHelpers/entities"; | ||
| import type { Meta, StoryObj } from "@storybook/react-vite"; | ||
| import { action } from "storybook/actions"; | ||
| import { userEvent, within } from "storybook/test"; | ||
| import { BatchDeleteConfirmation } from "./BatchDeleteConfirmation"; | ||
|
|
||
| const meta: Meta<typeof BatchDeleteConfirmation> = { | ||
| title: "pages/TasksPage/BatchDeleteConfirmation", | ||
| parameters: { chromatic }, | ||
| component: BatchDeleteConfirmation, | ||
| args: { | ||
| onClose: action("onClose"), | ||
| onConfirm: action("onConfirm"), | ||
| open: true, | ||
| isLoading: false, | ||
| checkedTasks: [ | ||
| MockTask, | ||
| { | ||
| ...MockTask, | ||
| id: "task-2", | ||
| name: "task-test-456", | ||
| initial_prompt: "Add comprehensive tests for the API endpoints", | ||
| owner_name: "bob", | ||
| created_at: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(), | ||
| updated_at: new Date(Date.now() - 1 * 60 * 60 * 1000).toISOString(), | ||
| }, | ||
| { | ||
| ...MockTask, | ||
| id: "task-3", | ||
| name: "task-docs-789", | ||
| initial_prompt: "Update documentation for the new features", | ||
| workspace_id: null, | ||
mafredri marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| created_at: new Date( | ||
| Date.now() - 3 * 24 * 60 * 60 * 1000, | ||
| ).toISOString(), | ||
| updated_at: new Date( | ||
| Date.now() - 2 * 24 * 60 * 60 * 1000, | ||
| ).toISOString(), | ||
| }, | ||
| ], | ||
| workspaceCount: 2, | ||
| }, | ||
| }; | ||
|
|
||
| export default meta; | ||
| type Story = StoryObj<typeof BatchDeleteConfirmation>; | ||
|
|
||
| export const Consequences: Story = {}; | ||
|
|
||
| export const ReviewTasks: Story = { | ||
| play: async ({ canvasElement, step }) => { | ||
| const body = within(canvasElement.ownerDocument.body); | ||
|
|
||
| await step("Advance to stage 2: Review tasks", async () => { | ||
| const confirmButton = await body.findByRole("button", { | ||
| name: /review selected tasks/i, | ||
| }); | ||
| await userEvent.click(confirmButton); | ||
| }); | ||
| }, | ||
| }; | ||
|
|
||
| export const ReviewResources: Story = { | ||
| play: async ({ canvasElement, step }) => { | ||
| const body = within(canvasElement.ownerDocument.body); | ||
|
|
||
| await step("Advance to stage 2: Review tasks", async () => { | ||
| const confirmButton = await body.findByRole("button", { | ||
| name: /review selected tasks/i, | ||
| }); | ||
| await userEvent.click(confirmButton); | ||
| }); | ||
|
|
||
| await step("Advance to stage 3: Review resources", async () => { | ||
| const confirmButton = await body.findByRole("button", { | ||
| name: /confirm.*tasks/i, | ||
| }); | ||
| await userEvent.click(confirmButton); | ||
| }); | ||
| }, | ||
| }; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,197 @@ | ||
| import type { Task } from "api/typesGenerated"; | ||
| import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog"; | ||
| import dayjs from "dayjs"; | ||
| import relativeTime from "dayjs/plugin/relativeTime"; | ||
| import { ClockIcon, ServerIcon, UserIcon } from "lucide-react"; | ||
| import { type FC, type ReactNode, useState } from "react"; | ||
|
|
||
| dayjs.extend(relativeTime); | ||
|
|
||
| type BatchDeleteConfirmationProps = { | ||
| checkedTasks: readonly Task[]; | ||
| workspaceCount: number; | ||
| open: boolean; | ||
| isLoading: boolean; | ||
| onClose: () => void; | ||
| onConfirm: () => void; | ||
| }; | ||
|
|
||
| export const BatchDeleteConfirmation: FC<BatchDeleteConfirmationProps> = ({ | ||
| checkedTasks, | ||
| workspaceCount, | ||
| open, | ||
| onClose, | ||
| onConfirm, | ||
| isLoading, | ||
| }) => { | ||
| const [stage, setStage] = useState<"consequences" | "tasks" | "resources">( | ||
| "consequences", | ||
| ); | ||
|
|
||
| const onProceed = () => { | ||
| switch (stage) { | ||
| case "resources": | ||
| onConfirm(); | ||
| break; | ||
| case "tasks": | ||
| setStage("resources"); | ||
| break; | ||
| case "consequences": | ||
| setStage("tasks"); | ||
| break; | ||
| } | ||
| }; | ||
|
|
||
| const taskCount = `${checkedTasks.length} ${ | ||
| checkedTasks.length === 1 ? "task" : "tasks" | ||
| }`; | ||
|
|
||
| let confirmText: ReactNode = <>Review selected tasks…</>; | ||
| if (stage === "tasks") { | ||
| confirmText = <>Confirm {taskCount}…</>; | ||
| } | ||
| if (stage === "resources") { | ||
| const workspaceCountText = `${workspaceCount} ${ | ||
| workspaceCount === 1 ? "workspace" : "workspaces" | ||
| }`; | ||
| confirmText = ( | ||
| <> | ||
| Delete {taskCount} and {workspaceCountText} | ||
| </> | ||
| ); | ||
| } | ||
|
|
||
| return ( | ||
| <ConfirmDialog | ||
| type="delete" | ||
| open={open} | ||
| onClose={() => { | ||
| setStage("consequences"); | ||
| onClose(); | ||
| }} | ||
| title={`Delete ${taskCount}`} | ||
| hideCancel | ||
| confirmLoading={isLoading} | ||
| confirmText={confirmText} | ||
| onConfirm={onProceed} | ||
| description={ | ||
| <> | ||
| {stage === "consequences" && <Consequences />} | ||
| {stage === "tasks" && <Tasks tasks={checkedTasks} />} | ||
| {stage === "resources" && ( | ||
| <Resources tasks={checkedTasks} workspaceCount={workspaceCount} /> | ||
| )} | ||
| {/* Preload ServerIcon to prevent flicker on stage 3 */} | ||
| <ServerIcon className="sr-only" aria-hidden /> | ||
| </> | ||
| } | ||
| /> | ||
| ); | ||
| }; | ||
|
|
||
| interface TasksStageProps { | ||
| tasks: readonly Task[]; | ||
| } | ||
|
|
||
| interface ResourcesStageProps { | ||
| tasks: readonly Task[]; | ||
| workspaceCount: number; | ||
| } | ||
|
|
||
| const Consequences: FC = () => { | ||
| return ( | ||
| <> | ||
| <p>Deleting tasks is irreversible!</p> | ||
| <ul className="flex flex-col gap-2 pl-4 mb-0"> | ||
| <li> | ||
| Tasks with associated workspaces will have those workspaces deleted. | ||
| </li> | ||
| <li>Any data stored in task workspaces will be permanently deleted.</li> | ||
| </ul> | ||
| </> | ||
| ); | ||
| }; | ||
|
|
||
| const Tasks: FC<TasksStageProps> = ({ tasks }) => { | ||
| const mostRecent = tasks.reduce( | ||
| (latestSoFar, against) => { | ||
| if (!latestSoFar) { | ||
| return against; | ||
| } | ||
|
|
||
| return new Date(against.updated_at).getTime() > | ||
| new Date(latestSoFar.updated_at).getTime() | ||
| ? against | ||
| : latestSoFar; | ||
| }, | ||
| undefined as Task | undefined, | ||
| ); | ||
|
|
||
| const owners = new Set(tasks.map((it) => it.owner_name)).size; | ||
| const ownersCount = `${owners} ${owners === 1 ? "owner" : "owners"}`; | ||
mafredri marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| return ( | ||
| <> | ||
| <ul className="list-none p-0 border border-solid border-zinc-200 dark:border-zinc-700 rounded-lg overflow-x-hidden overflow-y-auto max-h-[184px]"> | ||
| {tasks.map((task) => ( | ||
| <li | ||
| key={task.id} | ||
| className="py-2 px-4 border-solid border-0 border-b border-zinc-200 dark:border-zinc-700 last:border-b-0" | ||
| > | ||
| <div className="flex items-center justify-between gap-6"> | ||
| <span className="font-medium text-content-primary max-w-[400px] overflow-hidden text-ellipsis whitespace-nowrap"> | ||
| {task.display_name} | ||
| </span> | ||
|
|
||
| <div className="flex flex-col text-sm items-end"> | ||
| <div className="flex items-center gap-2"> | ||
| <span className="whitespace-nowrap">{task.owner_name}</span> | ||
| <UserIcon className="size-icon-sm -m-px" /> | ||
| </div> | ||
| <div className="flex items-center gap-2"> | ||
| <span className="whitespace-nowrap"> | ||
| {dayjs(task.updated_at).fromNow()} | ||
| </span> | ||
| <ClockIcon className="size-icon-xs" /> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </li> | ||
| ))} | ||
| </ul> | ||
| <div className="flex flex-wrap justify-center gap-x-5 gap-y-1.5 text-sm"> | ||
| <div className="flex items-center gap-2"> | ||
| <UserIcon className="size-icon-sm -m-px" /> | ||
| <span>{ownersCount}</span> | ||
| </div> | ||
| {mostRecent && ( | ||
| <div className="flex items-center gap-2"> | ||
| <ClockIcon className="size-icon-xs" /> | ||
| <span>Last updated {dayjs(mostRecent.updated_at).fromNow()}</span> | ||
| </div> | ||
| )} | ||
| </div> | ||
| </> | ||
| ); | ||
| }; | ||
|
|
||
| const Resources: FC<ResourcesStageProps> = ({ tasks, workspaceCount }) => { | ||
| const taskCount = tasks.length; | ||
|
|
||
| return ( | ||
| <div className="flex flex-col gap-4"> | ||
| <p> | ||
| Deleting {taskCount === 1 ? "this task" : "these tasks"} will also | ||
| permanently destroy… | ||
| </p> | ||
| <div className="flex flex-wrap justify-center gap-x-5 gap-y-1.5 text-sm"> | ||
| <div className="flex items-center gap-2"> | ||
| <ServerIcon className="size-icon-sm" /> | ||
| <span> | ||
| {workspaceCount} {workspaceCount === 1 ? "workspace" : "workspaces"} | ||
| </span> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| ); | ||
| }; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.