mirror of
https://github.com/coder/coder.git
synced 2026-09-21 20:51:01 +08:00
chore(site): migrate all <Dialog />s off MUI (#27506)
> 🤖 This PR was modified by Coder Agents on behalf of Jake Howell. Removes Material UI from every dialog, moving them onto the internal shadcn/radix `Dialog` primitives. After this change there are **no** `@mui/material/Dialog` usages left in `site/src`. ## Changes - Consolidated `components/Dialogs/*` → `components/Dialog/*` and folded the old `ConfirmDeleteDialog` into `ConfirmDialog` (`type="delete"`). - Rewrote `ConfirmDialog`, `DeleteDialog`, `WorkspaceDeleteDialog`, `ScheduleDialog`, and `AnnouncementBannerDialog` onto the internal primitives (native `Input`/`Label`/`Checkbox`/`Link` instead of MUI). - Finished the migration for the last two MUI hold-outs: `UpdateBuildParametersDialog` and `MissingTemplateVariablesDialog`. - Dialog prop types now compose the rendered component's props (`ComponentProps<typeof Dialog>` / MUI `DialogProps`) instead of hand-rolled `{ open; onOpenChange }` shapes. ## Testing AI-Driven manual dogfood sweep in a live instance (premium license), driven in a browser. Each dialog checked for logical rendering (layout, variant styling, buttons, no overlap/blank/console error) and function (open, primary action, cancel/close, guard states): | Dialog / surface | How tested | Result | | --- | --- | --- | | `ConfirmDialog` (delete / info / success) | Token delete, update-confirm, change-version | ✅ | | `DeleteDialog` (type-to-confirm) | Delete user, group, license, provider, OAuth2 app | ✅ | | `WorkspaceDeleteDialog` | Workspace actions → Delete (+ orphan path via failed workspace) | ✅ | | `ScheduleDialog` | Template schedule → dormancy/deletion warning | ✅ | | `AnnouncementBannerDialog` | Deployment → Appearance → New banner (live preview + color) | ✅ | | `ChangeWorkspaceVersionDialog` | Workspace actions → Change version | ✅ | | `DownloadLogsDialog` | Workspace actions → Download logs | ✅ | | Batch delete (workspaces) | Workspaces list → multi-select → Delete | ✅ | | `TemplatePageHeader` delete | Template → Delete (cancelled) | ✅ | | `FileDialog` (create/rename/delete) | Template editor file tree | ✅ | | `MissingTemplateVariablesDialog` *(migrated)* | Editor → add variable → Build | ✅ | | `PublishTemplateVersionDialog` | Editor → Publish | ✅ | | `UpdateBuildParametersDialog` *(migrated)* | Classic flow + required param → workspace Update (renders + submits) | ✅ | | Update-confirmation (`WorkspaceUpdateDialogs`) | Workspace Update | ✅ | | Suspend/activate confirm | Users → member row | ✅ | | `ResetPasswordDialog` | Users → member → Reset password | ✅ | | Token delete confirm | Settings → Tokens | ✅ | | Create-token confirm | Token create flow | ✅ | | SSH key regenerate confirm | Settings → SSH Keys | ✅ | | Change-login-type confirm | Settings → Security | ✅ | | Secret delete | Settings → Secrets | ✅ | | Group delete | Admin → Groups | ✅ | | Org member remove | Admin → Organization → Members | ✅ | | License remove | Deployment → Licenses (cancelled) | ✅ | | Announcement banner delete | Deployment → Appearance | ✅ | | OAuth2 app delete | Deployment → OAuth2 apps | ✅ | | `ModelFormDialogs` (form + delete) | AI → Models | ✅ | | Provider delete | AI → Providers | ✅ | | Gateway key create/delete | AI → Gateway keys | ✅ | | `MCPServerFormDialogs` delete | AI → MCP servers | ✅ | | Personal skill (create form + delete) | Agents → Personal Skills | ✅ | | Spend user-override (add + delete) | AI → Spend | ✅ | Human tested: - [x] template version promote/archive - [x] external-auth/OAuth2-provider delete - [x] custom-role delete - [x] cancel-provisioner-job Things that have a chance to bleed: - tasks dialogs - dormant inline confirm Should be known that each of these renders through the already-verified `ConfirmDialog`/`DeleteDialog`, so the underlying component is covered even where the specific trigger wasn't reachable. <details> <summary>Plan & decision log</summary> **Goal:** finish the de-MUI migration everywhere and confirm every affected modal renders logically and functions. **Phase 1 - complete the migration** - Confirmed only two files still rendered MUI `Dialog` (`UpdateBuildParametersDialog`, `MissingTemplateVariablesDialog`); ported both to the internal primitives, preserving the `{ open, onClose, ... }` public API (mapped to `onOpenChange` internally) so call sites were unchanged. radix now wires `aria-labelledby`/`aria-describedby`, removing a duplicated element id. **Phase 2 - sighting sweep (live browser, premium license)** - Batch A (workspaces/tasks): 4 PASS, rest state-gated. - Batch B (templates): `MissingTemplateVariablesDialog`, `FileDialog`, `PublishTemplateVersionDialog`, template delete - all PASS. - `UpdateBuildParametersDialog`: reached by enabling classic parameter flow + pushing a version with a required parameter - PASS (renders + submits). - Batch C (users/org/settings): 10 PASS. - Batch D (deployment/AI): 10 PASS. **Decisions** - Kept `ConfirmDialog`-wrapper prop types explicit (composing `DialogProps` there reintroduced a MUI smell). - Dropped the unused `ConfirmDialogType` export (knip) and updated the `WorkspacePage` orphan-delete test: the radix `Checkbox` puts the test id on the `role=checkbox` button itself, so the previous `within(...).getByRole` no longer matched. </details>
This commit is contained in:
@@ -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();
|
||||
},
|
||||
};
|
||||
@@ -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.
|
||||
*/
|
||||
readonly onConfirm?: () => void;
|
||||
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"}
|
||||
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>
|
||||
<DialogActions
|
||||
cancelText={cancelText}
|
||||
onCancel={shouldHideCancel ? undefined : onClose}
|
||||
confirmText={resolvedConfirmText}
|
||||
confirmLoading={confirmLoading}
|
||||
confirmDisabled={disabled}
|
||||
confirmVariant={type === "delete" ? "destructive" : undefined}
|
||||
onConfirm={handleConfirm}
|
||||
/>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -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();
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,144 @@
|
||||
import { type FC, type FormEvent, useId, useState } from "react";
|
||||
import { Alert } from "#/components/Alert/Alert";
|
||||
import { Button } from "#/components/Button/Button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "#/components/Dialog/Dialog";
|
||||
import { Input } from "#/components/Input/Input";
|
||||
import { Label } from "#/components/Label/Label";
|
||||
import { Spinner } from "#/components/Spinner/Spinner";
|
||||
|
||||
interface DeleteDialogProps {
|
||||
isOpen: boolean;
|
||||
onConfirm: () => void;
|
||||
onCancel: () => void;
|
||||
entity: string;
|
||||
name: string;
|
||||
info?: string;
|
||||
confirmLoading?: boolean;
|
||||
verb?: string;
|
||||
title?: string;
|
||||
label?: string;
|
||||
confirmText?: string;
|
||||
}
|
||||
|
||||
export const DeleteDialog: FC<DeleteDialogProps> = ({
|
||||
isOpen,
|
||||
onCancel,
|
||||
onConfirm,
|
||||
entity,
|
||||
info,
|
||||
name,
|
||||
confirmLoading = false,
|
||||
// Optional overrides for verbiage, e.g. "unlinking" vs "deleting".
|
||||
verb,
|
||||
title,
|
||||
label,
|
||||
confirmText = "Delete",
|
||||
}) => {
|
||||
const confirmId = useId();
|
||||
const errorId = `${confirmId}-error`;
|
||||
|
||||
const [userConfirmationText, setUserConfirmationText] = useState("");
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
|
||||
const deletionConfirmed = name === userConfirmationText;
|
||||
const hasError = !deletionConfirmed && userConfirmationText.length > 0;
|
||||
const displayErrorMessage = hasError && !isFocused;
|
||||
|
||||
const resetConfirmation = () => {
|
||||
setUserConfirmationText("");
|
||||
setIsFocused(false);
|
||||
};
|
||||
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
if (!open) {
|
||||
resetConfirmation();
|
||||
onCancel();
|
||||
}
|
||||
};
|
||||
|
||||
const onSubmit = (event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
if (deletionConfirmed && !confirmLoading) {
|
||||
onConfirm();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={handleOpenChange}>
|
||||
<DialogContent variant="destructive" data-testid="dialog">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{title ?? `Delete ${entity}`}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{verb ?? "Deleting"} this {entity} is irreversible!
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
{info && (
|
||||
<Alert severity="warning" prominent>
|
||||
{info}
|
||||
</Alert>
|
||||
)}
|
||||
<p className="m-0 text-sm text-content-secondary font-medium">
|
||||
Type <strong className="text-content-primary">{name}</strong> below
|
||||
to confirm.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form className="flex flex-col gap-6" onSubmit={onSubmit}>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor={confirmId}>
|
||||
{label ?? `Name of the ${entity} to delete`}
|
||||
</Label>
|
||||
<Input
|
||||
id={confirmId}
|
||||
name="confirmation"
|
||||
autoComplete="off"
|
||||
autoFocus
|
||||
placeholder={name}
|
||||
value={userConfirmationText}
|
||||
onChange={(event) => setUserConfirmationText(event.target.value)}
|
||||
onFocus={() => setIsFocused(true)}
|
||||
onBlur={() => setIsFocused(false)}
|
||||
aria-invalid={displayErrorMessage}
|
||||
aria-describedby={displayErrorMessage ? errorId : undefined}
|
||||
data-testid="delete-dialog-name-confirmation"
|
||||
/>
|
||||
{displayErrorMessage && (
|
||||
<span id={errorId} className="text-xs text-content-destructive">
|
||||
{userConfirmationText} does not match the name of this {entity}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
disabled={confirmLoading}
|
||||
onClick={() => handleOpenChange(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="destructive"
|
||||
disabled={!deletionConfirmed || confirmLoading}
|
||||
data-testid="confirm-button"
|
||||
>
|
||||
<Spinner loading={confirmLoading} />
|
||||
{confirmText}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -33,7 +33,7 @@ const DialogOverlay: React.FC<
|
||||
};
|
||||
|
||||
const dialogVariants = cva(
|
||||
`fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg gap-6
|
||||
`fixed left-[50%] top-[50%] z-50 grid max-h-[90vh] w-full max-w-lg gap-6 overflow-y-auto
|
||||
border border-solid bg-surface-primary p-8 shadow-lg duration-200 sm:rounded-lg
|
||||
translate-x-[-50%] translate-y-[-50%] outline-none
|
||||
data-[state=open]:animate-in data-[state=closed]:animate-out
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
import type { FC, ReactNode } from "react";
|
||||
import { Button } from "#/components/Button/Button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "#/components/Dialog/Dialog";
|
||||
import { Spinner } from "#/components/Spinner/Spinner";
|
||||
|
||||
interface ConfirmDeleteDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
/** The entity type being deleted, shown in the title and button. */
|
||||
entity: string;
|
||||
/**
|
||||
* Optional description. Defaults to "Are you sure you want to
|
||||
* delete this {entity}? This action is irreversible."
|
||||
*/
|
||||
description?: ReactNode;
|
||||
children?: ReactNode;
|
||||
onConfirm: () => void;
|
||||
isPending?: boolean;
|
||||
}
|
||||
|
||||
export const ConfirmDeleteDialog: FC<ConfirmDeleteDialogProps> = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
entity,
|
||||
description,
|
||||
children,
|
||||
onConfirm,
|
||||
isPending = false,
|
||||
}) => (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent variant="destructive">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Delete {entity}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{description ??
|
||||
`Are you sure you want to delete this ${entity}? This action is irreversible.`}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{children}
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
disabled={isPending}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="destructive" onClick={onConfirm} disabled={isPending}>
|
||||
{isPending && <Spinner className="size-4" loading />}
|
||||
Delete {entity}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
@@ -1,66 +0,0 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { action } from "storybook/actions";
|
||||
import { ConfirmDialog } from "./ConfirmDialog";
|
||||
|
||||
const meta: Meta<typeof ConfirmDialog> = {
|
||||
title: "components/Dialogs/ConfirmDialog",
|
||||
component: ConfirmDialog,
|
||||
args: {
|
||||
onClose: action("onClose"),
|
||||
onConfirm: action("onConfirm"),
|
||||
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",
|
||||
},
|
||||
};
|
||||
|
||||
export const InfoDialog: Story = {
|
||||
args: {
|
||||
description: "Information is cool!",
|
||||
hideCancel: true,
|
||||
type: "info",
|
||||
},
|
||||
};
|
||||
|
||||
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,
|
||||
},
|
||||
};
|
||||
@@ -1,47 +0,0 @@
|
||||
import { fireEvent, screen } from "@testing-library/react";
|
||||
import { renderComponent } from "#/testHelpers/renderHelpers";
|
||||
import { ConfirmDialog } from "./ConfirmDialog";
|
||||
|
||||
describe("ConfirmDialog", () => {
|
||||
it("onClose is called when cancelled", () => {
|
||||
// Given
|
||||
const onCloseMock = vi.fn();
|
||||
const props = {
|
||||
cancelText: "CANCEL",
|
||||
hideCancel: false,
|
||||
onClose: onCloseMock,
|
||||
open: true,
|
||||
title: "Test",
|
||||
};
|
||||
|
||||
// When
|
||||
renderComponent(<ConfirmDialog {...props} />);
|
||||
fireEvent.click(screen.getByText("CANCEL"));
|
||||
|
||||
// Then
|
||||
expect(onCloseMock).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it("onConfirm is called when confirmed", () => {
|
||||
// Given
|
||||
const onCloseMock = vi.fn();
|
||||
const onConfirmMock = vi.fn();
|
||||
const props = {
|
||||
cancelText: "CANCEL",
|
||||
confirmText: "CONFIRM",
|
||||
hideCancel: false,
|
||||
onClose: onCloseMock,
|
||||
onConfirm: onConfirmMock,
|
||||
open: true,
|
||||
title: "Test",
|
||||
};
|
||||
|
||||
// When
|
||||
renderComponent(<ConfirmDialog {...props} />);
|
||||
fireEvent.click(screen.getByText("CONFIRM"));
|
||||
|
||||
// Then
|
||||
expect(onCloseMock).toBeCalledTimes(0);
|
||||
expect(onConfirmMock).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -1,145 +0,0 @@
|
||||
import type { Interpolation, Theme } from "@emotion/react";
|
||||
import DialogActions from "@mui/material/DialogActions";
|
||||
import type { FC, ReactNode } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogActionButtons,
|
||||
type DialogActionButtonsProps,
|
||||
} from "../Dialog";
|
||||
import type { ConfirmDialogType } from "../types";
|
||||
|
||||
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
|
||||
extends Omit<DialogActionButtonsProps, "color" | "onCancel"> {
|
||||
readonly description?: ReactNode;
|
||||
/**
|
||||
* hideCancel hides the cancel button when set true, and shows the cancel
|
||||
* button when set to false. When undefined:
|
||||
* - cancel is not displayed for "info" dialogs
|
||||
* - cancel is displayed for "delete" dialogs
|
||||
*/
|
||||
readonly hideCancel?: boolean;
|
||||
/**
|
||||
* onClose is called when canceling (if cancel is showing).
|
||||
*
|
||||
* Additionally, if onConfirm is not defined onClose will be used in its place
|
||||
* when confirming.
|
||||
*/
|
||||
readonly onClose: () => void;
|
||||
readonly open: boolean;
|
||||
readonly title: string;
|
||||
}
|
||||
|
||||
const styles = {
|
||||
dialogWrapper: (theme) => ({
|
||||
"& .MuiPaper-root": {
|
||||
background: theme.palette.background.paper,
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
width: "100%",
|
||||
maxWidth: 440,
|
||||
},
|
||||
"& .MuiDialogActions-spacing": {
|
||||
padding: "0 40px 40px",
|
||||
},
|
||||
}),
|
||||
dialogContent: (theme) => ({
|
||||
color: theme.palette.text.secondary,
|
||||
padding: "40px 40px 20px",
|
||||
}),
|
||||
dialogTitle: (theme) => ({
|
||||
margin: 0,
|
||||
marginBottom: 16,
|
||||
color: theme.palette.text.primary,
|
||||
fontWeight: 400,
|
||||
fontSize: 20,
|
||||
}),
|
||||
dialogDescription: (theme) => ({
|
||||
color: theme.palette.text.secondary,
|
||||
lineHeight: "160%",
|
||||
fontSize: 16,
|
||||
|
||||
"& strong": {
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
|
||||
"& p:not(.MuiFormHelperText-root)": {
|
||||
margin: 0,
|
||||
},
|
||||
|
||||
"& > p": {
|
||||
margin: "8px 0",
|
||||
},
|
||||
}),
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
|
||||
/**
|
||||
* Quick-use version of the Dialog component with slightly alternative styles,
|
||||
* great to use for dialogs that don't have any interaction beyond yes / no.
|
||||
*/
|
||||
export const ConfirmDialog: FC<ConfirmDialogProps> = ({
|
||||
cancelText,
|
||||
confirmLoading,
|
||||
confirmText,
|
||||
description,
|
||||
disabled = false,
|
||||
hideCancel,
|
||||
onClose,
|
||||
onConfirm,
|
||||
open = false,
|
||||
title,
|
||||
type = "info",
|
||||
}) => {
|
||||
const defaults = CONFIRM_DIALOG_DEFAULTS[type];
|
||||
|
||||
if (typeof hideCancel === "undefined") {
|
||||
hideCancel = defaults.hideCancel;
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
css={styles.dialogWrapper}
|
||||
onClose={onClose}
|
||||
open={open}
|
||||
data-testid="dialog"
|
||||
>
|
||||
<div css={styles.dialogContent}>
|
||||
<h3 css={styles.dialogTitle}>{title}</h3>
|
||||
{description && <div css={styles.dialogDescription}>{description}</div>}
|
||||
</div>
|
||||
|
||||
<DialogActions>
|
||||
<DialogActionButtons
|
||||
cancelText={cancelText}
|
||||
confirmLoading={confirmLoading}
|
||||
confirmText={confirmText || defaults.confirmText}
|
||||
disabled={disabled}
|
||||
onCancel={!hideCancel ? onClose : undefined}
|
||||
onConfirm={onConfirm || onClose}
|
||||
type={type}
|
||||
/>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -1,47 +0,0 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { action } from "storybook/actions";
|
||||
import { userEvent, within } from "storybook/test";
|
||||
import { DeleteDialog } from "./DeleteDialog";
|
||||
|
||||
const meta: Meta<typeof DeleteDialog> = {
|
||||
title: "components/Dialogs/DeleteDialog",
|
||||
component: DeleteDialog,
|
||||
args: {
|
||||
onCancel: action("onClose"),
|
||||
onConfirm: action("onConfirm"),
|
||||
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 = {};
|
||||
|
||||
export const FilledSuccessfully: 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, "MyFoo");
|
||||
},
|
||||
};
|
||||
|
||||
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");
|
||||
},
|
||||
};
|
||||
|
||||
export const Loading: Story = {
|
||||
args: {
|
||||
confirmLoading: true,
|
||||
},
|
||||
};
|
||||
@@ -1,72 +0,0 @@
|
||||
import { screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { act } from "react";
|
||||
import { renderComponent } from "#/testHelpers/renderHelpers";
|
||||
import { DeleteDialog } from "./DeleteDialog";
|
||||
|
||||
const inputTestId = "delete-dialog-name-confirmation";
|
||||
|
||||
async function fillInputField(inputElement: HTMLElement, text: string) {
|
||||
// 2023-10-06 - There's something wonky with MUI's ConfirmDialog that causes
|
||||
// its state to update after a typing event gets fired, and React Testing
|
||||
// Library isn't able to catch it, making React DOM freak out because an
|
||||
// "unexpected" state change happened. It won't fail the test, but it makes
|
||||
// the console look really scary because it'll spit out a big warning message.
|
||||
// Tried everything under the sun to catch the state changes the proper way,
|
||||
// but the only way to get around it for now might be to manually make React
|
||||
// DOM aware of the changes
|
||||
return act(() => userEvent.type(inputElement, text));
|
||||
}
|
||||
|
||||
describe("DeleteDialog", () => {
|
||||
it("disables confirm button when the text field is empty", () => {
|
||||
renderComponent(
|
||||
<DeleteDialog
|
||||
isOpen
|
||||
onConfirm={vi.fn()}
|
||||
onCancel={vi.fn()}
|
||||
entity="template"
|
||||
name="MyTemplate"
|
||||
/>,
|
||||
);
|
||||
|
||||
const confirmButton = screen.getByRole("button", { name: "Delete" });
|
||||
expect(confirmButton).toBeDisabled();
|
||||
});
|
||||
|
||||
it("disables confirm button when the text field is filled incorrectly", async () => {
|
||||
renderComponent(
|
||||
<DeleteDialog
|
||||
isOpen
|
||||
onConfirm={vi.fn()}
|
||||
onCancel={vi.fn()}
|
||||
entity="template"
|
||||
name="MyTemplate"
|
||||
/>,
|
||||
);
|
||||
|
||||
const textField = screen.getByTestId(inputTestId);
|
||||
await fillInputField(textField, "MyTemplateButWrong");
|
||||
|
||||
const confirmButton = screen.getByRole("button", { name: "Delete" });
|
||||
expect(confirmButton).toBeDisabled();
|
||||
});
|
||||
|
||||
it("enables confirm button when the text field is filled correctly", async () => {
|
||||
renderComponent(
|
||||
<DeleteDialog
|
||||
isOpen
|
||||
onConfirm={vi.fn()}
|
||||
onCancel={vi.fn()}
|
||||
entity="template"
|
||||
name="MyTemplate"
|
||||
/>,
|
||||
);
|
||||
|
||||
const textField = screen.getByTestId(inputTestId);
|
||||
await fillInputField(textField, "MyTemplate");
|
||||
|
||||
const confirmButton = screen.getByRole("button", { name: "Delete" });
|
||||
expect(confirmButton).not.toBeDisabled();
|
||||
});
|
||||
});
|
||||
@@ -1,108 +0,0 @@
|
||||
import TextField from "@mui/material/TextField";
|
||||
import { useId, useState } from "react";
|
||||
import { Alert } from "#/components/Alert/Alert";
|
||||
import { ConfirmDialog } from "../ConfirmDialog/ConfirmDialog";
|
||||
|
||||
interface DeleteDialogProps {
|
||||
isOpen: boolean;
|
||||
onConfirm: () => void;
|
||||
onCancel: () => void;
|
||||
entity: string;
|
||||
name: string;
|
||||
info?: string;
|
||||
confirmLoading?: boolean;
|
||||
verb?: string;
|
||||
title?: string;
|
||||
label?: string;
|
||||
confirmText?: string;
|
||||
}
|
||||
|
||||
export const DeleteDialog: React.FC<DeleteDialogProps> = ({
|
||||
isOpen,
|
||||
onCancel,
|
||||
onConfirm,
|
||||
entity,
|
||||
info,
|
||||
name,
|
||||
confirmLoading,
|
||||
// All optional to change the verbiage. For example, "unlinking" vs "deleting"
|
||||
verb,
|
||||
title,
|
||||
label,
|
||||
confirmText,
|
||||
}) => {
|
||||
const hookId = useId();
|
||||
|
||||
const [userConfirmationText, setUserConfirmationText] = useState("");
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
|
||||
const deletionConfirmed = name === userConfirmationText;
|
||||
const onSubmit = (event: React.SubmitEvent) => {
|
||||
event.preventDefault();
|
||||
if (deletionConfirmed) {
|
||||
onConfirm();
|
||||
}
|
||||
};
|
||||
|
||||
const hasError = !deletionConfirmed && userConfirmationText.length > 0;
|
||||
const displayErrorMessage = hasError && !isFocused;
|
||||
const inputColor = hasError ? "error" : "primary";
|
||||
|
||||
return (
|
||||
<ConfirmDialog
|
||||
type="delete"
|
||||
hideCancel={false}
|
||||
open={isOpen}
|
||||
title={title ?? `Delete ${entity}`}
|
||||
onConfirm={onConfirm}
|
||||
onClose={onCancel}
|
||||
confirmLoading={confirmLoading}
|
||||
disabled={!deletionConfirmed}
|
||||
confirmText={confirmText}
|
||||
description={
|
||||
<>
|
||||
<div className="flex flex-col gap-3">
|
||||
<p>
|
||||
{verb ?? "Deleting"} this {entity} is irreversible!
|
||||
</p>
|
||||
{Boolean(info) && (
|
||||
<Alert severity="warning" prominent>
|
||||
{info}
|
||||
</Alert>
|
||||
)}
|
||||
<p>
|
||||
Type <strong>{name}</strong> below to confirm.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={onSubmit}>
|
||||
<TextField
|
||||
fullWidth
|
||||
autoFocus
|
||||
className="mt-6"
|
||||
name="confirmation"
|
||||
autoComplete="off"
|
||||
id={`${hookId}-confirm`}
|
||||
placeholder={name}
|
||||
value={userConfirmationText}
|
||||
onChange={(event) => setUserConfirmationText(event.target.value)}
|
||||
onFocus={() => setIsFocused(true)}
|
||||
onBlur={() => setIsFocused(false)}
|
||||
label={label ?? `Name of the ${entity} to delete`}
|
||||
color={inputColor}
|
||||
error={displayErrorMessage}
|
||||
helperText={
|
||||
displayErrorMessage &&
|
||||
`${userConfirmationText} does not match the name of this ${entity}`
|
||||
}
|
||||
InputProps={{ color: inputColor }}
|
||||
inputProps={{
|
||||
"data-testid": "delete-dialog-name-confirmation",
|
||||
}}
|
||||
/>
|
||||
</form>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,70 +0,0 @@
|
||||
import MuiDialog, { type DialogProps } from "@mui/material/Dialog";
|
||||
import type { FC, ReactNode } from "react";
|
||||
import { Button } from "#/components/Button/Button";
|
||||
import { Spinner } from "#/components/Spinner/Spinner";
|
||||
import type { ConfirmDialogType } from "./types";
|
||||
|
||||
export interface DialogActionButtonsProps {
|
||||
/** Text to display in the cancel button */
|
||||
cancelText?: string;
|
||||
/** Text to display in the confirm button */
|
||||
confirmText?: ReactNode;
|
||||
/** Whether or not confirm is loading, also disables cancel when true */
|
||||
confirmLoading?: boolean;
|
||||
/** Whether or not the submit button is disabled */
|
||||
disabled?: boolean;
|
||||
/** Called when cancel is clicked */
|
||||
onCancel?: () => void;
|
||||
/** Called when confirm is clicked */
|
||||
onConfirm?: () => void;
|
||||
type?: ConfirmDialogType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Quickly handles most modals actions, some combination of a cancel and confirm button
|
||||
*/
|
||||
export const DialogActionButtons: FC<DialogActionButtonsProps> = ({
|
||||
cancelText = "Cancel",
|
||||
confirmText = "Confirm",
|
||||
confirmLoading = false,
|
||||
disabled = false,
|
||||
onCancel,
|
||||
onConfirm,
|
||||
type = "info",
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
{onCancel && (
|
||||
<Button
|
||||
disabled={confirmLoading}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onCancel();
|
||||
}}
|
||||
variant="outline"
|
||||
>
|
||||
{cancelText}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{onConfirm && (
|
||||
<Button
|
||||
variant={type === "delete" ? "destructive" : undefined}
|
||||
disabled={confirmLoading || disabled}
|
||||
onClick={onConfirm}
|
||||
data-testid="confirm-button"
|
||||
type="submit"
|
||||
>
|
||||
<Spinner loading={confirmLoading} />
|
||||
{confirmText}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Re-export of MUI's Dialog component, for convenience.
|
||||
* @link See original documentation here: https://mui.com/material-ui/react-dialog/
|
||||
*/
|
||||
export { type DialogProps, MuiDialog as Dialog };
|
||||
@@ -1 +0,0 @@
|
||||
export type ConfirmDialogType = "delete" | "info" | "success";
|
||||
@@ -1,7 +1,7 @@
|
||||
import { EllipsisVerticalIcon } from "lucide-react";
|
||||
import { type FC, useId, useState } from "react";
|
||||
import { Button } from "#/components/Button/Button";
|
||||
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
|
||||
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
|
||||
@@ -4,7 +4,7 @@ import { toast } from "sonner";
|
||||
import { API } from "#/api/api";
|
||||
import { getErrorDetail, getErrorMessage } from "#/api/errors";
|
||||
import type { Task } from "#/api/typesGenerated";
|
||||
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
|
||||
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
|
||||
|
||||
type TaskDeleteDialogProps = {
|
||||
open: boolean;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useFormik } from "formik";
|
||||
import { FrownIcon, MehIcon, SmileIcon } from "lucide-react";
|
||||
import type { FC, HTMLProps, ReactNode } from "react";
|
||||
import type { ComponentProps, FC, HTMLProps, ReactNode } from "react";
|
||||
import { useMutation } from "react-query";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "#/components/Dialog/Dialog";
|
||||
import type { DialogProps } from "#/components/Dialogs/Dialog";
|
||||
import { Spinner } from "#/components/Spinner/Spinner";
|
||||
import { Textarea } from "#/components/Textarea/Textarea";
|
||||
|
||||
@@ -28,7 +27,7 @@ type TaskFeedbackFormValues = {
|
||||
comment: string;
|
||||
};
|
||||
|
||||
type TaskFeedbackDialogProps = DialogProps & {
|
||||
type TaskFeedbackDialogProps = ComponentProps<typeof Dialog> & {
|
||||
taskId: string;
|
||||
};
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import type { FC } from "react";
|
||||
import type { Workspace } from "#/api/typesGenerated";
|
||||
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
|
||||
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
|
||||
|
||||
interface WorkspaceBuildCancelDialogProps {
|
||||
open: boolean;
|
||||
|
||||
@@ -16,8 +16,7 @@ import {
|
||||
ComboboxList,
|
||||
ComboboxTrigger,
|
||||
} from "#/components/Combobox/Combobox";
|
||||
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
|
||||
import type { DialogProps } from "#/components/Dialogs/Dialog";
|
||||
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
|
||||
import type { SelectFilterOption } from "#/components/Filter/SelectFilter";
|
||||
import { FormFields } from "#/components/Form/Form";
|
||||
import { Loader } from "#/components/Loader/Loader";
|
||||
@@ -26,7 +25,8 @@ import { TemplateUpdateMessage } from "#/modules/templates/TemplateUpdateMessage
|
||||
import { cn } from "#/utils/cn";
|
||||
import { createDayString } from "#/utils/createDayString";
|
||||
|
||||
type ChangeWorkspaceVersionDialogProps = DialogProps & {
|
||||
type ChangeWorkspaceVersionDialogProps = {
|
||||
open: boolean;
|
||||
workspace: Workspace;
|
||||
onClose: () => void;
|
||||
onConfirm: (version: TemplateVersion) => void;
|
||||
@@ -34,7 +34,7 @@ type ChangeWorkspaceVersionDialogProps = DialogProps & {
|
||||
|
||||
export const ChangeWorkspaceVersionDialog: FC<
|
||||
ChangeWorkspaceVersionDialogProps
|
||||
> = ({ workspace, onClose, onConfirm, ...dialogProps }) => {
|
||||
> = ({ workspace, onClose, onConfirm, open }) => {
|
||||
const { data: versions } = useQuery({
|
||||
...templateVersions(workspace.template_id),
|
||||
select: (data) => [...data].reverse(),
|
||||
@@ -65,7 +65,7 @@ export const ChangeWorkspaceVersionDialog: FC<
|
||||
|
||||
return (
|
||||
<ConfirmDialog
|
||||
{...dialogProps}
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
onConfirm={() => {
|
||||
if (newVersion) {
|
||||
|
||||
@@ -10,7 +10,7 @@ import { Alert } from "#/components/Alert/Alert";
|
||||
import {
|
||||
ConfirmDialog,
|
||||
type ConfirmDialogProps,
|
||||
} from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
|
||||
} from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
|
||||
import { Skeleton } from "#/components/Skeleton/Skeleton";
|
||||
import { cn } from "#/utils/cn";
|
||||
import { getWorkspaceAgents } from "#/utils/workspace";
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { expect, fn, userEvent, within } from "storybook/test";
|
||||
import {
|
||||
MockFailedWorkspace,
|
||||
MockTaskWorkspace,
|
||||
@@ -20,13 +21,27 @@ const meta: Meta<typeof WorkspaceDeleteDialog> = {
|
||||
},
|
||||
canDeleteFailedWorkspace: false,
|
||||
isOpen: true,
|
||||
onCancel: fn(),
|
||||
onConfirm: fn(),
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof WorkspaceDeleteDialog>;
|
||||
|
||||
export const Example: Story = {};
|
||||
export const Example: Story = {
|
||||
play: async ({ canvasElement, args }) => {
|
||||
const body = within(canvasElement.ownerDocument.body);
|
||||
const confirm = body.getByTestId("delete-dialog-name-confirmation");
|
||||
const deleteButton = body.getByRole("button", { name: "Delete" });
|
||||
|
||||
await expect(deleteButton).toBeDisabled();
|
||||
await userEvent.type(confirm, MockWorkspace.name);
|
||||
await expect(deleteButton).toBeEnabled();
|
||||
await userEvent.click(deleteButton);
|
||||
await expect(args.onConfirm).toHaveBeenCalledWith(false);
|
||||
},
|
||||
};
|
||||
|
||||
// Should look the same as `Example`
|
||||
export const Unhealthy: Story = {
|
||||
@@ -48,10 +63,43 @@ export const UnhealthyAdminView: Story = {
|
||||
workspace: MockFailedWorkspace,
|
||||
canDeleteFailedWorkspace: true,
|
||||
},
|
||||
play: async ({ canvasElement, args }) => {
|
||||
const body = within(canvasElement.ownerDocument.body);
|
||||
const orphan = body.getByTestId("orphan-checkbox");
|
||||
const confirm = body.getByTestId("delete-dialog-name-confirmation");
|
||||
|
||||
await userEvent.click(orphan);
|
||||
await userEvent.type(confirm, MockFailedWorkspace.name);
|
||||
await userEvent.click(body.getByRole("button", { name: "Delete" }));
|
||||
await expect(args.onConfirm).toHaveBeenCalledWith(true);
|
||||
},
|
||||
};
|
||||
|
||||
export const WithTask: Story = {
|
||||
args: {
|
||||
workspace: MockTaskWorkspace,
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const body = within(canvasElement.ownerDocument.body);
|
||||
await expect(
|
||||
body.getByText("This workspace is related to a task"),
|
||||
).toBeInTheDocument();
|
||||
await expect(
|
||||
body.getByRole("link", { name: /this task/i }),
|
||||
).toBeInTheDocument();
|
||||
},
|
||||
};
|
||||
|
||||
export const FilledWrong: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const body = within(canvasElement.ownerDocument.body);
|
||||
const confirm = body.getByTestId("delete-dialog-name-confirmation");
|
||||
|
||||
await userEvent.type(confirm, "wrong-name");
|
||||
await userEvent.tab();
|
||||
await expect(
|
||||
body.getByText("wrong-name does not match the name of this workspace"),
|
||||
).toBeVisible();
|
||||
await expect(body.getByRole("button", { name: "Delete" })).toBeDisabled();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import type { Interpolation, Theme } from "@emotion/react";
|
||||
import Checkbox from "@mui/material/Checkbox";
|
||||
import Link from "@mui/material/Link";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import dayjs from "dayjs";
|
||||
import { type FC, type FormEvent, useId, useState } from "react";
|
||||
import type {
|
||||
CreateWorkspaceBuildRequest,
|
||||
Workspace,
|
||||
} from "#/api/typesGenerated";
|
||||
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
|
||||
import { Checkbox } from "#/components/Checkbox/Checkbox";
|
||||
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
|
||||
import { Input } from "#/components/Input/Input";
|
||||
import { Label } from "#/components/Label/Label";
|
||||
import { Link } from "#/components/Link/Link";
|
||||
import { docs } from "#/utils/docs";
|
||||
|
||||
const warnBoxClassName =
|
||||
"mt-6 flex gap-2 rounded-lg border border-solid border-border-warning bg-surface-orange p-3 leading-snug text-content-warning";
|
||||
|
||||
interface WorkspaceDeleteDialogProps {
|
||||
workspace: Workspace;
|
||||
canDeleteFailedWorkspace: boolean;
|
||||
@@ -26,13 +29,19 @@ export const WorkspaceDeleteDialog: FC<WorkspaceDeleteDialogProps> = ({
|
||||
onCancel,
|
||||
onConfirm,
|
||||
}) => {
|
||||
const hookId = useId();
|
||||
const confirmId = useId();
|
||||
const errorId = `${confirmId}-error`;
|
||||
const orphanId = `${confirmId}-orphan`;
|
||||
|
||||
const [userConfirmationText, setUserConfirmationText] = useState("");
|
||||
const [orphanWorkspace, setOrphanWorkspace] =
|
||||
useState<CreateWorkspaceBuildRequest["orphan"]>(false);
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
|
||||
const deletionConfirmed = workspace.name === userConfirmationText;
|
||||
const hasError = !deletionConfirmed && userConfirmationText.length > 0;
|
||||
const displayErrorMessage = hasError && !isFocused;
|
||||
|
||||
const onSubmit = (event: FormEvent) => {
|
||||
event.preventDefault();
|
||||
if (deletionConfirmed) {
|
||||
@@ -40,9 +49,6 @@ export const WorkspaceDeleteDialog: FC<WorkspaceDeleteDialogProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const hasError = !deletionConfirmed && userConfirmationText.length > 0;
|
||||
const displayErrorMessage = hasError && !isFocused;
|
||||
const inputColor = hasError ? "error" : "primary";
|
||||
// Orphaning is sort of a "last resort" that should really only
|
||||
// be used under the following circumstances:
|
||||
// a) Terraform is failing to apply while deleting, which
|
||||
@@ -69,14 +75,18 @@ export const WorkspaceDeleteDialog: FC<WorkspaceDeleteDialogProps> = ({
|
||||
disabled={!deletionConfirmed}
|
||||
description={
|
||||
<>
|
||||
<div css={styles.workspaceInfo}>
|
||||
<div className="flex items-center justify-between rounded-md border border-solid border-border p-4 mb-5 leading-snug">
|
||||
<div>
|
||||
<p className="name">{workspace.name}</p>
|
||||
<p className="label">workspace</p>
|
||||
<p className="m-0 text-base font-semibold text-content-primary">
|
||||
{workspace.name}
|
||||
</p>
|
||||
<p className="m-0 text-xs text-content-secondary">workspace</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="info">{dayjs(workspace.created_at).fromNow()}</p>
|
||||
<p className="label">created</p>
|
||||
<p className="m-0 text-xs font-medium text-content-primary">
|
||||
{dayjs(workspace.created_at).fromNow()}
|
||||
</p>
|
||||
<p className="m-0 text-xs text-content-secondary">created</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -86,39 +96,40 @@ export const WorkspaceDeleteDialog: FC<WorkspaceDeleteDialogProps> = ({
|
||||
confirm:
|
||||
</p>
|
||||
|
||||
<form onSubmit={onSubmit}>
|
||||
<TextField
|
||||
fullWidth
|
||||
autoFocus
|
||||
className="mt-8"
|
||||
<form className="mt-2 flex flex-col gap-2" onSubmit={onSubmit}>
|
||||
<Label htmlFor={confirmId}>Workspace name</Label>
|
||||
<Input
|
||||
id={confirmId}
|
||||
name="confirmation"
|
||||
autoComplete="off"
|
||||
id={`${hookId}-confirm`}
|
||||
autoFocus
|
||||
placeholder={workspace.name}
|
||||
value={userConfirmationText}
|
||||
onChange={(event) => setUserConfirmationText(event.target.value)}
|
||||
onFocus={() => setIsFocused(true)}
|
||||
onBlur={() => setIsFocused(false)}
|
||||
label="Workspace name"
|
||||
color={inputColor}
|
||||
error={displayErrorMessage}
|
||||
helperText={
|
||||
displayErrorMessage &&
|
||||
`${userConfirmationText} does not match the name of this workspace`
|
||||
}
|
||||
InputProps={{ color: inputColor }}
|
||||
inputProps={{
|
||||
"data-testid": "delete-dialog-name-confirmation",
|
||||
}}
|
||||
aria-invalid={displayErrorMessage}
|
||||
aria-describedby={displayErrorMessage ? errorId : undefined}
|
||||
data-testid="delete-dialog-name-confirmation"
|
||||
/>
|
||||
{displayErrorMessage && (
|
||||
<span id={errorId} className="text-xs text-content-destructive">
|
||||
{userConfirmationText} does not match the name of this workspace
|
||||
</span>
|
||||
)}
|
||||
|
||||
{hasTask && (
|
||||
<div css={styles.warnContainer}>
|
||||
<div className="flex-col">
|
||||
<p className="info">This workspace is related to a task</p>
|
||||
<span className="text-xs mt-1 block">
|
||||
<div className={warnBoxClassName}>
|
||||
<div>
|
||||
<p className="m-0 text-sm font-semibold">
|
||||
This workspace is related to a task
|
||||
</p>
|
||||
<span className="mt-1 block text-xs text-content-secondary">
|
||||
Deleting this workspace will also delete{" "}
|
||||
<Link
|
||||
href={`/tasks/${workspace.owner_name}/${workspace.task_id}`}
|
||||
size="sm"
|
||||
showExternalIcon={false}
|
||||
>
|
||||
this task
|
||||
</Link>
|
||||
@@ -127,39 +138,44 @@ export const WorkspaceDeleteDialog: FC<WorkspaceDeleteDialogProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{canOrphan && (
|
||||
<div css={styles.warnContainer}>
|
||||
<div className="flex-col">
|
||||
<div className={warnBoxClassName}>
|
||||
<label
|
||||
htmlFor={orphanId}
|
||||
className="flex items-start gap-2 cursor-pointer"
|
||||
>
|
||||
<Checkbox
|
||||
id="orphan_resources"
|
||||
size="small"
|
||||
color="warning"
|
||||
onChange={() => {
|
||||
setOrphanWorkspace(!orphanWorkspace);
|
||||
}}
|
||||
className="option"
|
||||
id={orphanId}
|
||||
name="orphan_resources"
|
||||
checked={orphanWorkspace}
|
||||
onCheckedChange={(checked) => {
|
||||
setOrphanWorkspace(checked === true);
|
||||
}}
|
||||
data-testid="orphan-checkbox"
|
||||
className="mt-0.5 border-content-warning hover:enabled:border-content-warning data-[state=checked]:bg-content-warning data-[state=checked]:border-content-warning data-[state=checked]:text-content-invert hover:data-[state=checked]:bg-content-warning hover:data-[state=checked]:border-content-warning"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-col">
|
||||
<p className="info">Orphan Resources</p>
|
||||
<span className="text-xs mt-1 block">
|
||||
As a Template Admin, you may skip resource cleanup to delete
|
||||
a failed workspace. Resources such as volumes and virtual
|
||||
machines will not be destroyed.
|
||||
<Link
|
||||
href={docs(
|
||||
"/user-guides/workspace-management#workspace-resources",
|
||||
)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Learn more...
|
||||
</Link>
|
||||
<span>
|
||||
<span className="block text-sm font-semibold">
|
||||
Orphan Resources
|
||||
</span>
|
||||
<span className="mt-1 block text-xs text-content-secondary">
|
||||
As a Template Admin, you may skip resource cleanup to
|
||||
delete a failed workspace. Resources such as volumes and
|
||||
virtual machines will not be destroyed.{" "}
|
||||
<Link
|
||||
href={docs(
|
||||
"/user-guides/workspace-management#workspace-resources",
|
||||
)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
size="sm"
|
||||
>
|
||||
Learn more
|
||||
</Link>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
@@ -168,56 +184,3 @@ export const WorkspaceDeleteDialog: FC<WorkspaceDeleteDialogProps> = ({
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = {
|
||||
workspaceInfo: (theme) => ({
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
borderRadius: 6,
|
||||
padding: 16,
|
||||
marginBottom: 20,
|
||||
lineHeight: "1.3em",
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
|
||||
"& .name": {
|
||||
fontSize: 16,
|
||||
fontWeight: 600,
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
|
||||
"& .label": {
|
||||
fontSize: 12,
|
||||
color: theme.palette.text.secondary,
|
||||
},
|
||||
|
||||
"& .info": {
|
||||
fontSize: 12,
|
||||
fontWeight: 500,
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
}),
|
||||
warnContainer: (theme) => ({
|
||||
marginTop: 24,
|
||||
display: "flex",
|
||||
backgroundColor: theme.roles.danger.background,
|
||||
justifyContent: "space-between",
|
||||
border: `1px solid ${theme.roles.danger.outline}`,
|
||||
borderRadius: 8,
|
||||
padding: 12,
|
||||
gap: 8,
|
||||
lineHeight: "18px",
|
||||
|
||||
"& .option": {
|
||||
color: theme.roles.danger.fill.solid,
|
||||
"&.Mui-checked": {
|
||||
color: theme.roles.danger.fill.solid,
|
||||
},
|
||||
},
|
||||
|
||||
"& .info": {
|
||||
fontSize: 14,
|
||||
fontWeight: 600,
|
||||
color: theme.roles.danger.text,
|
||||
},
|
||||
}),
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
|
||||
@@ -9,6 +9,7 @@ import type {
|
||||
WorkspaceBuild,
|
||||
} from "#/api/typesGenerated";
|
||||
import { Button } from "#/components/Button/Button";
|
||||
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -17,7 +18,6 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "#/components/Dialog/Dialog";
|
||||
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
|
||||
import { MemoizedInlineMarkdown } from "#/components/Markdown/InlineMarkdown";
|
||||
|
||||
type UseWorkspaceUpdateOptions = {
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
deleteAIGatewayKeyMutation,
|
||||
} from "#/api/queries/aiGatewayKeys";
|
||||
import type { AIGatewayKey } from "#/api/typesGenerated";
|
||||
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
|
||||
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
|
||||
import { useAuthenticated } from "#/hooks/useAuthenticated";
|
||||
import { useFeatureVisibility } from "#/modules/dashboard/useFeatureVisibility";
|
||||
import { RequirePermission } from "#/modules/permissions/RequirePermission";
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { TriangleAlertIcon } from "lucide-react";
|
||||
import type { FC } from "react";
|
||||
import type * as TypesGen from "#/api/typesGenerated";
|
||||
import { ConfirmDeleteDialog } from "#/components/Dialogs/ConfirmDeleteDialog/ConfirmDeleteDialog";
|
||||
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
|
||||
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
|
||||
import type { useUnsavedChangesPrompt } from "#/hooks/useUnsavedChangesPrompt";
|
||||
|
||||
interface MCPServerFormDialogsProps {
|
||||
@@ -25,13 +24,15 @@ export const MCPServerFormDialogs: FC<MCPServerFormDialogsProps> = ({
|
||||
return (
|
||||
<>
|
||||
{server && onDeleteServer && (
|
||||
<ConfirmDeleteDialog
|
||||
<ConfirmDialog
|
||||
type="delete"
|
||||
open={confirmingDelete}
|
||||
onOpenChange={setConfirmingDelete}
|
||||
entity="MCP server"
|
||||
onClose={() => setConfirmingDelete(false)}
|
||||
title="Delete MCP server"
|
||||
confirmText="Delete MCP server"
|
||||
description={`Delete "${server.display_name}"? Agents will no longer be able to use this server.`}
|
||||
onConfirm={() => void onDeleteServer(server.id)}
|
||||
isPending={isDeleting}
|
||||
confirmLoading={isDeleting}
|
||||
/>
|
||||
)}
|
||||
<ConfirmDialog
|
||||
|
||||
@@ -2,6 +2,7 @@ import { TriangleAlertIcon } from "lucide-react";
|
||||
import type { FC } from "react";
|
||||
import type * as TypesGen from "#/api/typesGenerated";
|
||||
import { Button } from "#/components/Button/Button";
|
||||
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -10,7 +11,6 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "#/components/Dialog/Dialog";
|
||||
import { ConfirmDeleteDialog } from "#/components/Dialogs/ConfirmDeleteDialog/ConfirmDeleteDialog";
|
||||
import type { ModelFormValues } from "#/pages/AgentsPage/components/ChatModelAdminPanel/modelConfigFormLogic";
|
||||
|
||||
export const ModelFormDialogs: FC<{
|
||||
@@ -47,11 +47,14 @@ export const ModelFormDialogs: FC<{
|
||||
return (
|
||||
<>
|
||||
{editingModel && onDeleteModel && (
|
||||
<ConfirmDeleteDialog
|
||||
entity="model"
|
||||
isPending={isDeleting}
|
||||
<ConfirmDialog
|
||||
type="delete"
|
||||
title="Delete model"
|
||||
confirmText="Delete model"
|
||||
description="Are you sure you want to delete this model? This action is irreversible."
|
||||
confirmLoading={isDeleting}
|
||||
open={confirmingDelete}
|
||||
onOpenChange={(open) => !open && setConfirmingDelete(false)}
|
||||
onClose={() => setConfirmingDelete(false)}
|
||||
onConfirm={() => {
|
||||
resetForm(formValues);
|
||||
void onDeleteModel(editingModel.id);
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ import {
|
||||
import { Avatar } from "#/components/Avatar/Avatar";
|
||||
import { Badge } from "#/components/Badge/Badge";
|
||||
import { Button } from "#/components/Button/Button";
|
||||
import { DeleteDialog } from "#/components/Dialogs/DeleteDialog/DeleteDialog";
|
||||
import { DeleteDialog } from "#/components/Dialog/DeleteDialog/DeleteDialog";
|
||||
import { Loader } from "#/components/Loader/Loader";
|
||||
import { SettingsHeaderTitle } from "#/components/SettingsHeader/SettingsHeader";
|
||||
import { Switch } from "#/components/Switch/Switch";
|
||||
|
||||
@@ -10,7 +10,7 @@ import type {
|
||||
import { ErrorAlert } from "#/components/Alert/ErrorAlert";
|
||||
import { Button } from "#/components/Button/Button";
|
||||
import { CodeExample } from "#/components/CodeExample/CodeExample";
|
||||
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
|
||||
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
|
||||
import { Form, FormFields } from "#/components/Form/Form";
|
||||
import { FormField } from "#/components/FormField/FormField";
|
||||
import { Label } from "#/components/Label/Label";
|
||||
|
||||
@@ -3,7 +3,7 @@ import { type FC, useId, useState } from "react";
|
||||
import { getErrorMessage } from "#/api/errors";
|
||||
import type { ChatUsageLimitGroupOverride, Group } from "#/api/typesGenerated";
|
||||
import { Button } from "#/components/Button/Button";
|
||||
import { ConfirmDeleteDialog } from "#/components/Dialogs/ConfirmDeleteDialog/ConfirmDeleteDialog";
|
||||
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
|
||||
import { SearchField } from "#/components/SearchField/SearchField";
|
||||
import { paginateItems } from "#/utils/paginateItems";
|
||||
import { SpendSectionHeader } from "../SpendSectionHeader";
|
||||
@@ -173,15 +173,18 @@ export const GroupLimitsSection: FC<GroupLimitsSectionProps> = ({
|
||||
)}
|
||||
</div>
|
||||
{pendingDeleteGroupId && (
|
||||
<ConfirmDeleteDialog
|
||||
entity="group override"
|
||||
<ConfirmDialog
|
||||
type="delete"
|
||||
title="Delete group override"
|
||||
confirmText="Delete group override"
|
||||
description="Are you sure you want to delete this group override? This action is irreversible."
|
||||
onConfirm={() => {
|
||||
void onDeleteGroupOverride(pendingDeleteGroupId);
|
||||
setPendingDeleteGroupId(null);
|
||||
}}
|
||||
isPending={deletePending}
|
||||
confirmLoading={deletePending}
|
||||
open
|
||||
onOpenChange={(open) => !open && setPendingDeleteGroupId(null)}
|
||||
onClose={() => setPendingDeleteGroupId(null)}
|
||||
/>
|
||||
)}
|
||||
</section>
|
||||
|
||||
+8
-5
@@ -3,7 +3,7 @@ import { type FC, useId, useState } from "react";
|
||||
import { getErrorMessage } from "#/api/errors";
|
||||
import type { User } from "#/api/typesGenerated";
|
||||
import { Button } from "#/components/Button/Button";
|
||||
import { ConfirmDeleteDialog } from "#/components/Dialogs/ConfirmDeleteDialog/ConfirmDeleteDialog";
|
||||
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
|
||||
import { SearchField } from "#/components/SearchField/SearchField";
|
||||
import { paginateItems } from "#/utils/paginateItems";
|
||||
import { SpendSectionHeader } from "../SpendSectionHeader";
|
||||
@@ -154,15 +154,18 @@ export const UserOverridesSection: FC<UserOverridesSectionProps> = ({
|
||||
/>
|
||||
</div>
|
||||
{pendingDeleteUserId && (
|
||||
<ConfirmDeleteDialog
|
||||
entity="user override"
|
||||
<ConfirmDialog
|
||||
type="delete"
|
||||
title="Delete user override"
|
||||
confirmText="Delete user override"
|
||||
description="Are you sure you want to delete this user override? This action is irreversible."
|
||||
onConfirm={() => {
|
||||
void onDeleteOverride(pendingDeleteUserId);
|
||||
setPendingDeleteUserId(null);
|
||||
}}
|
||||
isPending={deletePending}
|
||||
confirmLoading={deletePending}
|
||||
open
|
||||
onOpenChange={(open) => !open && setPendingDeleteUserId(null)}
|
||||
onClose={() => setPendingDeleteUserId(null)}
|
||||
/>
|
||||
)}
|
||||
</section>
|
||||
|
||||
@@ -7,7 +7,7 @@ import type {
|
||||
import { ErrorAlert } from "#/components/Alert/ErrorAlert";
|
||||
import { Badge } from "#/components/Badge/Badge";
|
||||
import { Button } from "#/components/Button/Button";
|
||||
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
|
||||
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
|
||||
import { EmptyState } from "#/components/EmptyState/EmptyState";
|
||||
import { Input } from "#/components/Input/Input";
|
||||
import { Loader } from "#/components/Loader/Loader";
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { UserSkillMetadata } from "#/api/typesGenerated";
|
||||
import { Alert, AlertDescription } from "#/components/Alert/Alert";
|
||||
import { ErrorAlert } from "#/components/Alert/ErrorAlert";
|
||||
import { Button } from "#/components/Button/Button";
|
||||
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -11,7 +12,6 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "#/components/Dialog/Dialog";
|
||||
import { ConfirmDeleteDialog } from "#/components/Dialogs/ConfirmDeleteDialog/ConfirmDeleteDialog";
|
||||
import { EmptyState } from "#/components/EmptyState/EmptyState";
|
||||
import { Loader } from "#/components/Loader/Loader";
|
||||
import { Spinner } from "#/components/Spinner/Spinner";
|
||||
@@ -168,35 +168,32 @@ const EditSkillDialog: FC<{
|
||||
const DeleteSkillDialog: FC<{ state: PersonalSkillDeleteState }> = ({
|
||||
state,
|
||||
}) => {
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
if (!open) {
|
||||
state.onClose();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ConfirmDeleteDialog
|
||||
<ConfirmDialog
|
||||
type="delete"
|
||||
open
|
||||
onOpenChange={handleOpenChange}
|
||||
entity="skill"
|
||||
onClose={state.onClose}
|
||||
title="Delete skill"
|
||||
confirmText="Delete skill"
|
||||
description={
|
||||
<>
|
||||
Delete {state.skill.name}? Agents will no longer be able to use this
|
||||
skill. This action cannot be undone.
|
||||
<p className="m-0">
|
||||
Delete {state.skill.name}? Agents will no longer be able to use this
|
||||
skill. This action cannot be undone.
|
||||
</p>
|
||||
{state.error && (
|
||||
<Alert severity="error" className="mt-3">
|
||||
<AlertDescription>
|
||||
{state.error.message}
|
||||
{state.error.detail ? ` ${state.error.detail}` : ""}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
onConfirm={state.onConfirm}
|
||||
isPending={state.isDeleting}
|
||||
>
|
||||
{state.error && (
|
||||
<Alert severity="error">
|
||||
<AlertDescription>
|
||||
{state.error.message}
|
||||
{state.error.detail ? ` ${state.error.detail}` : ""}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</ConfirmDeleteDialog>
|
||||
confirmLoading={state.isDeleting}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import { reactRouterParameters } from "storybook-addon-remix-react-router";
|
||||
import { API } from "#/api/api";
|
||||
import type * as TypesGen from "#/api/typesGenerated";
|
||||
import type { Chat } from "#/api/typesGenerated";
|
||||
import { DeleteDialog } from "#/components/Dialogs/DeleteDialog/DeleteDialog";
|
||||
import { DeleteDialog } from "#/components/Dialog/DeleteDialog/DeleteDialog";
|
||||
import { MockChat } from "#/testHelpers/chatEntities";
|
||||
import {
|
||||
MockNoPermissions,
|
||||
|
||||
@@ -48,8 +48,8 @@ import {
|
||||
workspaceByIdKey,
|
||||
} from "#/api/queries/workspaces";
|
||||
import type * as TypesGen from "#/api/typesGenerated";
|
||||
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
|
||||
import { DeleteDialog } from "#/components/Dialogs/DeleteDialog/DeleteDialog";
|
||||
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
|
||||
import { DeleteDialog } from "#/components/Dialog/DeleteDialog/DeleteDialog";
|
||||
import { useAuthenticated } from "#/hooks/useAuthenticated";
|
||||
import {
|
||||
getDefaultOrganizationName,
|
||||
|
||||
@@ -42,7 +42,7 @@ import {
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "#/components/Command/Command";
|
||||
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
|
||||
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
|
||||
import { ExternalImage } from "#/components/ExternalImage/ExternalImage";
|
||||
import {
|
||||
Popover,
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
} from "storybook/test";
|
||||
import { API } from "#/api/api";
|
||||
import type * as TypesGen from "#/api/typesGenerated";
|
||||
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
|
||||
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
|
||||
import { MockChatModelConfig } from "#/testHelpers/chatModels";
|
||||
import {
|
||||
MockDefaultOrganization,
|
||||
|
||||
@@ -9,7 +9,7 @@ import type { AgentChatSendShortcut } from "#/api/typesGenerated";
|
||||
import { Alert, AlertDescription, AlertTitle } from "#/components/Alert/Alert";
|
||||
import { ErrorAlert } from "#/components/Alert/ErrorAlert";
|
||||
import { Button } from "#/components/Button/Button";
|
||||
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
|
||||
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
|
||||
import { useDashboard } from "#/modules/dashboard/useDashboard";
|
||||
import { docs } from "#/utils/docs";
|
||||
import { useFileAttachments } from "../hooks/useFileAttachments";
|
||||
|
||||
@@ -7,7 +7,7 @@ import { API } from "#/api/api";
|
||||
import { getErrorDetail } from "#/api/errors";
|
||||
import { ErrorAlert } from "#/components/Alert/ErrorAlert";
|
||||
import { CodeExample } from "#/components/CodeExample/CodeExample";
|
||||
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
|
||||
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
|
||||
import { FullPageHorizontalForm } from "#/components/FullPageForm/FullPageHorizontalForm";
|
||||
import { Loader } from "#/components/Loader/Loader";
|
||||
import { pageTitle } from "#/utils/page";
|
||||
|
||||
+28
-3
@@ -1,5 +1,5 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { action } from "storybook/actions";
|
||||
import { expect, fn, userEvent, within } from "storybook/test";
|
||||
import { AnnouncementBannerDialog } from "./AnnouncementBannerDialog";
|
||||
|
||||
const meta: Meta<typeof AnnouncementBannerDialog> = {
|
||||
@@ -11,8 +11,8 @@ const meta: Meta<typeof AnnouncementBannerDialog> = {
|
||||
message: "The beep-bop will be boop-beeped on Saturday at 12AM PST.",
|
||||
background_color: "#ffaff3",
|
||||
},
|
||||
onCancel: action("onCancel"),
|
||||
onUpdate: () => Promise.resolve(void action("onUpdate")),
|
||||
onCancel: fn(),
|
||||
onUpdate: fn(async () => undefined),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -22,3 +22,28 @@ type Story = StoryObj<typeof AnnouncementBannerDialog>;
|
||||
const Example: Story = {};
|
||||
|
||||
export { Example as AnnouncementBannerDialog };
|
||||
|
||||
export const EditsMessage: Story = {
|
||||
play: async ({ args }) => {
|
||||
const body = within(document.body);
|
||||
const message = await body.findByLabelText("Message");
|
||||
await expect(message).toHaveValue(
|
||||
"The beep-bop will be boop-beeped on Saturday at 12AM PST.",
|
||||
);
|
||||
|
||||
await userEvent.clear(message);
|
||||
await userEvent.type(message, "Scheduled maintenance tonight.");
|
||||
await userEvent.click(body.getByRole("button", { name: "Update" }));
|
||||
await expect(args.onUpdate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ message: "Scheduled maintenance tonight." }),
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const CancelClosesDialog: Story = {
|
||||
play: async ({ args }) => {
|
||||
const body = within(document.body);
|
||||
await userEvent.click(await body.findByRole("button", { name: "Cancel" }));
|
||||
await expect(args.onCancel).toHaveBeenCalled();
|
||||
},
|
||||
};
|
||||
|
||||
+44
-53
@@ -1,12 +1,18 @@
|
||||
import { type Interpolation, type Theme, useTheme } from "@emotion/react";
|
||||
import DialogActions from "@mui/material/DialogActions";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import { useFormik } from "formik";
|
||||
import { type FC, useState } from "react";
|
||||
import { SliderPicker, TwitterPicker } from "react-color";
|
||||
import type { BannerConfig } from "#/api/typesGenerated";
|
||||
import { Button } from "#/components/Button/Button";
|
||||
import { Dialog, DialogActionButtons } from "#/components/Dialogs/Dialog";
|
||||
import {
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "#/components/Dialog/Dialog";
|
||||
import { AnnouncementBannerView } from "#/modules/dashboard/AnnouncementBanners/AnnouncementBannerView";
|
||||
import { getFormHelpers } from "#/utils/formUtils";
|
||||
|
||||
@@ -38,20 +44,36 @@ export const AnnouncementBannerDialog: FC<AnnouncementBannerDialogProps> = ({
|
||||
const [showHuePicker, setShowHuePicker] = useState(false);
|
||||
|
||||
return (
|
||||
<Dialog css={styles.dialogWrapper} open onClose={onCancel}>
|
||||
<Dialog
|
||||
open
|
||||
onOpenChange={(nextOpen) => {
|
||||
if (!nextOpen) {
|
||||
onCancel();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{/* Banner preview */}
|
||||
<div className="fixed top-0 left-0 right-0">
|
||||
<div className="fixed top-0 left-0 right-0 z-[60]">
|
||||
<AnnouncementBannerView
|
||||
message={bannerForm.values.message}
|
||||
backgroundColor={bannerForm.values.background_color}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div css={styles.dialogContent}>
|
||||
<h3 css={styles.dialogTitle}>Announcement banner</h3>
|
||||
<DialogContent
|
||||
className="max-w-[500px]"
|
||||
data-testid="dialog"
|
||||
aria-describedby={undefined}
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Announcement banner</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
<div>
|
||||
<h4 css={styles.settingName}>Message</h4>
|
||||
<h4 className="m-0 mb-2 text-base font-semibold text-content-primary">
|
||||
Message
|
||||
</h4>
|
||||
<TextField
|
||||
{...bannerFieldHelpers("message", {
|
||||
helperText: "Markdown bold, italics, and links are supported.",
|
||||
@@ -65,7 +87,9 @@ export const AnnouncementBannerDialog: FC<AnnouncementBannerDialogProps> = ({
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h4 css={styles.settingName}>Background color</h4>
|
||||
<h4 className="m-0 mb-2 text-base font-semibold text-content-primary">
|
||||
Background color
|
||||
</h4>
|
||||
<div className="flex flex-col gap-4">
|
||||
{showHuePicker ? (
|
||||
<SliderPicker
|
||||
@@ -129,51 +153,18 @@ export const AnnouncementBannerDialog: FC<AnnouncementBannerDialogProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogActions>
|
||||
<DialogActionButtons
|
||||
cancelText="Cancel"
|
||||
confirmLoading={bannerForm.isSubmitting}
|
||||
confirmText="Update"
|
||||
disabled={bannerForm.isSubmitting}
|
||||
onCancel={onCancel}
|
||||
onConfirm={bannerForm.handleSubmit}
|
||||
/>
|
||||
</DialogActions>
|
||||
<DialogFooter>
|
||||
<DialogActions
|
||||
cancelText="Cancel"
|
||||
confirmLoading={bannerForm.isSubmitting}
|
||||
confirmText="Update"
|
||||
confirmDisabled={bannerForm.isSubmitting}
|
||||
onCancel={onCancel}
|
||||
onConfirm={bannerForm.handleSubmit}
|
||||
/>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = {
|
||||
dialogWrapper: (theme) => ({
|
||||
"& .MuiPaper-root": {
|
||||
background: theme.palette.background.paper,
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
width: "100%",
|
||||
maxWidth: 500,
|
||||
},
|
||||
"& .MuiDialogActions-spacing": {
|
||||
padding: "0 40px 40px",
|
||||
},
|
||||
}),
|
||||
dialogContent: (theme) => ({
|
||||
color: theme.palette.text.secondary,
|
||||
padding: "40px 40px 20px",
|
||||
}),
|
||||
dialogTitle: (theme) => ({
|
||||
margin: 0,
|
||||
marginBottom: 16,
|
||||
color: theme.palette.text.primary,
|
||||
fontWeight: 400,
|
||||
fontSize: 20,
|
||||
}),
|
||||
settingName: (theme) => ({
|
||||
marginTop: 0,
|
||||
marginBottom: 8,
|
||||
color: theme.palette.text.primary,
|
||||
fontSize: 16,
|
||||
lineHeight: "150%",
|
||||
fontWeight: 600,
|
||||
}),
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ import { PlusIcon } from "lucide-react";
|
||||
import { type FC, useState } from "react";
|
||||
import type { BannerConfig } from "#/api/typesGenerated";
|
||||
import { Button } from "#/components/Button/Button";
|
||||
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
|
||||
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
|
||||
import { EmptyState } from "#/components/EmptyState/EmptyState";
|
||||
import { Link } from "#/components/Link/Link";
|
||||
import {
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "#/components/Collapsible/Collapsible";
|
||||
import { DeleteDialog } from "#/components/Dialogs/DeleteDialog/DeleteDialog";
|
||||
import { DeleteDialog } from "#/components/Dialog/DeleteDialog/DeleteDialog";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
|
||||
+2
-2
@@ -19,8 +19,8 @@ import { Avatar } from "#/components/Avatar/Avatar";
|
||||
import { Button } from "#/components/Button/Button";
|
||||
import { CodeExample } from "#/components/CodeExample/CodeExample";
|
||||
import { CopyButton } from "#/components/CopyButton/CopyButton";
|
||||
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
|
||||
import { DeleteDialog } from "#/components/Dialogs/DeleteDialog/DeleteDialog";
|
||||
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
|
||||
import { DeleteDialog } from "#/components/Dialog/DeleteDialog/DeleteDialog";
|
||||
import { Loader } from "#/components/Loader/Loader";
|
||||
import { SettingsHeaderTitle } from "#/components/SettingsHeader/SettingsHeader";
|
||||
import { Spinner } from "#/components/Spinner/Spinner";
|
||||
|
||||
@@ -6,7 +6,7 @@ import * as Yup from "yup";
|
||||
import type * as TypesGen from "#/api/typesGenerated";
|
||||
import { ErrorAlert } from "#/components/Alert/ErrorAlert";
|
||||
import { Button } from "#/components/Button/Button";
|
||||
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
|
||||
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
|
||||
import { Form, FormFields } from "#/components/Form/Form";
|
||||
import { FormField } from "#/components/FormField/FormField";
|
||||
import { Label } from "#/components/Label/Label";
|
||||
|
||||
@@ -21,7 +21,7 @@ import { ErrorAlert } from "#/components/Alert/ErrorAlert";
|
||||
import { Avatar } from "#/components/Avatar/Avatar";
|
||||
import { AvatarData } from "#/components/Avatar/AvatarData";
|
||||
import { Button } from "#/components/Button/Button";
|
||||
import { DeleteDialog } from "#/components/Dialogs/DeleteDialog/DeleteDialog";
|
||||
import { DeleteDialog } from "#/components/Dialog/DeleteDialog/DeleteDialog";
|
||||
import { useFilter } from "#/components/Filter/Filter";
|
||||
import type { UsersFilter } from "#/components/Filter/UsersFilter";
|
||||
import { Loader } from "#/components/Loader/Loader";
|
||||
|
||||
@@ -6,7 +6,7 @@ import { getErrorDetail, getErrorMessage } from "#/api/errors";
|
||||
import { updateOrganization } from "#/api/queries/organizations";
|
||||
import { deleteOrganizationRole, organizationRoles } from "#/api/queries/roles";
|
||||
import type { Role } from "#/api/typesGenerated";
|
||||
import { DeleteDialog } from "#/components/Dialogs/DeleteDialog/DeleteDialog";
|
||||
import { DeleteDialog } from "#/components/Dialog/DeleteDialog/DeleteDialog";
|
||||
import { EmptyState } from "#/components/EmptyState/EmptyState";
|
||||
import {
|
||||
SettingsHeader,
|
||||
|
||||
@@ -16,7 +16,7 @@ import type {
|
||||
OrganizationMemberWithUserData,
|
||||
User,
|
||||
} from "#/api/typesGenerated";
|
||||
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
|
||||
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
|
||||
import { EmptyState } from "#/components/EmptyState/EmptyState";
|
||||
import { useFilter } from "#/components/Filter/Filter";
|
||||
import { useAuthenticated } from "#/hooks/useAuthenticated";
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ import {
|
||||
provisionerJobsQueryKey,
|
||||
} from "#/api/queries/organizations";
|
||||
import type { ProvisionerJob } from "#/api/typesGenerated";
|
||||
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
|
||||
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
|
||||
|
||||
type CancelJobConfirmationDialogProps = {
|
||||
open: boolean;
|
||||
|
||||
@@ -12,7 +12,7 @@ import { Alert, AlertTitle } from "#/components/Alert/Alert";
|
||||
import { ErrorAlert } from "#/components/Alert/ErrorAlert";
|
||||
import { Button } from "#/components/Button/Button";
|
||||
import { Checkbox } from "#/components/Checkbox/Checkbox";
|
||||
import { DeleteDialog } from "#/components/Dialogs/DeleteDialog/DeleteDialog";
|
||||
import { DeleteDialog } from "#/components/Dialog/DeleteDialog/DeleteDialog";
|
||||
import {
|
||||
FormFields,
|
||||
FormFooter,
|
||||
|
||||
@@ -3,7 +3,7 @@ import relativeTime from "dayjs/plugin/relativeTime";
|
||||
import { ClockIcon, UserIcon } from "lucide-react";
|
||||
import { type FC, type ReactNode, useState } from "react";
|
||||
import type { Task } from "#/api/typesGenerated";
|
||||
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
|
||||
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
|
||||
@@ -21,8 +21,8 @@ import type {
|
||||
} from "#/api/typesGenerated";
|
||||
import { Avatar } from "#/components/Avatar/Avatar";
|
||||
import { Button, Button as ShadcnButton } from "#/components/Button/Button";
|
||||
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
|
||||
import { DeleteDialog } from "#/components/Dialogs/DeleteDialog/DeleteDialog";
|
||||
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
|
||||
import { DeleteDialog } from "#/components/Dialog/DeleteDialog/DeleteDialog";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
templateVersions,
|
||||
templateVersionsQueryKey,
|
||||
} from "#/api/queries/templates";
|
||||
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
|
||||
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
|
||||
import { linkToTemplate, useLinks } from "#/modules/navigation";
|
||||
import { useTemplateLayoutContext } from "#/pages/TemplatePage/TemplateLayout";
|
||||
import { getTemplatePageTitle } from "../utils";
|
||||
|
||||
+68
-3
@@ -1,15 +1,25 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { action } from "storybook/actions";
|
||||
import { expect, fn, userEvent, within } from "storybook/test";
|
||||
import { ScheduleDialog } from "./ScheduleDialog";
|
||||
|
||||
const meta: Meta<typeof ScheduleDialog> = {
|
||||
title: "pages/TemplateSettingsPage/ScheduleDialog",
|
||||
component: ScheduleDialog,
|
||||
args: {
|
||||
onConfirm: action("onConfirm"),
|
||||
onClose: action("onClose"),
|
||||
onConfirm: fn(),
|
||||
onClose: fn(),
|
||||
updateDormantWorkspaces: fn(),
|
||||
updateInactiveWorkspaces: fn(),
|
||||
open: true,
|
||||
title: "Workspace Scheduling",
|
||||
inactiveWorkspacesToGoDormant: 0,
|
||||
inactiveWorkspacesToGoDormantInWeek: 0,
|
||||
dormantWorkspacesToBeDeleted: 0,
|
||||
dormantWorkspacesToBeDeletedInWeek: 0,
|
||||
dormantWorkspacesChecked: false,
|
||||
inactiveWorkspacesChecked: false,
|
||||
dormantValueChanged: false,
|
||||
deletionValueChanged: false,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -31,3 +41,58 @@ export const DormancyDeletion: Story = {
|
||||
dormantWorkspacesToBeDeletedInWeek: 5,
|
||||
},
|
||||
};
|
||||
|
||||
export const PreventDormancyAndSubmit: Story = {
|
||||
args: {
|
||||
dormantValueChanged: true,
|
||||
inactiveWorkspacesToGoDormant: 1,
|
||||
inactiveWorkspacesToGoDormantInWeek: 5,
|
||||
},
|
||||
play: async ({ args, step }) => {
|
||||
const body = within(document.body);
|
||||
|
||||
await step("selecting prevention resets inactivity periods", async () => {
|
||||
await userEvent.click(await body.findByRole("checkbox"));
|
||||
await expect(args.updateInactiveWorkspaces).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
await step("submitting confirms the schedule change", async () => {
|
||||
await userEvent.click(body.getByRole("button", { name: "Submit" }));
|
||||
await expect(args.onConfirm).toHaveBeenCalled();
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export const PreventDeletionAndSubmit: Story = {
|
||||
args: {
|
||||
deletionValueChanged: true,
|
||||
dormantWorkspacesToBeDeleted: 1,
|
||||
dormantWorkspacesToBeDeletedInWeek: 5,
|
||||
},
|
||||
play: async ({ args, step }) => {
|
||||
const body = within(document.body);
|
||||
|
||||
await step("selecting prevention resets dormancy periods", async () => {
|
||||
await userEvent.click(await body.findByRole("checkbox"));
|
||||
await expect(args.updateDormantWorkspaces).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
await step("submitting confirms the schedule change", async () => {
|
||||
await userEvent.click(body.getByRole("button", { name: "Submit" }));
|
||||
await expect(args.onConfirm).toHaveBeenCalled();
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export const CancelClosesDialog: Story = {
|
||||
args: {
|
||||
dormantValueChanged: true,
|
||||
inactiveWorkspacesToGoDormant: 1,
|
||||
inactiveWorkspacesToGoDormantInWeek: 5,
|
||||
},
|
||||
play: async ({ args }) => {
|
||||
const body = within(document.body);
|
||||
await userEvent.click(await body.findByRole("button", { name: "Cancel" }));
|
||||
await expect(args.onClose).toHaveBeenCalled();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,18 +1,35 @@
|
||||
import type { Interpolation, Theme } from "@emotion/react";
|
||||
import Checkbox from "@mui/material/Checkbox";
|
||||
import DialogActions from "@mui/material/DialogActions";
|
||||
import FormControlLabel from "@mui/material/FormControlLabel";
|
||||
import type { FC } from "react";
|
||||
import type { ConfirmDialogProps } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
|
||||
import { Dialog, DialogActionButtons } from "#/components/Dialogs/Dialog";
|
||||
import { Checkbox } from "#/components/Checkbox/Checkbox";
|
||||
import type { ConfirmDialogProps } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
|
||||
import {
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "#/components/Dialog/Dialog";
|
||||
|
||||
interface ScheduleDialogProps extends ConfirmDialogProps {
|
||||
interface ScheduleDialogProps
|
||||
extends Pick<
|
||||
ConfirmDialogProps,
|
||||
| "open"
|
||||
| "onClose"
|
||||
| "onConfirm"
|
||||
| "title"
|
||||
| "cancelText"
|
||||
| "confirmLoading"
|
||||
| "disabled"
|
||||
| "hideCancel"
|
||||
> {
|
||||
readonly inactiveWorkspacesToGoDormant: number;
|
||||
readonly inactiveWorkspacesToGoDormantInWeek: number;
|
||||
readonly dormantWorkspacesToBeDeleted: number;
|
||||
readonly dormantWorkspacesToBeDeletedInWeek: number;
|
||||
readonly updateDormantWorkspaces: (confirm: boolean) => void;
|
||||
readonly updateInactiveWorkspaces: (confirm: boolean) => void;
|
||||
readonly dormantWorkspacesChecked: boolean;
|
||||
readonly inactiveWorkspacesChecked: boolean;
|
||||
readonly dormantValueChanged: boolean;
|
||||
readonly deletionValueChanged: boolean;
|
||||
}
|
||||
@@ -21,7 +38,7 @@ export const ScheduleDialog: FC<ScheduleDialogProps> = ({
|
||||
cancelText,
|
||||
confirmLoading,
|
||||
disabled = false,
|
||||
hideCancel,
|
||||
hideCancel = false,
|
||||
onClose,
|
||||
onConfirm,
|
||||
open = false,
|
||||
@@ -32,18 +49,11 @@ export const ScheduleDialog: FC<ScheduleDialogProps> = ({
|
||||
dormantWorkspacesToBeDeletedInWeek,
|
||||
updateDormantWorkspaces,
|
||||
updateInactiveWorkspaces,
|
||||
dormantWorkspacesChecked,
|
||||
inactiveWorkspacesChecked,
|
||||
dormantValueChanged,
|
||||
deletionValueChanged,
|
||||
}) => {
|
||||
const defaults = {
|
||||
confirmText: "Delete",
|
||||
hideCancel: false,
|
||||
};
|
||||
|
||||
if (typeof hideCancel === "undefined") {
|
||||
hideCancel = defaults.hideCancel;
|
||||
}
|
||||
|
||||
const showDormancyWarning =
|
||||
dormantValueChanged &&
|
||||
(inactiveWorkspacesToGoDormant > 0 ||
|
||||
@@ -55,126 +65,110 @@ export const ScheduleDialog: FC<ScheduleDialogProps> = ({
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
css={styles.dialogWrapper}
|
||||
onClose={onClose}
|
||||
open={open}
|
||||
data-testid="dialog"
|
||||
onOpenChange={(nextOpen) => {
|
||||
if (!nextOpen) {
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div css={styles.dialogContent}>
|
||||
<h3 css={styles.dialogTitle}>{title}</h3>
|
||||
<DialogContent
|
||||
variant="destructive"
|
||||
data-testid="dialog"
|
||||
aria-describedby={undefined}
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{showDormancyWarning && (
|
||||
<>
|
||||
<h4>Dormancy Threshold</h4>
|
||||
<p css={styles.dialogDescription}>
|
||||
This change will result in{" "}
|
||||
<strong>{inactiveWorkspacesToGoDormant}</strong>{" "}
|
||||
{inactiveWorkspacesToGoDormant === 1 ? "workspace" : "workspaces"}{" "}
|
||||
being immediately transitioned to the dormant state and{" "}
|
||||
<strong>{inactiveWorkspacesToGoDormantInWeek}</strong>{" "}
|
||||
{inactiveWorkspacesToGoDormantInWeek === 1
|
||||
? "workspace"
|
||||
: "workspaces"}{" "}
|
||||
over the next 7 days. To prevent this, do you want to reset the
|
||||
inactivity period for all template workspaces?
|
||||
</p>
|
||||
<FormControlLabel
|
||||
className="mt-4"
|
||||
control={
|
||||
<div className="flex flex-col gap-4 text-sm text-content-secondary font-medium [&_strong]:text-content-primary">
|
||||
{showDormancyWarning && (
|
||||
<div className="flex flex-col gap-3">
|
||||
<h4 className="m-0 text-base font-semibold text-content-primary">
|
||||
Dormancy Threshold
|
||||
</h4>
|
||||
<p className="m-0 leading-relaxed">
|
||||
This change will result in{" "}
|
||||
<strong>{inactiveWorkspacesToGoDormant}</strong>{" "}
|
||||
{inactiveWorkspacesToGoDormant === 1
|
||||
? "workspace"
|
||||
: "workspaces"}{" "}
|
||||
being immediately transitioned to the dormant state and{" "}
|
||||
<strong>{inactiveWorkspacesToGoDormantInWeek}</strong>{" "}
|
||||
{inactiveWorkspacesToGoDormantInWeek === 1
|
||||
? "workspace"
|
||||
: "workspaces"}{" "}
|
||||
over the next 7 days. To prevent this, do you want to reset the
|
||||
inactivity period for all template workspaces?
|
||||
</p>
|
||||
<label
|
||||
htmlFor="prevent-dormancy"
|
||||
className="flex items-center gap-2 text-content-primary"
|
||||
>
|
||||
<Checkbox
|
||||
size="small"
|
||||
onChange={(e) => {
|
||||
updateInactiveWorkspaces(e.target.checked);
|
||||
id="prevent-dormancy"
|
||||
checked={inactiveWorkspacesChecked}
|
||||
onCheckedChange={(checked) => {
|
||||
updateInactiveWorkspaces(checked === true);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
label="Prevent Dormancy - Reset all workspace inactivity periods"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<span>
|
||||
Prevent Dormancy - Reset all workspace inactivity periods
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showDeletionWarning && (
|
||||
<>
|
||||
<h4>Dormancy Auto-Deletion</h4>
|
||||
<p css={styles.dialogDescription}>
|
||||
This change will result in{" "}
|
||||
<strong>{dormantWorkspacesToBeDeleted}</strong>{" "}
|
||||
{dormantWorkspacesToBeDeleted === 1 ? "workspace" : "workspaces"}{" "}
|
||||
being immediately deleted and{" "}
|
||||
<strong>{dormantWorkspacesToBeDeletedInWeek}</strong>{" "}
|
||||
{dormantWorkspacesToBeDeletedInWeek === 1
|
||||
? "workspace"
|
||||
: "workspaces"}{" "}
|
||||
over the next 7 days. To prevent this, do you want to reset the
|
||||
dormancy period for all template workspaces?
|
||||
</p>
|
||||
<FormControlLabel
|
||||
className="mt-4"
|
||||
control={
|
||||
{showDeletionWarning && (
|
||||
<div className="flex flex-col gap-3">
|
||||
<h4 className="m-0 text-base font-semibold text-content-primary">
|
||||
Dormancy Auto-Deletion
|
||||
</h4>
|
||||
<p className="m-0 leading-relaxed">
|
||||
This change will result in{" "}
|
||||
<strong>{dormantWorkspacesToBeDeleted}</strong>{" "}
|
||||
{dormantWorkspacesToBeDeleted === 1
|
||||
? "workspace"
|
||||
: "workspaces"}{" "}
|
||||
being immediately deleted and{" "}
|
||||
<strong>{dormantWorkspacesToBeDeletedInWeek}</strong>{" "}
|
||||
{dormantWorkspacesToBeDeletedInWeek === 1
|
||||
? "workspace"
|
||||
: "workspaces"}{" "}
|
||||
over the next 7 days. To prevent this, do you want to reset the
|
||||
dormancy period for all template workspaces?
|
||||
</p>
|
||||
<label
|
||||
htmlFor="prevent-deletion"
|
||||
className="flex items-center gap-2 text-content-primary"
|
||||
>
|
||||
<Checkbox
|
||||
size="small"
|
||||
onChange={(e) => {
|
||||
updateDormantWorkspaces(e.target.checked);
|
||||
id="prevent-deletion"
|
||||
checked={dormantWorkspacesChecked}
|
||||
onCheckedChange={(checked) => {
|
||||
updateDormantWorkspaces(checked === true);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
label="Prevent Deletion - Reset all workspace dormancy periods"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<span>
|
||||
Prevent Deletion - Reset all workspace dormancy periods
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogActions>
|
||||
<DialogActionButtons
|
||||
cancelText={cancelText}
|
||||
confirmLoading={confirmLoading}
|
||||
confirmText="Submit"
|
||||
disabled={disabled}
|
||||
onCancel={!hideCancel ? onClose : undefined}
|
||||
onConfirm={onConfirm || onClose}
|
||||
type="delete"
|
||||
/>
|
||||
</DialogActions>
|
||||
<DialogFooter>
|
||||
<DialogActions
|
||||
cancelText={cancelText}
|
||||
confirmLoading={confirmLoading}
|
||||
confirmText="Submit"
|
||||
confirmDisabled={disabled}
|
||||
confirmVariant="destructive"
|
||||
onCancel={!hideCancel ? onClose : undefined}
|
||||
onConfirm={onConfirm || onClose}
|
||||
/>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = {
|
||||
dialogWrapper: (theme) => ({
|
||||
"& .MuiPaper-root": {
|
||||
background: theme.palette.background.paper,
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
},
|
||||
"& .MuiDialogActions-spacing": {
|
||||
padding: "0 40px 40px",
|
||||
},
|
||||
}),
|
||||
dialogContent: (theme) => ({
|
||||
color: theme.palette.text.secondary,
|
||||
padding: 40,
|
||||
}),
|
||||
dialogTitle: (theme) => ({
|
||||
margin: 0,
|
||||
marginBottom: 16,
|
||||
color: theme.palette.text.primary,
|
||||
fontWeight: 400,
|
||||
fontSize: 20,
|
||||
}),
|
||||
dialogDescription: (theme) => ({
|
||||
color: theme.palette.text.secondary,
|
||||
lineHeight: "160%",
|
||||
fontSize: 16,
|
||||
|
||||
"& strong": {
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
|
||||
"& p:not(.MuiFormHelperText-root)": {
|
||||
margin: 0,
|
||||
},
|
||||
|
||||
"& > p": {
|
||||
margin: "8px 0",
|
||||
},
|
||||
}),
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
|
||||
@@ -618,6 +618,12 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
|
||||
updateInactiveWorkspaces={(update: boolean) =>
|
||||
form.setFieldValue("update_workspace_last_used_at", update)
|
||||
}
|
||||
dormantWorkspacesChecked={
|
||||
form.values.update_workspace_dormant_at ?? false
|
||||
}
|
||||
inactiveWorkspacesChecked={
|
||||
form.values.update_workspace_last_used_at ?? false
|
||||
}
|
||||
dormantValueChanged={
|
||||
form.initialValues.time_til_dormant_ms !==
|
||||
form.values.time_til_dormant_ms
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import TextField from "@mui/material/TextField";
|
||||
import { type ChangeEvent, type FC, useState } from "react";
|
||||
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
|
||||
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
|
||||
import { type FileTree, isFolder, validatePath } from "#/utils/filetree";
|
||||
|
||||
interface CreateFileDialogProps {
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { expect, fn, userEvent, within } from "storybook/test";
|
||||
import { MockTemplateVersionVariable5 } from "#/testHelpers/entities";
|
||||
import { MissingTemplateVariablesDialog } from "./MissingTemplateVariablesDialog";
|
||||
|
||||
const meta = {
|
||||
title: "pages/TemplateVersionEditorPage/MissingTemplateVariablesDialog",
|
||||
component: MissingTemplateVariablesDialog,
|
||||
args: {
|
||||
open: true,
|
||||
onClose: fn(),
|
||||
onSubmit: fn(),
|
||||
missingVariables: [MockTemplateVersionVariable5],
|
||||
},
|
||||
} satisfies Meta<typeof MissingTemplateVariablesDialog>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Example: Story = {};
|
||||
|
||||
export const SubmitsEnteredValues: Story = {
|
||||
play: async ({ args }) => {
|
||||
const body = within(document.body);
|
||||
const input = await body.findByRole("textbox");
|
||||
await userEvent.type(input, "production");
|
||||
await userEvent.click(body.getByRole("button", { name: "Submit" }));
|
||||
await expect(args.onSubmit).toHaveBeenCalledWith([
|
||||
{ name: "fifth_variable", value: "production" },
|
||||
]);
|
||||
},
|
||||
};
|
||||
@@ -1,20 +1,23 @@
|
||||
import Dialog from "@mui/material/Dialog";
|
||||
import DialogActions from "@mui/material/DialogActions";
|
||||
import DialogContent from "@mui/material/DialogContent";
|
||||
import DialogContentText from "@mui/material/DialogContentText";
|
||||
import DialogTitle from "@mui/material/DialogTitle";
|
||||
import { type FC, useEffect, useState } from "react";
|
||||
import type {
|
||||
TemplateVersionVariable,
|
||||
VariableValue,
|
||||
} from "#/api/typesGenerated";
|
||||
import { Button } from "#/components/Button/Button";
|
||||
import type { DialogProps } from "#/components/Dialogs/Dialog";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "#/components/Dialog/Dialog";
|
||||
import { FormFields, VerticalForm } from "#/components/Form/Form";
|
||||
import { Loader } from "#/components/Loader/Loader";
|
||||
import { VariableInput } from "#/pages/CreateTemplatePage/VariableInput";
|
||||
|
||||
type MissingTemplateVariablesDialogProps = Omit<DialogProps, "onSubmit"> & {
|
||||
type MissingTemplateVariablesDialogProps = {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (values: VariableValue[]) => void;
|
||||
missingVariables?: TemplateVersionVariable[];
|
||||
@@ -22,7 +25,7 @@ type MissingTemplateVariablesDialogProps = Omit<DialogProps, "onSubmit"> & {
|
||||
|
||||
export const MissingTemplateVariablesDialog: FC<
|
||||
MissingTemplateVariablesDialogProps
|
||||
> = ({ missingVariables, onSubmit, ...dialogProps }) => {
|
||||
> = ({ missingVariables, onSubmit, open, onClose }) => {
|
||||
const [variableValues, setVariableValues] = useState<VariableValue[]>([]);
|
||||
|
||||
// Pre-fill the form with the default values when missing variables are loaded
|
||||
@@ -37,24 +40,23 @@ export const MissingTemplateVariablesDialog: FC<
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
{...dialogProps}
|
||||
scroll="body"
|
||||
aria-labelledby="update-build-parameters-title"
|
||||
maxWidth="xs"
|
||||
data-testid="dialog"
|
||||
open={open}
|
||||
onOpenChange={(nextOpen) => {
|
||||
if (!nextOpen) {
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogTitle
|
||||
id="update-build-parameters-title"
|
||||
className="px-10 py-6 text-xl font-normal"
|
||||
>
|
||||
Template variables
|
||||
</DialogTitle>
|
||||
<DialogContent className="px-10">
|
||||
<DialogContentText className="m-0">
|
||||
There are a few missing template variable values. Please fill them in.
|
||||
</DialogContentText>
|
||||
<DialogContent className="max-w-md" data-testid="dialog">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Template variables</DialogTitle>
|
||||
<DialogDescription>
|
||||
There are a few missing template variable values. Please fill them
|
||||
in.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<VerticalForm
|
||||
className="pt-8"
|
||||
id="updateVariables"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
@@ -70,13 +72,9 @@ export const MissingTemplateVariablesDialog: FC<
|
||||
variable={variable}
|
||||
key={variable.name}
|
||||
onChange={async (value) => {
|
||||
setVariableValues((prev) => {
|
||||
prev[index] = {
|
||||
name: variable.name,
|
||||
value,
|
||||
};
|
||||
return [...prev];
|
||||
});
|
||||
setVariableValues((prev) =>
|
||||
prev.with(index, { name: variable.name, value }),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@@ -86,20 +84,16 @@ export const MissingTemplateVariablesDialog: FC<
|
||||
<Loader />
|
||||
)}
|
||||
</VerticalForm>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" type="button" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" form="updateVariables">
|
||||
Submit
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
<DialogActions disableSpacing className="flex flex-col gap-2 p-10">
|
||||
<Button className="w-full" type="submit" form="updateVariables">
|
||||
Submit
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
type="button"
|
||||
onClick={dialogProps.onClose}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -5,8 +5,7 @@ import { useFormik } from "formik";
|
||||
import type { FC } from "react";
|
||||
import * as Yup from "yup";
|
||||
import { EnterpriseBadge } from "#/components/Badges/Badges";
|
||||
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
|
||||
import type { DialogProps } from "#/components/Dialogs/Dialog";
|
||||
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
|
||||
import { FormFields } from "#/components/Form/Form";
|
||||
import {
|
||||
HelpPopover,
|
||||
@@ -21,7 +20,8 @@ import type { PublishVersionData } from "#/pages/TemplateVersionEditorPage/types
|
||||
import { docs } from "#/utils/docs";
|
||||
import { getFormHelpers } from "#/utils/formUtils";
|
||||
|
||||
type PublishTemplateVersionDialogProps = DialogProps & {
|
||||
type PublishTemplateVersionDialogProps = {
|
||||
open: boolean;
|
||||
defaultName: string;
|
||||
isPublishing: boolean;
|
||||
publishingError?: unknown;
|
||||
@@ -32,12 +32,12 @@ type PublishTemplateVersionDialogProps = DialogProps & {
|
||||
export const PublishTemplateVersionDialog: FC<
|
||||
PublishTemplateVersionDialogProps
|
||||
> = ({
|
||||
open,
|
||||
onConfirm,
|
||||
isPublishing,
|
||||
onClose,
|
||||
defaultName,
|
||||
publishingError,
|
||||
...dialogProps
|
||||
}) => {
|
||||
const form = useFormik({
|
||||
initialValues: {
|
||||
@@ -60,7 +60,7 @@ export const PublishTemplateVersionDialog: FC<
|
||||
|
||||
return (
|
||||
<ConfirmDialog
|
||||
{...dialogProps}
|
||||
open={open}
|
||||
confirmLoading={isPublishing}
|
||||
onClose={handleClose}
|
||||
onConfirm={async () => {
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
validateExternalAuth,
|
||||
} from "#/api/queries/externalAuth";
|
||||
import type { ExternalAuthLinkProvider } from "#/api/typesGenerated";
|
||||
import { DeleteDialog } from "#/components/Dialogs/DeleteDialog/DeleteDialog";
|
||||
import { DeleteDialog } from "#/components/Dialog/DeleteDialog/DeleteDialog";
|
||||
import {
|
||||
SettingsHeader,
|
||||
SettingsHeaderTitle,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useMutation, useQuery, useQueryClient } from "react-query";
|
||||
import { toast } from "sonner";
|
||||
import { getErrorDetail, getErrorMessage } from "#/api/errors";
|
||||
import { getApps, revokeApp } from "#/api/queries/oauth2";
|
||||
import { DeleteDialog } from "#/components/Dialogs/DeleteDialog/DeleteDialog";
|
||||
import { DeleteDialog } from "#/components/Dialog/DeleteDialog/DeleteDialog";
|
||||
import {
|
||||
SettingsHeader,
|
||||
SettingsHeaderTitle,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useMutation, useQuery, useQueryClient } from "react-query";
|
||||
import { toast } from "sonner";
|
||||
import { getErrorDetail, getErrorMessage } from "#/api/errors";
|
||||
import { regenerateUserSSHKey, userSSHKey } from "#/api/queries/sshKeys";
|
||||
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
|
||||
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
|
||||
import {
|
||||
SettingsHeader,
|
||||
SettingsHeaderTitle,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { type FC, useRef, useState } from "react";
|
||||
import type { UserSecret } from "#/api/typesGenerated";
|
||||
import { Badge } from "#/components/Badge/Badge";
|
||||
import { Button } from "#/components/Button/Button";
|
||||
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
|
||||
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
|
||||
@@ -12,7 +12,7 @@ import type {
|
||||
UserLoginType,
|
||||
} from "#/api/typesGenerated";
|
||||
import { Button } from "#/components/Button/Button";
|
||||
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
|
||||
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
|
||||
import { EmptyState } from "#/components/EmptyState/EmptyState";
|
||||
import { ExternalImage } from "#/components/ExternalImage/ExternalImage";
|
||||
import {
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { FC } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { getErrorDetail, getErrorMessage } from "#/api/errors";
|
||||
import type { APIKeyWithOwner } from "#/api/typesGenerated";
|
||||
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
|
||||
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
|
||||
import { useDeleteToken } from "./hooks";
|
||||
|
||||
interface ConfirmDeleteDialogProps {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { FC } from "react";
|
||||
import type * as TypesGen from "#/api/typesGenerated";
|
||||
import { CodeExample } from "#/components/CodeExample/CodeExample";
|
||||
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
|
||||
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
|
||||
|
||||
interface ResetPasswordDialogProps {
|
||||
open: boolean;
|
||||
|
||||
@@ -15,8 +15,8 @@ import {
|
||||
updateRoles,
|
||||
} from "#/api/queries/users";
|
||||
import type { User } from "#/api/typesGenerated";
|
||||
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
|
||||
import { DeleteDialog } from "#/components/Dialogs/DeleteDialog/DeleteDialog";
|
||||
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
|
||||
import { DeleteDialog } from "#/components/Dialog/DeleteDialog/DeleteDialog";
|
||||
import { useFilter } from "#/components/Filter/Filter";
|
||||
import { useStatusFilterMenu } from "#/components/Filter/UsersFilter";
|
||||
import { useAuthenticated } from "#/hooks/useAuthenticated";
|
||||
|
||||
@@ -153,9 +153,7 @@ describe("WorkspacePage", () => {
|
||||
await user.type(textField, MockFailedWorkspace.name);
|
||||
|
||||
// check orphan option
|
||||
const orphanCheckbox = within(
|
||||
screen.getByTestId("orphan-checkbox"),
|
||||
).getByRole("checkbox");
|
||||
const orphanCheckbox = screen.getByTestId("orphan-checkbox");
|
||||
|
||||
await user.click(orphanCheckbox);
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ import type * as TypesGen from "#/api/typesGenerated";
|
||||
import {
|
||||
ConfirmDialog,
|
||||
type ConfirmDialogProps,
|
||||
} from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
|
||||
} from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
|
||||
import { useWorkspaceBuildLogs } from "#/hooks/useWorkspaceBuildLogs";
|
||||
import { EphemeralParametersDialog } from "#/modules/workspaces/EphemeralParametersDialog/EphemeralParametersDialog";
|
||||
import { WorkspaceErrorDialog } from "#/modules/workspaces/ErrorDialog/WorkspaceErrorDialog";
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ import type {
|
||||
WorkspaceBuildParameter,
|
||||
} from "#/api/typesGenerated";
|
||||
import { ErrorAlert } from "#/components/Alert/ErrorAlert";
|
||||
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
|
||||
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
|
||||
import { EmptyState } from "#/components/EmptyState/EmptyState";
|
||||
import { Link } from "#/components/Link/Link";
|
||||
import { Loader } from "#/components/Loader/Loader";
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ import { workspaceByOwnerAndNameKey } from "#/api/queries/workspaces";
|
||||
import type * as TypesGen from "#/api/typesGenerated";
|
||||
import { Alert } from "#/components/Alert/Alert";
|
||||
import { ErrorAlert } from "#/components/Alert/ErrorAlert";
|
||||
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
|
||||
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
|
||||
import { Link } from "#/components/Link/Link";
|
||||
import { Loader } from "#/components/Loader/Loader";
|
||||
import {
|
||||
|
||||
@@ -3,7 +3,7 @@ import relativeTime from "dayjs/plugin/relativeTime";
|
||||
import { ClockIcon, UserIcon } from "lucide-react";
|
||||
import { type FC, type ReactNode, useState } from "react";
|
||||
import type { Workspace } from "#/api/typesGenerated";
|
||||
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
|
||||
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
|
||||
import { ExternalImage } from "#/components/ExternalImage/ExternalImage";
|
||||
import { getResourceIconPath } from "#/utils/workspace";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { FC } from "react";
|
||||
import type { Workspace } from "#/api/typesGenerated";
|
||||
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
|
||||
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
|
||||
|
||||
type BatchStopConfirmationProps = {
|
||||
workspacesToStop: readonly Workspace[];
|
||||
|
||||
@@ -38,7 +38,7 @@ 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 { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
|
||||
import { ExternalImage } from "#/components/ExternalImage/ExternalImage";
|
||||
import { VSCodeIcon } from "#/components/Icons/VSCodeIcon";
|
||||
import { VSCodeInsidersIcon } from "#/components/Icons/VSCodeInsidersIcon";
|
||||
|
||||
Reference in New Issue
Block a user