Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
66c09e8
fix: demui `<Dialog />` (globally)
jakehwll Jul 27, 2026
1b2dfc6
πŸ€– chore(site): restore vite to 8.0.16
jakehwll Jul 27, 2026
23e4814
πŸ€– refactor(site/src): compose dialog prop types from the rendered com…
jakehwll Jul 27, 2026
76adb73
πŸ€– fix(site/src): fix CI lint and unit test after dialog migration
jakehwll Jul 27, 2026
fd21f59
πŸ€– refactor(site/src): finish de-MUI-ing the last two dialogs
jakehwll Jul 27, 2026
e93243b
πŸ€– chore(site): keep vite at 8.0.10
jakehwll Jul 27, 2026
1086bb2
πŸ€– chore(site): drop vite changes from this PR
jakehwll Jul 27, 2026
e7d6f75
πŸ€– fix(site/src): restore dialog scrolling and control schedule checkb…
jakehwll Jul 27, 2026
4ae7a3d
πŸ€– refactor(site/src): address coder-agents-review feedback on dialog …
jakehwll Jul 27, 2026
a0286d1
πŸ€– revert(site/src): keep WorkspaceDeleteDialog warnings orange
jakehwll Jul 27, 2026
37bcd4e
πŸ€– docs(site/src): document ConfirmDialog aria-describedby opt-out
jakehwll Jul 27, 2026
073e313
Merge remote-tracking branch 'origin/main' into jakehwll/demui-dialogs
jakehwll Jul 27, 2026
6cba6a6
πŸ€– refactor(site/src/components/Dialog/ConfirmDialog): require descrip…
jakehwll Jul 28, 2026
0e66cb6
πŸ€– test(site/src): use screen in ConfirmDialog stories
jakehwll Jul 28, 2026
2db38b3
πŸ€– refactor(site/src/components/Dialog): cap dialog height in the prim…
jakehwll Jul 28, 2026
0976c23
Merge remote-tracking branch 'origin/main' into jakehwll/demui-dialogs
jakehwll Jul 28, 2026
c83daeb
Merge branch 'main' into jakehwll/demui-dialogs
jakehwll Jul 29, 2026
0e2ad1f
Merge remote-tracking branch 'origin/main' into jakehwll/demui-dialogs
jakehwll Jul 30, 2026
70dc01f
Merge branch 'main' into jakehwll/demui-dialogs
jakehwll Jul 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions site/src/components/Dialog/ConfirmDialog/ConfirmDialog.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { expect, fn, screen, userEvent } from "storybook/test";
import { ConfirmDialog } from "./ConfirmDialog";

const meta: Meta<typeof ConfirmDialog> = {
title: "components/Dialog/ConfirmDialog",
component: ConfirmDialog,
args: {
onClose: fn(),
onConfirm: fn(),
open: true,
title: "Confirm Dialog",
},
};

export default meta;
type Story = StoryObj<typeof ConfirmDialog>;

export const Example: Story = {
args: {
description: "Do you really want to delete me?",
hideCancel: false,
type: "delete",
},
play: async ({ args }) => {
const dialog = await screen.findByRole("dialog");
await expect(dialog).toBeInTheDocument();

await userEvent.click(screen.getByRole("button", { name: "Cancel" }));
await expect(args.onClose).toHaveBeenCalled();
},
};

export const InfoDialog: Story = {
args: {
description: "Information is cool!",
hideCancel: true,
type: "info",
},
play: async ({ args }) => {
await expect(
screen.queryByRole("button", { name: "Cancel" }),
).not.toBeInTheDocument();

await userEvent.click(screen.getByRole("button", { name: "OK" }));
await expect(args.onConfirm).toHaveBeenCalled();
},
};

export const InfoDialogWithCancel: Story = {
args: {
description: "Information can be cool!",
hideCancel: false,
type: "info",
},
};

export const SuccessDialog: Story = {
args: {
description: "I am successful.",
hideCancel: true,
type: "success",
},
};

export const SuccessDialogWithCancel: Story = {
args: {
description: "I may be successful.",
hideCancel: false,
type: "success",
},
};

export const SuccessDialogLoading: Story = {
args: {
description: "I am successful.",
hideCancel: true,
type: "success",
confirmLoading: true,
},
play: async () => {
// Spinner prefixes the accessible name with "Loading spinner".
await expect(screen.getByRole("button", { name: /ok/i })).toBeDisabled();
},
};

export const ConfirmAction: Story = {
args: {
description: "Do you really want to delete me?",
hideCancel: false,
type: "delete",
confirmText: "CONFIRM",
cancelText: "CANCEL",
},
play: async ({ args }) => {
await userEvent.click(screen.getByRole("button", { name: "CONFIRM" }));
await expect(args.onConfirm).toHaveBeenCalled();
await expect(args.onClose).not.toHaveBeenCalled();
},
};
115 changes: 115 additions & 0 deletions site/src/components/Dialog/ConfirmDialog/ConfirmDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import type { FC, ReactNode } from "react";
import {
Dialog,
DialogActions,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "#/components/Dialog/Dialog";

type ConfirmDialogType = "delete" | "info" | "success";

interface ConfirmDialogTypeConfig {
confirmText: ReactNode;
hideCancel: boolean;
}

const CONFIRM_DIALOG_DEFAULTS: Record<
ConfirmDialogType,
ConfirmDialogTypeConfig
> = {
delete: {
confirmText: "Delete",
hideCancel: false,
},
info: {
confirmText: "OK",
hideCancel: true,
},
success: {
confirmText: "OK",
hideCancel: true,
},
};

export interface ConfirmDialogProps {
readonly title: string;
readonly open: boolean;
readonly onClose: () => void;
readonly description: ReactNode;
readonly cancelText?: string;
readonly confirmText?: ReactNode;
readonly confirmLoading?: boolean;
readonly disabled?: boolean;
/**
* When omitted, `onClose` doubles as the confirm handler, so a `delete`
* dialog without `onConfirm` closes without deleting anything.
*/
Comment thread
jakehwll marked this conversation as resolved.
readonly onConfirm?: () => void;
Comment thread
jakehwll marked this conversation as resolved.
readonly type?: ConfirmDialogType;
/**
* Defaults to shown for "delete", hidden for "info"/"success".
*/
readonly hideCancel?: boolean;
}

/**
* Quick-use dialog for yes/no style confirmations without custom layout.
*/
export const ConfirmDialog: FC<ConfirmDialogProps> = ({
cancelText = "Cancel",
confirmLoading = false,
confirmText,
description,
disabled = false,
hideCancel,
onClose,
onConfirm,
open = false,
title,
type = "info",
}) => {
const defaults = CONFIRM_DIALOG_DEFAULTS[type];
const shouldHideCancel = hideCancel ?? defaults.hideCancel;
const resolvedConfirmText = confirmText ?? defaults.confirmText;
const handleConfirm = onConfirm ?? onClose;

return (
<Dialog
open={open}
onOpenChange={(nextOpen) => {
if (!nextOpen) {
onClose();
}
}}
>
<DialogContent
variant={type === "delete" ? "destructive" : "default"}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok but the annoying thing here is that we also use "confirm" for delete dialogs all over the place. this component and the way we use it is a total mess.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed the confirmText/delete-button semantics across callers are messy. That's broader than this migration's scope (mechanical de-MUI), so leaving it for the dialog design cleanup rather than reshaping the API here. Flagging for that follow-up.

πŸ€– Replied by Coder Agents on behalf of Jake Howell.

data-testid="dialog"
>
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
<DialogDescription asChild>
<div className="text-sm text-content-secondary font-medium [&_strong]:text-content-primary [&_p]:m-0 [&_p+p]:mt-2">
{description}
</div>
</DialogDescription>
</DialogHeader>

<DialogFooter>
Comment thread
jakehwll marked this conversation as resolved.
<DialogActions
cancelText={cancelText}
onCancel={shouldHideCancel ? undefined : onClose}
confirmText={resolvedConfirmText}
confirmLoading={confirmLoading}
confirmDisabled={disabled}
confirmVariant={type === "delete" ? "destructive" : undefined}
onConfirm={handleConfirm}
/>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
69 changes: 69 additions & 0 deletions site/src/components/Dialog/DeleteDialog/DeleteDialog.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { expect, fn, userEvent, within } from "storybook/test";
import { DeleteDialog } from "./DeleteDialog";

const meta: Meta<typeof DeleteDialog> = {
title: "components/Dialog/DeleteDialog",
component: DeleteDialog,
args: {
onCancel: fn(),
onConfirm: fn(),
isOpen: true,
entity: "foo",
name: "MyFoo",
info: "Here's some info about the foo so you know you're deleting the right one.",
},
};

export default meta;

type Story = StoryObj<typeof DeleteDialog>;

export const Idle: Story = {
play: async ({ canvasElement }) => {
const body = within(canvasElement.ownerDocument.body);
await expect(body.getByRole("button", { name: "Delete" })).toBeDisabled();
},
};

export const FilledSuccessfully: Story = {
play: async ({ canvasElement, args }) => {
const user = userEvent.setup();
const body = within(canvasElement.ownerDocument.body);
const input = await body.findByLabelText("Name of the foo to delete");
await user.type(input, "MyFoo");

const confirmButton = body.getByRole("button", { name: "Delete" });
await expect(confirmButton).toBeEnabled();
await user.click(confirmButton);
await expect(args.onConfirm).toHaveBeenCalled();
},
};

export const FilledWrong: Story = {
play: async ({ canvasElement }) => {
const user = userEvent.setup();
const body = within(canvasElement.ownerDocument.body);
const input = await body.findByLabelText("Name of the foo to delete");
await user.type(input, "InvalidFooName");
// Blur so the mismatch error becomes visible.
await user.tab();

await expect(body.getByRole("button", { name: "Delete" })).toBeDisabled();
await expect(
body.getByText("InvalidFooName does not match the name of this foo"),
).toBeVisible();
},
};

export const Loading: Story = {
args: {
confirmLoading: true,
},
play: async ({ canvasElement }) => {
const body = within(canvasElement.ownerDocument.body);
// Spinner prefixes the accessible name with "Loading spinner".
await expect(body.getByRole("button", { name: /delete/i })).toBeDisabled();
await expect(body.getByRole("button", { name: "Cancel" })).toBeDisabled();
},
};
Loading
Loading