mirror of
https://github.com/coder/coder.git
synced 2025-07-12 00:14:10 +00:00
refactor: use the new button component on forms and dialogs (#16050)
This is a significant PR that will impact many parts of the UI, so I’d like to ask you, @jaaydenh, for a very thorough review of the Storybook stories on Chromatic. I know it’s a bit of a hassle with around 180 stories affected, but it’s all for a good cause 💪 Main changes: - Update the `Button` component to match the [new buttons design](https://www.figma.com/design/WfqIgsTFXN2BscBSSyXWF8/Coder-kit?node-id=3-1756&p=f&m=dev). - Update forms and dialogs to use the new `Button` component. Related to https://github.com/coder/coder/issues/14978
This commit is contained in:
@ -147,7 +147,7 @@ export const createWorkspace = async (
|
||||
await popup.waitForSelector("text=You are now authenticated.");
|
||||
}
|
||||
|
||||
await page.getByTestId("form-submit").click();
|
||||
await page.getByRole("button", { name: /create workspace/i }).click();
|
||||
|
||||
const user = currentUser(page);
|
||||
|
||||
@ -276,7 +276,7 @@ export const createTemplate = async (
|
||||
|
||||
const name = randomName();
|
||||
await page.getByLabel("Name *").fill(name);
|
||||
await page.getByTestId("form-submit").click();
|
||||
await page.getByRole("button", { name: /save/i }).click();
|
||||
await expectUrl(page).toHavePathName(
|
||||
organizationsEnabled
|
||||
? `/templates/${orgName}/${name}/files`
|
||||
@ -298,7 +298,7 @@ export const createGroup = async (page: Page): Promise<string> => {
|
||||
|
||||
const name = randomName();
|
||||
await page.getByLabel("Name", { exact: true }).fill(name);
|
||||
await page.getByTestId("form-submit").click();
|
||||
await page.getByRole("button", { name: /save/i }).click();
|
||||
await expectUrl(page).toHavePathName(`/groups/${name}`);
|
||||
return name;
|
||||
};
|
||||
@ -982,7 +982,7 @@ export const updateTemplateSettings = async (
|
||||
await page.getByLabel(labelText, { exact: true }).fill(value);
|
||||
}
|
||||
|
||||
await page.getByTestId("form-submit").click();
|
||||
await page.getByRole("button", { name: /save/i }).click();
|
||||
|
||||
const name = templateSettingValues.name ?? templateName;
|
||||
await expectUrl(page).toHavePathNameEndingWith(`/${name}`);
|
||||
@ -1003,7 +1003,7 @@ export const updateWorkspace = async (
|
||||
await page.getByTestId("confirm-button").click();
|
||||
|
||||
await fillParameters(page, richParameters, buildParameters);
|
||||
await page.getByTestId("form-submit").click();
|
||||
await page.getByRole("button", { name: /update parameters/i }).click();
|
||||
|
||||
await page.waitForSelector("*[data-testid='build-status'] >> text=Running", {
|
||||
state: "visible",
|
||||
@ -1024,7 +1024,7 @@ export const updateWorkspaceParameters = async (
|
||||
);
|
||||
|
||||
await fillParameters(page, richParameters, buildParameters);
|
||||
await page.getByTestId("form-submit").click();
|
||||
await page.getByRole("button", { name: /submit and restart/i }).click();
|
||||
|
||||
await page.waitForSelector("*[data-testid='build-status'] >> text=Running", {
|
||||
state: "visible",
|
||||
@ -1091,7 +1091,7 @@ export async function createUser(
|
||||
// as the label for the currently active option.
|
||||
const passwordField = page.locator("input[name=password]");
|
||||
await passwordField.fill(password);
|
||||
await page.getByRole("button", { name: "Create user" }).click();
|
||||
await page.getByRole("button", { name: /save/i }).click();
|
||||
await expect(page.getByText("Successfully created user.")).toBeVisible();
|
||||
|
||||
await expect(page).toHaveTitle("Users - Coder");
|
||||
@ -1123,7 +1123,7 @@ export async function createOrganization(page: Page): Promise<{
|
||||
const description = `Org description ${name}`;
|
||||
await page.getByLabel("Description").fill(description);
|
||||
await page.getByLabel("Icon", { exact: true }).fill("/emojis/1f957.png");
|
||||
await page.getByRole("button", { name: "Submit" }).click();
|
||||
await page.getByRole("button", { name: /save/i }).click();
|
||||
|
||||
await expectUrl(page).toHavePathName(`/organizations/${name}`);
|
||||
await expect(page.getByText("Organization created.")).toBeVisible();
|
||||
|
@ -67,14 +67,14 @@ test.describe("IdpOrgSyncPage", () => {
|
||||
const syncField = page.getByRole("textbox", {
|
||||
name: "Organization sync field",
|
||||
});
|
||||
const saveButton = page.getByRole("button", { name: "Save" }).first();
|
||||
const saveButton = page.getByRole("button", { name: /save/i }).first();
|
||||
|
||||
await expect(saveButton).toBeDisabled();
|
||||
|
||||
await syncField.fill("test-field");
|
||||
await expect(saveButton).toBeEnabled();
|
||||
|
||||
await page.getByRole("button", { name: "Save" }).click();
|
||||
await page.getByRole("button", { name: /save/i }).click();
|
||||
|
||||
await expect(
|
||||
page.getByText("Organization sync settings updated."),
|
||||
|
@ -27,7 +27,7 @@ test("create group", async ({ page, baseURL }) => {
|
||||
await page.getByLabel("Name", { exact: true }).fill(groupValues.name);
|
||||
await page.getByLabel("Display Name").fill(groupValues.displayName);
|
||||
await page.getByLabel("Avatar URL").fill(groupValues.avatarURL);
|
||||
await page.getByRole("button", { name: "Submit" }).click();
|
||||
await page.getByRole("button", { name: /save/i }).click();
|
||||
|
||||
await expect(page).toHaveTitle(`${groupValues.displayName} - Coder`);
|
||||
await expect(page.getByText(groupValues.displayName)).toBeVisible();
|
||||
|
@ -34,7 +34,7 @@ test("create group", async ({ page }) => {
|
||||
const displayName = `Group ${name}`;
|
||||
await page.getByLabel("Display Name").fill(displayName);
|
||||
await page.getByLabel("Avatar URL").fill("/emojis/1f60d.png");
|
||||
await page.getByRole("button", { name: "Submit" }).click();
|
||||
await page.getByRole("button", { name: /save/i }).click();
|
||||
|
||||
await expectUrl(page).toHavePathName(
|
||||
`/organizations/${org.name}/groups/${name}`,
|
||||
@ -91,7 +91,7 @@ test("change quota settings", async ({ page }) => {
|
||||
|
||||
// Update Quota
|
||||
await page.getByLabel("Quota Allowance").fill("100");
|
||||
await page.getByRole("button", { name: "Submit" }).click();
|
||||
await page.getByRole("button", { name: /save/i }).click();
|
||||
|
||||
// We should get sent back to the group page afterwards
|
||||
expectUrl(page).toHavePathName(
|
||||
|
@ -23,7 +23,7 @@ test("create and delete organization", async ({ page }) => {
|
||||
await page.getByLabel("Display name").fill(`Org ${name}`);
|
||||
await page.getByLabel("Description").fill(`Org description ${name}`);
|
||||
await page.getByLabel("Icon", { exact: true }).fill("/emojis/1f957.png");
|
||||
await page.getByRole("button", { name: "Submit" }).click();
|
||||
await page.getByRole("button", { name: /save/i }).click();
|
||||
|
||||
// Expect to be redirected to the new organization
|
||||
await expectUrl(page).toHavePathName(`/organizations/${name}`);
|
||||
@ -32,7 +32,7 @@ test("create and delete organization", async ({ page }) => {
|
||||
const newName = randomName();
|
||||
await page.getByLabel("Slug").fill(newName);
|
||||
await page.getByLabel("Description").fill(`Org description ${newName}`);
|
||||
await page.getByRole("button", { name: "Submit" }).click();
|
||||
await page.getByRole("button", { name: /save/i }).click();
|
||||
|
||||
// Expect to be redirected when renaming the organization
|
||||
await expectUrl(page).toHavePathName(`/organizations/${newName}`);
|
||||
|
@ -87,7 +87,7 @@ test.describe("CustomRolesPage", () => {
|
||||
await expect(organizationMemberCheckbox).toBeVisible();
|
||||
await organizationMemberCheckbox.click();
|
||||
|
||||
const saveButton = page.getByRole("button", { name: "Save" }).first();
|
||||
const saveButton = page.getByRole("button", { name: /save/i }).first();
|
||||
await expect(saveButton).toBeVisible();
|
||||
await saveButton.click();
|
||||
|
||||
|
@ -36,7 +36,7 @@ test("update template schedule settings without override other settings", async
|
||||
waitUntil: "domcontentloaded",
|
||||
});
|
||||
await page.getByLabel("Default autostop (hours)").fill("48");
|
||||
await page.getByRole("button", { name: "Submit" }).click();
|
||||
await page.getByRole("button", { name: /save/i }).click();
|
||||
await expect(page.getByText("Template updated successfully")).toBeVisible();
|
||||
|
||||
const updatedTemplate = await API.getTemplate(template.id);
|
||||
|
@ -67,7 +67,7 @@ test("require latest version", async ({ page }) => {
|
||||
await expectUrl(page).toHavePathName(`/templates/${templateName}/settings`);
|
||||
let checkbox = await page.waitForSelector("#require_active_version");
|
||||
await checkbox.click();
|
||||
await page.getByTestId("form-submit").click();
|
||||
await page.getByRole("button", { name: /save/i }).click();
|
||||
|
||||
await page.goto(`/templates/${templateName}/settings`, {
|
||||
waitUntil: "domcontentloaded",
|
||||
|
@ -1,5 +1,5 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { Trash } from "lucide-react";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { Button } from "./Button";
|
||||
|
||||
const meta: Meta<typeof Button> = {
|
||||
@ -8,7 +8,7 @@ const meta: Meta<typeof Button> = {
|
||||
args: {
|
||||
children: (
|
||||
<>
|
||||
<Trash />
|
||||
<PlusIcon />
|
||||
Button
|
||||
</>
|
||||
),
|
||||
@ -20,30 +20,24 @@ type Story = StoryObj<typeof Button>;
|
||||
|
||||
export const Default: Story = {};
|
||||
|
||||
export const Outline: Story = {
|
||||
args: {
|
||||
variant: "outline",
|
||||
},
|
||||
};
|
||||
|
||||
export const Subtle: Story = {
|
||||
args: {
|
||||
variant: "subtle",
|
||||
},
|
||||
};
|
||||
|
||||
export const Warning: Story = {
|
||||
args: {
|
||||
variant: "warning",
|
||||
},
|
||||
};
|
||||
|
||||
export const DefaultDisabled: Story = {
|
||||
args: {
|
||||
disabled: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const DefaultSmall: Story = {
|
||||
args: {
|
||||
size: "sm",
|
||||
},
|
||||
};
|
||||
|
||||
export const Outline: Story = {
|
||||
args: {
|
||||
variant: "outline",
|
||||
},
|
||||
};
|
||||
|
||||
export const OutlineDisabled: Story = {
|
||||
args: {
|
||||
variant: "outline",
|
||||
@ -51,6 +45,19 @@ export const OutlineDisabled: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
export const OutlineSmall: Story = {
|
||||
args: {
|
||||
variant: "outline",
|
||||
size: "sm",
|
||||
},
|
||||
};
|
||||
|
||||
export const Subtle: Story = {
|
||||
args: {
|
||||
variant: "subtle",
|
||||
},
|
||||
};
|
||||
|
||||
export const SubtleDisabled: Story = {
|
||||
args: {
|
||||
variant: "subtle",
|
||||
@ -58,23 +65,51 @@ export const SubtleDisabled: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
export const SubtleSmall: Story = {
|
||||
args: {
|
||||
variant: "subtle",
|
||||
size: "sm",
|
||||
},
|
||||
};
|
||||
|
||||
export const Destructive: Story = {
|
||||
args: {
|
||||
variant: "destructive",
|
||||
children: "Delete",
|
||||
},
|
||||
};
|
||||
|
||||
export const DestructiveDisabled: Story = {
|
||||
args: {
|
||||
...Destructive.args,
|
||||
disabled: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const DestructiveSmall: Story = {
|
||||
args: {
|
||||
...Destructive.args,
|
||||
size: "sm",
|
||||
},
|
||||
};
|
||||
|
||||
export const IconButtonDefault: Story = {
|
||||
args: {
|
||||
variant: "default",
|
||||
children: <Trash />,
|
||||
children: <PlusIcon />,
|
||||
},
|
||||
};
|
||||
|
||||
export const IconButtonOutline: Story = {
|
||||
args: {
|
||||
variant: "outline",
|
||||
children: <Trash />,
|
||||
children: <PlusIcon />,
|
||||
},
|
||||
};
|
||||
|
||||
export const IconButtonSubtle: Story = {
|
||||
args: {
|
||||
variant: "subtle",
|
||||
children: <Trash />,
|
||||
children: <PlusIcon />,
|
||||
},
|
||||
};
|
||||
|
@ -8,38 +8,33 @@ import { forwardRef } from "react";
|
||||
import { cn } from "utils/cn";
|
||||
|
||||
export const buttonVariants = cva(
|
||||
`inline-flex items-center justify-center gap-2 whitespace-nowrap
|
||||
`inline-flex items-center justify-center gap-1 whitespace-nowrap
|
||||
border-solid rounded-md transition-colors
|
||||
text-sm font-semibold font-medium cursor-pointer
|
||||
text-sm font-semibold font-medium cursor-pointer no-underline
|
||||
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-content-link
|
||||
disabled:pointer-events-none disabled:text-content-disabled
|
||||
[&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0
|
||||
px-3 py-2`,
|
||||
[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg]:p-[2px]`,
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-surface-invert-primary text-content-invert hover:bg-surface-invert-secondary border-none disabled:bg-surface-secondary",
|
||||
"bg-surface-invert-primary text-content-invert hover:bg-surface-invert-secondary border-none disabled:bg-surface-secondary font-semibold",
|
||||
outline:
|
||||
"border border-border-default text-content-primary bg-transparent hover:bg-surface-secondary",
|
||||
subtle:
|
||||
"border-none bg-transparent text-content-secondary hover:text-content-primary",
|
||||
warning:
|
||||
"border border-border-error text-content-primary bg-surface-error hover:bg-transparent",
|
||||
ghost:
|
||||
"text-content-primary bg-transparent border-0 hover:bg-surface-secondary",
|
||||
destructive:
|
||||
"border border-border-destructive text-content-primary bg-surface-destructive hover:bg-transparent disabled:bg-transparent disabled:text-content-disabled font-semibold",
|
||||
},
|
||||
|
||||
size: {
|
||||
lg: "h-10",
|
||||
default: "h-9",
|
||||
sm: "h-8 px-2 py-1.5 text-xs",
|
||||
icon: "h-10 w-10",
|
||||
lg: "h-10 px-3 py-2 [&_svg]:size-icon-lg",
|
||||
sm: "h-[30px] px-2 py-1.5 text-xs [&_svg]:size-icon-sm",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
size: "lg",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { action } from "@storybook/addon-actions";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { userEvent } from "@storybook/test";
|
||||
import { within } from "@testing-library/react";
|
||||
import { DeleteDialog } from "./DeleteDialog";
|
||||
|
||||
const meta: Meta<typeof DeleteDialog> = {
|
||||
@ -19,12 +21,28 @@ export default meta;
|
||||
|
||||
type Story = StoryObj<typeof DeleteDialog>;
|
||||
|
||||
const Example: Story = {};
|
||||
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,
|
||||
},
|
||||
};
|
||||
|
||||
export { Example as DeleteDialog };
|
||||
|
@ -1,8 +1,6 @@
|
||||
import type { Interpolation, Theme } from "@emotion/react";
|
||||
import LoadingButton, { type LoadingButtonProps } from "@mui/lab/LoadingButton";
|
||||
import MuiDialog, {
|
||||
type DialogProps as MuiDialogProps,
|
||||
} from "@mui/material/Dialog";
|
||||
import MuiDialog, { type DialogProps } from "@mui/material/Dialog";
|
||||
import { Button } from "components/Button/Button";
|
||||
import { Spinner } from "components/Spinner/Spinner";
|
||||
import type { FC, ReactNode } from "react";
|
||||
import type { ConfirmDialogType } from "./types";
|
||||
|
||||
@ -22,13 +20,6 @@ export interface DialogActionButtonsProps {
|
||||
type?: ConfirmDialogType;
|
||||
}
|
||||
|
||||
const typeToColor = (type: ConfirmDialogType): LoadingButtonProps["color"] => {
|
||||
if (type === "delete") {
|
||||
return "secondary";
|
||||
}
|
||||
return "primary";
|
||||
};
|
||||
|
||||
/**
|
||||
* Quickly handles most modals actions, some combination of a cancel and confirm button
|
||||
*/
|
||||
@ -44,124 +35,29 @@ export const DialogActionButtons: FC<DialogActionButtonsProps> = ({
|
||||
return (
|
||||
<>
|
||||
{onCancel && (
|
||||
<LoadingButton disabled={confirmLoading} onClick={onCancel} fullWidth>
|
||||
<Button disabled={confirmLoading} onClick={onCancel} variant="outline">
|
||||
{cancelText}
|
||||
</LoadingButton>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{onConfirm && (
|
||||
<LoadingButton
|
||||
fullWidth
|
||||
data-testid="confirm-button"
|
||||
variant="contained"
|
||||
<Button
|
||||
variant={type === "delete" ? "destructive" : undefined}
|
||||
disabled={confirmLoading || disabled}
|
||||
onClick={onConfirm}
|
||||
color={typeToColor(type)}
|
||||
loading={confirmLoading}
|
||||
disabled={disabled}
|
||||
data-testid="confirm-button"
|
||||
type="submit"
|
||||
css={[
|
||||
type === "delete" && styles.dangerButton,
|
||||
type === "success" && styles.successButton,
|
||||
]}
|
||||
>
|
||||
{confirmLoading && <Spinner loading />}
|
||||
{confirmText}
|
||||
</LoadingButton>
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = {
|
||||
dangerButton: (theme) => ({
|
||||
"&.MuiButton-contained": {
|
||||
backgroundColor: theme.roles.danger.fill.solid,
|
||||
borderColor: theme.roles.danger.fill.outline,
|
||||
|
||||
"&:not(.MuiLoadingButton-loading)": {
|
||||
color: theme.roles.danger.fill.text,
|
||||
},
|
||||
|
||||
"&:hover:not(:disabled)": {
|
||||
backgroundColor: theme.roles.danger.hover.fill.solid,
|
||||
borderColor: theme.roles.danger.hover.fill.outline,
|
||||
},
|
||||
|
||||
"&.Mui-disabled": {
|
||||
backgroundColor: theme.roles.danger.disabled.background,
|
||||
borderColor: theme.roles.danger.disabled.outline,
|
||||
|
||||
"&:not(.MuiLoadingButton-loading)": {
|
||||
color: theme.roles.danger.disabled.fill.text,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
successButton: (theme) => ({
|
||||
"&.MuiButton-contained": {
|
||||
backgroundColor: theme.palette.success.dark,
|
||||
|
||||
"&:not(.MuiLoadingButton-loading)": {
|
||||
color: theme.palette.primary.contrastText,
|
||||
},
|
||||
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.success.main,
|
||||
|
||||
"@media (hover: none)": {
|
||||
backgroundColor: "transparent",
|
||||
},
|
||||
|
||||
"&.Mui-disabled": {
|
||||
backgroundColor: "transparent",
|
||||
},
|
||||
},
|
||||
|
||||
"&.Mui-disabled": {
|
||||
backgroundColor: theme.palette.success.dark,
|
||||
|
||||
"&:not(.MuiLoadingButton-loading)": {
|
||||
color: theme.palette.text.secondary,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
"&.MuiButton-outlined": {
|
||||
color: theme.palette.success.main,
|
||||
borderColor: theme.palette.success.main,
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.success.dark,
|
||||
"@media (hover: none)": {
|
||||
backgroundColor: "transparent",
|
||||
},
|
||||
"&.Mui-disabled": {
|
||||
backgroundColor: "transparent",
|
||||
},
|
||||
},
|
||||
"&.Mui-disabled": {
|
||||
color: theme.palette.text.secondary,
|
||||
borderColor: theme.palette.action.disabled,
|
||||
},
|
||||
},
|
||||
|
||||
"&.MuiButton-text": {
|
||||
color: theme.palette.success.main,
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.success.dark,
|
||||
"@media (hover: none)": {
|
||||
backgroundColor: "transparent",
|
||||
},
|
||||
},
|
||||
"&.Mui-disabled": {
|
||||
color: theme.palette.text.secondary,
|
||||
},
|
||||
},
|
||||
}),
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
|
||||
export type DialogProps = MuiDialogProps;
|
||||
|
||||
/**
|
||||
* Re-export of MUI's Dialog component, for convenience.
|
||||
* @link See original documentation here: https://mui.com/material-ui/react-dialog/
|
||||
*/
|
||||
export { MuiDialog as Dialog };
|
||||
export { MuiDialog as Dialog, type DialogProps };
|
||||
|
@ -10,11 +10,7 @@ import {
|
||||
forwardRef,
|
||||
useContext,
|
||||
} from "react";
|
||||
import {
|
||||
FormFooter as BaseFormFooter,
|
||||
type FormFooterProps,
|
||||
type FormFooterStyles,
|
||||
} from "../FormFooter/FormFooter";
|
||||
import { cn } from "utils/cn";
|
||||
|
||||
type FormContextValue = { direction?: "horizontal" | "vertical" };
|
||||
|
||||
@ -191,29 +187,12 @@ const styles = {
|
||||
},
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
|
||||
export const FormFooter: FC<Exclude<FormFooterProps, "styles">> = (props) => (
|
||||
<BaseFormFooter {...props} styles={footerStyles} />
|
||||
export const FormFooter: FC<HTMLProps<HTMLDivElement>> = ({
|
||||
className,
|
||||
...props
|
||||
}) => (
|
||||
<footer
|
||||
className={cn("flex items-center justify-end space-x-2 mt-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
const footerStyles = {
|
||||
button: (theme) => ({
|
||||
minWidth: 184,
|
||||
|
||||
[theme.breakpoints.down("md")]: {
|
||||
width: "100%",
|
||||
},
|
||||
}),
|
||||
|
||||
footer: (theme) => ({
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-start",
|
||||
flexDirection: "row-reverse",
|
||||
gap: 16,
|
||||
|
||||
[theme.breakpoints.down("md")]: {
|
||||
flexDirection: "column",
|
||||
gap: 8,
|
||||
},
|
||||
}),
|
||||
} satisfies FormFooterStyles;
|
||||
|
@ -1,37 +0,0 @@
|
||||
import { action } from "@storybook/addon-actions";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { FormFooter } from "./FormFooter";
|
||||
|
||||
const meta: Meta<typeof FormFooter> = {
|
||||
title: "components/FormFooter",
|
||||
component: FormFooter,
|
||||
args: {
|
||||
isLoading: false,
|
||||
onCancel: action("onCancel"),
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof FormFooter>;
|
||||
|
||||
export const Ready: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const NoCancel: Story = {
|
||||
args: {
|
||||
onCancel: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
export const Custom: Story = {
|
||||
args: {
|
||||
submitLabel: "Create",
|
||||
},
|
||||
};
|
||||
|
||||
export const Loading: Story = {
|
||||
args: {
|
||||
isLoading: true,
|
||||
},
|
||||
};
|
@ -1,78 +0,0 @@
|
||||
import type { Interpolation, Theme } from "@emotion/react";
|
||||
import LoadingButton from "@mui/lab/LoadingButton";
|
||||
import Button from "@mui/material/Button";
|
||||
import type { FC } from "react";
|
||||
|
||||
export const Language = {
|
||||
cancelLabel: "Cancel",
|
||||
defaultSubmitLabel: "Submit",
|
||||
};
|
||||
|
||||
export interface FormFooterStyles {
|
||||
footer: Interpolation<Theme>;
|
||||
button: Interpolation<Theme>;
|
||||
}
|
||||
|
||||
export interface FormFooterProps {
|
||||
onCancel?: () => void;
|
||||
isLoading: boolean;
|
||||
styles?: FormFooterStyles;
|
||||
submitLabel?: string;
|
||||
submitDisabled?: boolean;
|
||||
extraActions?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const FormFooter: FC<FormFooterProps> = ({
|
||||
onCancel,
|
||||
extraActions,
|
||||
isLoading,
|
||||
submitDisabled,
|
||||
submitLabel = Language.defaultSubmitLabel,
|
||||
styles = defaultStyles,
|
||||
}) => {
|
||||
return (
|
||||
<div css={styles.footer}>
|
||||
<LoadingButton
|
||||
size="large"
|
||||
tabIndex={0}
|
||||
loading={isLoading}
|
||||
css={styles.button}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
type="submit"
|
||||
disabled={submitDisabled}
|
||||
data-testid="form-submit"
|
||||
>
|
||||
{submitLabel}
|
||||
</LoadingButton>
|
||||
{onCancel && (
|
||||
<Button
|
||||
size="large"
|
||||
type="button"
|
||||
css={styles.button}
|
||||
onClick={onCancel}
|
||||
tabIndex={0}
|
||||
>
|
||||
{Language.cancelLabel}
|
||||
</Button>
|
||||
)}
|
||||
{extraActions}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const defaultStyles = {
|
||||
footer: {
|
||||
display: "flex",
|
||||
flex: "0",
|
||||
// The first button is the submit so it is the first element to be focused
|
||||
// on tab so we use row-reverse to display it on the right
|
||||
flexDirection: "row-reverse",
|
||||
gap: 12,
|
||||
alignItems: "center",
|
||||
marginTop: 24,
|
||||
},
|
||||
button: {
|
||||
width: "100%",
|
||||
},
|
||||
} satisfies FormFooterStyles;
|
@ -1,8 +1,8 @@
|
||||
import TextField from "@mui/material/TextField";
|
||||
import { action } from "@storybook/addon-actions";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { Button } from "components/Button/Button";
|
||||
import { FormFooter } from "components/Form/Form";
|
||||
import type { FC } from "react";
|
||||
import { FormFooter } from "../FormFooter/FormFooter";
|
||||
import { Stack } from "../Stack/Stack";
|
||||
import { FullPageForm, type FullPageFormProps } from "./FullPageForm";
|
||||
|
||||
@ -16,7 +16,10 @@ const Template: FC<FullPageFormProps> = (props) => (
|
||||
<Stack>
|
||||
<TextField fullWidth label="Field 1" name="field1" />
|
||||
<TextField fullWidth label="Field 2" name="field2" />
|
||||
<FormFooter isLoading={false} onCancel={action("cancel")} />
|
||||
<FormFooter>
|
||||
<Button variant="outline">Cancel</Button>
|
||||
<Button type="submit">Save</Button>
|
||||
</FormFooter>
|
||||
</Stack>
|
||||
</form>
|
||||
</FullPageForm>
|
||||
|
@ -18,10 +18,10 @@
|
||||
--surface-quaternary: 240, 5%, 84%;
|
||||
--surface-invert-primary: 240, 4%, 16%;
|
||||
--surface-invert-secondary: 240, 5%, 26%;
|
||||
--surface-error: 0, 100%, 14%;
|
||||
--surface-destructive: 0, 93%, 94%;
|
||||
--border-default: 240, 6%, 90%;
|
||||
--border-success: 142, 76%, 36%;
|
||||
--border-error: 0, 84%, 60%;
|
||||
--border-destructive: 0, 84%, 60%;
|
||||
--radius: 0.5rem;
|
||||
--chart-1: 12 76% 61%;
|
||||
--chart-2: 173 58% 39%;
|
||||
@ -31,7 +31,6 @@
|
||||
--border: 240 5.9% 90%;
|
||||
--input: 240 5.9% 90%;
|
||||
--ring: 240 10% 3.9%;
|
||||
|
||||
--avatar-lg: 2.5rem;
|
||||
--avatar-default: 1.5rem;
|
||||
--avatar-sm: 1.125rem;
|
||||
@ -51,10 +50,10 @@
|
||||
--surface-quaternary: 240, 5%, 26%;
|
||||
--surface-invert-primary: 240, 6%, 90%;
|
||||
--surface-invert-secondary: 240, 5%, 65%;
|
||||
--surface-error: 0, 100%, 14%;
|
||||
--surface-destructive: 0, 75%, 15%;
|
||||
--border-default: 240, 4%, 16%;
|
||||
--border-success: 142, 76%, 36%;
|
||||
--border-error: 0, 91%, 71%;
|
||||
--border-destructive: 0, 91%, 71%;
|
||||
--chart-1: 220 70% 50%;
|
||||
--chart-2: 160 60% 45%;
|
||||
--chart-3: 30 80% 55%;
|
||||
|
@ -70,15 +70,11 @@ export const MobileMenu: FC<MobileMenuProps> = ({
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
aria-label={open ? "Close menu" : "Open menu"}
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="ml-auto md:hidden [&_svg]:size-6"
|
||||
size="lg"
|
||||
variant="subtle"
|
||||
className="ml-auto md:hidden"
|
||||
>
|
||||
{open ? (
|
||||
<XIcon className="size-icon-lg" />
|
||||
) : (
|
||||
<MenuIcon className="size-icon-lg" />
|
||||
)}
|
||||
{open ? <XIcon /> : <MenuIcon />}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
|
@ -11,6 +11,7 @@ import type {
|
||||
VariableValue,
|
||||
} from "api/typesGenerated";
|
||||
import { Alert } from "components/Alert/Alert";
|
||||
import { Button } from "components/Button/Button";
|
||||
import {
|
||||
FormFields,
|
||||
FormFooter,
|
||||
@ -19,6 +20,7 @@ import {
|
||||
} from "components/Form/Form";
|
||||
import { IconField } from "components/IconField/IconField";
|
||||
import { OrganizationAutocomplete } from "components/OrganizationAutocomplete/OrganizationAutocomplete";
|
||||
import { Spinner } from "components/Spinner/Spinner";
|
||||
import { useFormik } from "formik";
|
||||
import camelCase from "lodash/camelCase";
|
||||
import capitalize from "lodash/capitalize";
|
||||
@ -350,10 +352,15 @@ export const CreateTemplateForm: FC<CreateTemplateFormProps> = (props) => {
|
||||
</FormSection>
|
||||
)}
|
||||
|
||||
<div className="flex items-center">
|
||||
<FormFooter
|
||||
extraActions={
|
||||
logs && (
|
||||
<FormFooter>
|
||||
<Button onClick={onCancel} variant="outline">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={isSubmitting}>
|
||||
{isSubmitting && <Spinner />}
|
||||
{jobError ? "Retry" : "Save"}
|
||||
</Button>
|
||||
{logs && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onOpenBuildLogsDrawer}
|
||||
@ -374,13 +381,8 @@ export const CreateTemplateForm: FC<CreateTemplateFormProps> = (props) => {
|
||||
>
|
||||
Show build logs
|
||||
</button>
|
||||
)
|
||||
}
|
||||
onCancel={onCancel}
|
||||
isLoading={isSubmitting}
|
||||
submitLabel={jobError ? "Retry" : "Create template"}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</FormFooter>
|
||||
</HorizontalForm>
|
||||
);
|
||||
};
|
||||
|
@ -61,9 +61,7 @@ test("Create template from starter template", async () => {
|
||||
MockTemplateVersionVariable3,
|
||||
]);
|
||||
await userEvent.type(screen.getByLabelText(/Name/), "my-template");
|
||||
await userEvent.click(
|
||||
within(form).getByRole("button", { name: /create template/i }),
|
||||
);
|
||||
await userEvent.click(within(form).getByRole("button", { name: /save/i }));
|
||||
|
||||
// Wait for the drawer error to be rendered
|
||||
await screen.findByRole("heading", { name: /missing variables/i });
|
||||
@ -92,9 +90,7 @@ test("Create template from starter template", async () => {
|
||||
.mockResolvedValue(MockTemplateVersion);
|
||||
jest.spyOn(API, "getTemplateVersion").mockResolvedValue(MockTemplateVersion);
|
||||
jest.spyOn(API, "createTemplate").mockResolvedValue(MockTemplate);
|
||||
await userEvent.click(
|
||||
within(form).getByRole("button", { name: /create template/i }),
|
||||
);
|
||||
await userEvent.click(within(form).getByRole("button", { name: /save/i }));
|
||||
await waitFor(() => expect(API.createTemplate).toBeCalledTimes(1));
|
||||
expect(router.state.location.pathname).toEqual(
|
||||
`/templates/${MockTemplate.name}/files`,
|
||||
@ -142,9 +138,7 @@ test("Create template from duplicating a template", async () => {
|
||||
.mockResolvedValue(MockTemplateVersion);
|
||||
jest.spyOn(API, "getTemplateVersion").mockResolvedValue(MockTemplateVersion);
|
||||
jest.spyOn(API, "createTemplate").mockResolvedValue(MockTemplate);
|
||||
await userEvent.click(
|
||||
screen.getByRole("button", { name: /create template/i }),
|
||||
);
|
||||
await userEvent.click(screen.getByRole("button", { name: /save/i }));
|
||||
await waitFor(() => {
|
||||
expect(router.state.location.pathname).toEqual(
|
||||
`/templates/${MockTemplate.name}/files`,
|
||||
|
@ -1,12 +1,14 @@
|
||||
import { css } from "@emotion/css";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import { Button } from "components/Button/Button";
|
||||
import {
|
||||
FormFields,
|
||||
FormFooter,
|
||||
FormSection,
|
||||
HorizontalForm,
|
||||
} from "components/Form/Form";
|
||||
import { Spinner } from "components/Spinner/Spinner";
|
||||
import { Stack } from "components/Stack/Stack";
|
||||
import dayjs from "dayjs";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
@ -144,11 +146,16 @@ export const CreateTokenForm: FC<CreateTokenFormProps> = ({
|
||||
</Stack>
|
||||
</FormFields>
|
||||
</FormSection>
|
||||
<FormFooter
|
||||
onCancel={() => navigate("/settings/tokens")}
|
||||
isLoading={isCreating}
|
||||
submitLabel={creationFailed ? "Retry" : "Create token"}
|
||||
/>
|
||||
|
||||
<FormFooter>
|
||||
<Button onClick={() => navigate("/settings/tokens")} variant="outline">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={isCreating}>
|
||||
{isCreating && <Spinner />}
|
||||
{creationFailed ? "Retry" : "Create token"}
|
||||
</Button>
|
||||
</FormFooter>
|
||||
</HorizontalForm>
|
||||
);
|
||||
};
|
||||
|
@ -4,12 +4,14 @@ import TextField from "@mui/material/TextField";
|
||||
import { hasApiFieldErrors, isApiError } from "api/errors";
|
||||
import type * as TypesGen from "api/typesGenerated";
|
||||
import { ErrorAlert } from "components/Alert/ErrorAlert";
|
||||
import { FormFooter } from "components/FormFooter/FormFooter";
|
||||
import { Button } from "components/Button/Button";
|
||||
import { FormFooter } from "components/Form/Form";
|
||||
import { FullPageForm } from "components/FullPageForm/FullPageForm";
|
||||
import { PasswordField } from "components/PasswordField/PasswordField";
|
||||
import { Spinner } from "components/Spinner/Spinner";
|
||||
import { Stack } from "components/Stack/Stack";
|
||||
import { type FormikContextType, useFormik } from "formik";
|
||||
import { type FC, useEffect } from "react";
|
||||
import type { FC } from "react";
|
||||
import {
|
||||
displayNameValidator,
|
||||
getFormHelpers,
|
||||
@ -202,11 +204,16 @@ export const CreateUserForm: FC<
|
||||
label={Language.passwordLabel}
|
||||
/>
|
||||
</Stack>
|
||||
<FormFooter
|
||||
submitLabel="Create user"
|
||||
onCancel={onCancel}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
|
||||
<FormFooter className="mt-8">
|
||||
<Button onClick={onCancel} variant="outline">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={isLoading}>
|
||||
{isLoading && <Spinner />}
|
||||
Save
|
||||
</Button>
|
||||
</FormFooter>
|
||||
</form>
|
||||
</FullPageForm>
|
||||
);
|
||||
|
@ -35,7 +35,7 @@ const fillForm = async ({
|
||||
await userEvent.type(loginTypeField, "password");
|
||||
await userEvent.type(passwordField as HTMLElement, password);
|
||||
const submitButton = screen.getByRole("button", {
|
||||
name: "Create user",
|
||||
name: /save/i,
|
||||
});
|
||||
fireEvent.click(submitButton);
|
||||
};
|
||||
|
@ -22,7 +22,7 @@ import CreateWorkspacePage from "./CreateWorkspacePage";
|
||||
import { Language } from "./CreateWorkspacePageView";
|
||||
|
||||
const nameLabelText = "Workspace Name";
|
||||
const createWorkspaceText = "Create Workspace";
|
||||
const createWorkspaceText = "Create workspace";
|
||||
const validationNumberNotInRangeText = "Value must be between 1 and 3.";
|
||||
|
||||
const renderCreateWorkspacePage = () => {
|
||||
@ -93,7 +93,7 @@ describe("CreateWorkspacePage", () => {
|
||||
renderCreateWorkspacePage();
|
||||
await waitForLoaderToBeRemoved();
|
||||
|
||||
const element = await screen.findByText("Create Workspace");
|
||||
const element = await screen.findByText(createWorkspaceText);
|
||||
expect(element).toBeDefined();
|
||||
const secondParameter = await screen.findByText(
|
||||
MockTemplateVersionParameter2.description,
|
||||
|
@ -1,11 +1,11 @@
|
||||
import type { Interpolation, Theme } from "@emotion/react";
|
||||
import Button from "@mui/material/Button";
|
||||
import FormHelperText from "@mui/material/FormHelperText";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import type * as TypesGen from "api/typesGenerated";
|
||||
import { Alert } from "components/Alert/Alert";
|
||||
import { ErrorAlert } from "components/Alert/ErrorAlert";
|
||||
import { Avatar } from "components/Avatar/Avatar";
|
||||
import { Button } from "components/Button/Button";
|
||||
import {
|
||||
FormFields,
|
||||
FormFooter,
|
||||
@ -20,6 +20,7 @@ import {
|
||||
} from "components/PageHeader/PageHeader";
|
||||
import { Pill } from "components/Pill/Pill";
|
||||
import { RichParameterInput } from "components/RichParameterInput/RichParameterInput";
|
||||
import { Spinner } from "components/Spinner/Spinner";
|
||||
import { Stack } from "components/Stack/Stack";
|
||||
import { UserAutocomplete } from "components/UserAutocomplete/UserAutocomplete";
|
||||
import { type FormikContextType, useFormik } from "formik";
|
||||
@ -146,7 +147,13 @@ export const CreateWorkspacePageView: FC<CreateWorkspacePageViewProps> = ({
|
||||
|
||||
return (
|
||||
<Margins size="medium">
|
||||
<PageHeader actions={<Button onClick={onCancel}>Cancel</Button>}>
|
||||
<PageHeader
|
||||
actions={
|
||||
<Button size="sm" variant="outline" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Stack direction="row">
|
||||
<Avatar
|
||||
variant="icon"
|
||||
@ -218,7 +225,8 @@ export const CreateWorkspacePageView: FC<CreateWorkspacePageViewProps> = ({
|
||||
<FormHelperText data-chromatic="ignore">
|
||||
Need a suggestion?{" "}
|
||||
<Button
|
||||
variant="text"
|
||||
variant="subtle"
|
||||
size="sm"
|
||||
css={styles.nameSuggestion}
|
||||
onClick={async () => {
|
||||
await form.setFieldValue("name", suggestedName);
|
||||
@ -306,12 +314,18 @@ export const CreateWorkspacePageView: FC<CreateWorkspacePageViewProps> = ({
|
||||
</FormSection>
|
||||
)}
|
||||
|
||||
<FormFooter
|
||||
onCancel={onCancel}
|
||||
isLoading={creatingWorkspace}
|
||||
submitDisabled={!hasAllRequiredExternalAuth}
|
||||
submitLabel="Create Workspace"
|
||||
/>
|
||||
<FormFooter>
|
||||
<Button onClick={onCancel} variant="outline">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={creatingWorkspace || !hasAllRequiredExternalAuth}
|
||||
>
|
||||
{creatingWorkspace && <Spinner />}
|
||||
Create workspace
|
||||
</Button>
|
||||
</FormFooter>
|
||||
</HorizontalForm>
|
||||
</Margins>
|
||||
);
|
||||
|
@ -1,4 +1,3 @@
|
||||
import Button from "@mui/material/Button";
|
||||
import InputAdornment from "@mui/material/InputAdornment";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import type { UpdateAppearanceConfig } from "api/typesGenerated";
|
||||
@ -7,6 +6,7 @@ import {
|
||||
EnterpriseBadge,
|
||||
PremiumBadge,
|
||||
} from "components/Badges/Badges";
|
||||
import { Button } from "components/Button/Button";
|
||||
import { PopoverPaywall } from "components/Paywall/PopoverPaywall";
|
||||
import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
|
||||
import {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { type CSSObject, useTheme } from "@emotion/react";
|
||||
import Button from "@mui/material/Button";
|
||||
import { Button } from "components/Button/Button";
|
||||
import type { FC, FormEventHandler, ReactNode } from "react";
|
||||
|
||||
interface FieldsetProps {
|
||||
|
@ -26,7 +26,7 @@ export const OrganizationPills: FC<OrganizationPillsProps> = ({
|
||||
{orgs.length > 0 ? (
|
||||
<Pill
|
||||
className={cn("border-none w-fit", {
|
||||
"bg-surface-error": orgs[0].isUUID,
|
||||
"bg-surface-destructive": orgs[0].isUUID,
|
||||
"bg-surface-secondary": !orgs[0].isUUID,
|
||||
})}
|
||||
>
|
||||
@ -88,7 +88,7 @@ const OverflowPillList: FC<OverflowPillProps> = ({ organizations }) => {
|
||||
<li key={organization.name} className="mb-2 last:mb-0">
|
||||
<Pill
|
||||
className={cn("border-none w-fit", {
|
||||
"bg-surface-error": organization.isUUID,
|
||||
"bg-surface-destructive": organization.isUUID,
|
||||
"bg-surface-secondary": !organization.isUUID,
|
||||
})}
|
||||
>
|
||||
|
@ -2,10 +2,12 @@ import TextField from "@mui/material/TextField";
|
||||
import { isApiValidationError } from "api/errors";
|
||||
import type { CreateGroupRequest } from "api/typesGenerated";
|
||||
import { ErrorAlert } from "components/Alert/ErrorAlert";
|
||||
import { FormFooter } from "components/FormFooter/FormFooter";
|
||||
import { Button } from "components/Button/Button";
|
||||
import { FormFooter } from "components/Form/Form";
|
||||
import { FullPageForm } from "components/FullPageForm/FullPageForm";
|
||||
import { IconField } from "components/IconField/IconField";
|
||||
import { Margins } from "components/Margins/Margins";
|
||||
import { Spinner } from "components/Spinner/Spinner";
|
||||
import { Stack } from "components/Stack/Stack";
|
||||
import { type FormikTouched, useFormik } from "formik";
|
||||
import type { FC } from "react";
|
||||
@ -76,7 +78,17 @@ export const CreateGroupPageView: FC<CreateGroupPageViewProps> = ({
|
||||
onPickEmoji={(value) => form.setFieldValue("avatar_url", value)}
|
||||
/>
|
||||
</Stack>
|
||||
<FormFooter onCancel={onCancel} isLoading={isLoading} />
|
||||
|
||||
<FormFooter className="mt-8">
|
||||
<Button onClick={onCancel} variant="outline">
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<Button type="submit" disabled={isLoading}>
|
||||
{isLoading && <Spinner />}
|
||||
Save
|
||||
</Button>
|
||||
</FormFooter>
|
||||
</form>
|
||||
</FullPageForm>
|
||||
</Margins>
|
||||
|
@ -1,10 +1,12 @@
|
||||
import TextField from "@mui/material/TextField";
|
||||
import type { Group } from "api/typesGenerated";
|
||||
import { FormFooter } from "components/FormFooter/FormFooter";
|
||||
import { Button } from "components/Button/Button";
|
||||
import { FormFooter } from "components/Form/Form";
|
||||
import { FullPageForm } from "components/FullPageForm/FullPageForm";
|
||||
import { IconField } from "components/IconField/IconField";
|
||||
import { Loader } from "components/Loader/Loader";
|
||||
import { Margins } from "components/Margins/Margins";
|
||||
import { Spinner } from "components/Spinner/Spinner";
|
||||
import { Stack } from "components/Stack/Stack";
|
||||
import { useFormik } from "formik";
|
||||
import type { FC } from "react";
|
||||
@ -105,7 +107,16 @@ const UpdateGroupForm: FC<UpdateGroupFormProps> = ({
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<FormFooter onCancel={onCancel} isLoading={isLoading} />
|
||||
<FormFooter>
|
||||
<Button onClick={onCancel} variant="outline">
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<Button type="submit" disabled={isLoading}>
|
||||
{isLoading && <Spinner />}
|
||||
Save
|
||||
</Button>
|
||||
</FormFooter>
|
||||
</form>
|
||||
</FullPageForm>
|
||||
);
|
||||
|
@ -3,6 +3,7 @@ import { isApiValidationError } from "api/errors";
|
||||
import type { CreateOrganizationRequest } from "api/typesGenerated";
|
||||
import { ErrorAlert } from "components/Alert/ErrorAlert";
|
||||
import { Badges, PremiumBadge } from "components/Badges/Badges";
|
||||
import { Button } from "components/Button/Button";
|
||||
import { ChooseOne, Cond } from "components/Conditionals/ChooseOne";
|
||||
import {
|
||||
FormFields,
|
||||
@ -14,6 +15,7 @@ import { IconField } from "components/IconField/IconField";
|
||||
import { Paywall } from "components/Paywall/Paywall";
|
||||
import { PopoverPaywall } from "components/Paywall/PopoverPaywall";
|
||||
import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
|
||||
import { Spinner } from "components/Spinner/Spinner";
|
||||
import { Stack } from "components/Stack/Stack";
|
||||
import {
|
||||
Popover,
|
||||
@ -154,7 +156,13 @@ export const CreateOrganizationPageView: FC<
|
||||
</FormFields>
|
||||
</fieldset>
|
||||
</FormSection>
|
||||
<FormFooter isLoading={form.isSubmitting} />
|
||||
|
||||
<FormFooter>
|
||||
<Button type="submit" disabled={form.isSubmitting}>
|
||||
{form.isSubmitting && <Spinner />}
|
||||
Save
|
||||
</Button>
|
||||
</FormFooter>
|
||||
</HorizontalForm>
|
||||
</Cond>
|
||||
</ChooseOne>
|
||||
|
@ -1,7 +1,6 @@
|
||||
import type { Interpolation, Theme } from "@emotion/react";
|
||||
import VisibilityOffOutlinedIcon from "@mui/icons-material/VisibilityOffOutlined";
|
||||
import VisibilityOutlinedIcon from "@mui/icons-material/VisibilityOutlined";
|
||||
import Button from "@mui/material/Button";
|
||||
import Checkbox from "@mui/material/Checkbox";
|
||||
import FormControlLabel from "@mui/material/FormControlLabel";
|
||||
import Table from "@mui/material/Table";
|
||||
@ -23,8 +22,10 @@ import type {
|
||||
Role,
|
||||
} from "api/typesGenerated";
|
||||
import { ErrorAlert } from "components/Alert/ErrorAlert";
|
||||
import { Button } from "components/Button/Button";
|
||||
import { FormFields, FormFooter, VerticalForm } from "components/Form/Form";
|
||||
import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
|
||||
import { Spinner } from "components/Spinner/Spinner";
|
||||
import { Stack } from "components/Stack/Stack";
|
||||
import { useFormik } from "formik";
|
||||
import { type ChangeEvent, type FC, useState } from "react";
|
||||
@ -84,8 +85,9 @@ export const CreateEditRolePageView: FC<CreateEditRolePageViewProps> = ({
|
||||
description="Set a name and permissions for this role."
|
||||
/>
|
||||
{canAssignOrgRole && (
|
||||
<Stack direction="row" spacing={2}>
|
||||
<div className="flex space-x-2 items-center">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
navigate(`/organizations/${organizationName}/roles`);
|
||||
}}
|
||||
@ -93,15 +95,13 @@ export const CreateEditRolePageView: FC<CreateEditRolePageViewProps> = ({
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
form.handleSubmit();
|
||||
}}
|
||||
>
|
||||
{role !== undefined ? "Save" : "Create Role"}
|
||||
</Button>
|
||||
</Stack>
|
||||
</div>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
@ -135,11 +135,16 @@ export const CreateEditRolePageView: FC<CreateEditRolePageViewProps> = ({
|
||||
/>
|
||||
</FormFields>
|
||||
{canAssignOrgRole && (
|
||||
<FormFooter
|
||||
onCancel={onCancel}
|
||||
isLoading={isLoading}
|
||||
submitLabel={role !== undefined ? "Save" : "Create Role"}
|
||||
/>
|
||||
<FormFooter>
|
||||
<Button onClick={onCancel} variant="outline">
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<Button type="submit" disabled={isLoading}>
|
||||
{isLoading && <Spinner />}
|
||||
{role ? "Save role" : "Create Role"}
|
||||
</Button>
|
||||
</FormFooter>
|
||||
)}
|
||||
</VerticalForm>
|
||||
</>
|
||||
|
@ -2,6 +2,7 @@ import TextField from "@mui/material/TextField";
|
||||
import { isApiValidationError } from "api/errors";
|
||||
import type { CreateGroupRequest } from "api/typesGenerated";
|
||||
import { ErrorAlert } from "components/Alert/ErrorAlert";
|
||||
import { Button } from "components/Button/Button";
|
||||
import {
|
||||
FormFields,
|
||||
FormFooter,
|
||||
@ -10,6 +11,7 @@ import {
|
||||
} from "components/Form/Form";
|
||||
import { IconField } from "components/IconField/IconField";
|
||||
import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
|
||||
import { Spinner } from "components/Spinner/Spinner";
|
||||
import { useFormik } from "formik";
|
||||
import type { FC } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
@ -84,7 +86,17 @@ export const CreateGroupPageView: FC<CreateGroupPageViewProps> = ({
|
||||
/>
|
||||
</FormFields>
|
||||
</FormSection>
|
||||
<FormFooter onCancel={onCancel} isLoading={isLoading} />
|
||||
|
||||
<FormFooter>
|
||||
<Button onClick={onCancel} variant="outline">
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<Button type="submit" disabled={isLoading}>
|
||||
{isLoading && <Spinner />}
|
||||
Save
|
||||
</Button>
|
||||
</FormFooter>
|
||||
</HorizontalForm>
|
||||
</>
|
||||
);
|
||||
|
@ -1,5 +1,6 @@
|
||||
import TextField from "@mui/material/TextField";
|
||||
import type { Group } from "api/typesGenerated";
|
||||
import { Button } from "components/Button/Button";
|
||||
import {
|
||||
FormFields,
|
||||
FormFooter,
|
||||
@ -9,6 +10,7 @@ import {
|
||||
import { IconField } from "components/IconField/IconField";
|
||||
import { Loader } from "components/Loader/Loader";
|
||||
import { ResourcePageHeader } from "components/PageHeader/PageHeader";
|
||||
import { Spinner } from "components/Spinner/Spinner";
|
||||
import { useFormik } from "formik";
|
||||
import type { FC } from "react";
|
||||
import {
|
||||
@ -116,7 +118,16 @@ const UpdateGroupForm: FC<UpdateGroupFormProps> = ({
|
||||
</FormFields>
|
||||
</FormSection>
|
||||
|
||||
<FormFooter onCancel={onCancel} isLoading={isLoading} />
|
||||
<FormFooter className="mt-8">
|
||||
<Button onClick={onCancel} variant="outline">
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<Button type="submit" disabled={isLoading}>
|
||||
{isLoading && <Spinner />}
|
||||
Save
|
||||
</Button>
|
||||
</FormFooter>
|
||||
</HorizontalForm>
|
||||
);
|
||||
};
|
||||
|
@ -1,12 +1,12 @@
|
||||
import DownloadOutlined from "@mui/icons-material/DownloadOutlined";
|
||||
import Button from "@mui/material/Button";
|
||||
import type {
|
||||
GroupSyncSettings,
|
||||
Organization,
|
||||
RoleSyncSettings,
|
||||
} from "api/typesGenerated";
|
||||
import { Button } from "components/Button/Button";
|
||||
import { displayError } from "components/GlobalSnackbar/utils";
|
||||
import { saveAs } from "file-saver";
|
||||
import { DownloadIcon } from "lucide-react";
|
||||
import { type FC, useState } from "react";
|
||||
|
||||
interface DownloadPolicyButtonProps {
|
||||
@ -29,7 +29,6 @@ export const ExportPolicyButton: FC<DownloadPolicyButtonProps> = ({
|
||||
|
||||
return (
|
||||
<Button
|
||||
startIcon={<DownloadOutlined />}
|
||||
disabled={!canCreatePolicyJson || isDownloading}
|
||||
onClick={async () => {
|
||||
if (canCreatePolicyJson) {
|
||||
@ -48,6 +47,7 @@ export const ExportPolicyButton: FC<DownloadPolicyButtonProps> = ({
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DownloadIcon />
|
||||
Export Policy
|
||||
</Button>
|
||||
);
|
||||
|
@ -16,6 +16,7 @@ import {
|
||||
} from "components/Form/Form";
|
||||
import { IconField } from "components/IconField/IconField";
|
||||
import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
|
||||
import { Spinner } from "components/Spinner/Spinner";
|
||||
import { useFormik } from "formik";
|
||||
import { type FC, useState } from "react";
|
||||
import {
|
||||
@ -115,7 +116,13 @@ export const OrganizationSettingsPageView: FC<
|
||||
</FormFields>
|
||||
</fieldset>
|
||||
</FormSection>
|
||||
<FormFooter isLoading={form.isSubmitting} />
|
||||
|
||||
<FormFooter>
|
||||
<Button type="submit" disabled={form.isSubmitting}>
|
||||
{form.isSubmitting && <Spinner />}
|
||||
Save
|
||||
</Button>
|
||||
</FormFooter>
|
||||
</HorizontalForm>
|
||||
|
||||
{!organization.is_default && (
|
||||
|
@ -9,6 +9,7 @@ import {
|
||||
WorkspaceAppSharingLevels,
|
||||
} from "api/typesGenerated";
|
||||
import { PremiumBadge } from "components/Badges/Badges";
|
||||
import { Button } from "components/Button/Button";
|
||||
import {
|
||||
FormFields,
|
||||
FormFooter,
|
||||
@ -16,6 +17,7 @@ import {
|
||||
HorizontalForm,
|
||||
} from "components/Form/Form";
|
||||
import { IconField } from "components/IconField/IconField";
|
||||
import { Spinner } from "components/Spinner/Spinner";
|
||||
import { Stack } from "components/Stack/Stack";
|
||||
import {
|
||||
StackLabel,
|
||||
@ -290,7 +292,16 @@ export const TemplateSettingsForm: FC<TemplateSettingsForm> = ({
|
||||
</FormFields>
|
||||
</FormSection>
|
||||
|
||||
<FormFooter onCancel={onCancel} isLoading={isSubmitting} />
|
||||
<FormFooter>
|
||||
<Button onClick={onCancel} variant="outline">
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<Button type="submit" disabled={isSubmitting}>
|
||||
{isSubmitting && <Spinner />}
|
||||
Save
|
||||
</Button>
|
||||
</FormFooter>
|
||||
</HorizontalForm>
|
||||
);
|
||||
};
|
||||
|
@ -2,7 +2,6 @@ import { screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { API, withDefaultFeatures } from "api/api";
|
||||
import type { Template, UpdateTemplateMeta } from "api/typesGenerated";
|
||||
import { Language as FooterFormLanguage } from "components/FormFooter/FormFooter";
|
||||
import { http, HttpResponse } from "msw";
|
||||
import {
|
||||
MockEntitlements,
|
||||
@ -99,9 +98,7 @@ const fillAndSubmitForm = async ({
|
||||
await userEvent.click(allowCancelJobsField);
|
||||
}
|
||||
|
||||
const submitButton = await screen.findByText(
|
||||
FooterFormLanguage.defaultSubmitLabel,
|
||||
);
|
||||
const submitButton = await screen.findByText(/save/i);
|
||||
await userEvent.click(submitButton);
|
||||
};
|
||||
|
||||
@ -174,7 +171,7 @@ describe("TemplateSettingsPage", () => {
|
||||
const deprecationMessage = "This template is deprecated";
|
||||
|
||||
await renderTemplateSettingsPage();
|
||||
await deprecateTemplate(MockTemplate, deprecationMessage);
|
||||
await deprecateTemplate(deprecationMessage);
|
||||
|
||||
const [templateId, data] = updateTemplateMetaSpy.mock.calls[0];
|
||||
|
||||
@ -198,10 +195,7 @@ describe("TemplateSettingsPage", () => {
|
||||
const updateTemplateMetaSpy = jest.spyOn(API, "updateTemplateMeta");
|
||||
|
||||
await renderTemplateSettingsPage();
|
||||
await deprecateTemplate(
|
||||
MockTemplate,
|
||||
"This template should not be able to deprecate",
|
||||
);
|
||||
await deprecateTemplate("This template should not be able to deprecate");
|
||||
|
||||
const [templateId, data] = updateTemplateMetaSpy.mock.calls[0];
|
||||
|
||||
@ -213,12 +207,9 @@ describe("TemplateSettingsPage", () => {
|
||||
});
|
||||
});
|
||||
|
||||
async function deprecateTemplate(template: Template, message: string) {
|
||||
async function deprecateTemplate(message: string) {
|
||||
const deprecationField = screen.getByLabelText("Deprecation Message");
|
||||
await userEvent.type(deprecationField, message);
|
||||
|
||||
const submitButton = await screen.findByText(
|
||||
FooterFormLanguage.defaultSubmitLabel,
|
||||
);
|
||||
const submitButton = await screen.findByRole("button", { name: /save/i });
|
||||
await userEvent.click(submitButton);
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import MenuItem from "@mui/material/MenuItem";
|
||||
import Switch from "@mui/material/Switch";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import type { Template, UpdateTemplateMeta } from "api/typesGenerated";
|
||||
import { Button } from "components/Button/Button";
|
||||
import { DurationField } from "components/DurationField/DurationField";
|
||||
import {
|
||||
FormFields,
|
||||
@ -11,6 +12,7 @@ import {
|
||||
FormSection,
|
||||
HorizontalForm,
|
||||
} from "components/Form/Form";
|
||||
import { Spinner } from "components/Spinner/Spinner";
|
||||
import { Stack } from "components/Stack/Stack";
|
||||
import {
|
||||
StackLabel,
|
||||
@ -628,11 +630,19 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
|
||||
/>
|
||||
)}
|
||||
|
||||
<FormFooter
|
||||
onCancel={onCancel}
|
||||
isLoading={isSubmitting}
|
||||
submitDisabled={!form.isValid || !form.dirty}
|
||||
/>
|
||||
<FormFooter>
|
||||
<Button onClick={onCancel} variant="outline">
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isSubmitting || !form.isValid || !form.dirty}
|
||||
>
|
||||
{isSubmitting && <Spinner />}
|
||||
Save
|
||||
</Button>
|
||||
</FormFooter>
|
||||
</HorizontalForm>
|
||||
);
|
||||
};
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { API } from "api/api";
|
||||
import { Language as FooterFormLanguage } from "components/FormFooter/FormFooter";
|
||||
import {
|
||||
MockEntitlementsWithScheduling,
|
||||
MockTemplate,
|
||||
@ -100,7 +99,7 @@ const fillAndSubmitForm = async ({
|
||||
}
|
||||
|
||||
const submitButton = screen.getByRole("button", {
|
||||
name: FooterFormLanguage.defaultSubmitLabel,
|
||||
name: /save/i,
|
||||
});
|
||||
|
||||
expect(submitButton).not.toBeDisabled();
|
||||
|
@ -4,12 +4,14 @@ import type {
|
||||
TemplateVersionVariable,
|
||||
VariableValue,
|
||||
} from "api/typesGenerated";
|
||||
import { Button } from "components/Button/Button";
|
||||
import {
|
||||
FormFields,
|
||||
FormFooter,
|
||||
FormSection,
|
||||
HorizontalForm,
|
||||
} from "components/Form/Form";
|
||||
import { Spinner } from "components/Spinner/Spinner";
|
||||
import { type FormikContextType, type FormikTouched, useFormik } from "formik";
|
||||
import type { FC } from "react";
|
||||
import { type FormHelpers, getFormHelpers } from "utils/formUtils";
|
||||
@ -106,7 +108,16 @@ export const TemplateVariablesForm: FC<TemplateVariablesForm> = ({
|
||||
);
|
||||
})}
|
||||
|
||||
<FormFooter onCancel={onCancel} isLoading={isSubmitting} />
|
||||
<FormFooter>
|
||||
<Button onClick={onCancel} variant="outline">
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<Button type="submit" disabled={isSubmitting}>
|
||||
{isSubmitting && <Spinner />}
|
||||
Save
|
||||
</Button>
|
||||
</FormFooter>
|
||||
</HorizontalForm>
|
||||
);
|
||||
};
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { API } from "api/api";
|
||||
import { Language as FooterFormLanguage } from "components/FormFooter/FormFooter";
|
||||
import {
|
||||
MockTemplate,
|
||||
MockTemplateVersion,
|
||||
@ -100,9 +99,7 @@ describe("TemplateVariablesPage", () => {
|
||||
await userEvent.type(secondVariableField, validFormValues.second_variable);
|
||||
|
||||
// Submit the form
|
||||
const submitButton = await screen.findByText(
|
||||
FooterFormLanguage.defaultSubmitLabel,
|
||||
);
|
||||
const submitButton = await screen.findByText(/save/i);
|
||||
await userEvent.click(submitButton);
|
||||
// Wait for the success message
|
||||
await delay(1500);
|
||||
|
@ -1,7 +1,6 @@
|
||||
import type { Interpolation, Theme } from "@emotion/react";
|
||||
import AddIcon from "@mui/icons-material/AddOutlined";
|
||||
import ArrowForwardOutlined from "@mui/icons-material/ArrowForwardOutlined";
|
||||
import Button from "@mui/material/Button";
|
||||
import MuiButton from "@mui/material/Button";
|
||||
import Skeleton from "@mui/material/Skeleton";
|
||||
import Table from "@mui/material/Table";
|
||||
import TableBody from "@mui/material/TableBody";
|
||||
@ -16,6 +15,7 @@ import { Avatar } from "components/Avatar/Avatar";
|
||||
import { AvatarData } from "components/Avatar/AvatarData";
|
||||
import { AvatarDataSkeleton } from "components/Avatar/AvatarDataSkeleton";
|
||||
import { DeprecatedBadge } from "components/Badges/Badges";
|
||||
import { Button } from "components/Button/Button";
|
||||
import type { useFilter } from "components/Filter/Filter";
|
||||
import {
|
||||
HelpTooltip,
|
||||
@ -38,9 +38,10 @@ import {
|
||||
TableRowSkeleton,
|
||||
} from "components/TableLoader/TableLoader";
|
||||
import { useClickableTableRow } from "hooks/useClickableTableRow";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { linkToTemplate, useLinks } from "modules/navigation";
|
||||
import type { FC } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { createDayString } from "utils/createDayString";
|
||||
import { docs } from "utils/docs";
|
||||
import {
|
||||
@ -154,7 +155,7 @@ const TemplateRow: FC<TemplateRowProps> = ({ showOrganizations, template }) => {
|
||||
{template.deprecated ? (
|
||||
<DeprecatedBadge />
|
||||
) : (
|
||||
<Button
|
||||
<MuiButton
|
||||
size="small"
|
||||
css={styles.actionButton}
|
||||
className="actionButton"
|
||||
@ -166,7 +167,7 @@ const TemplateRow: FC<TemplateRowProps> = ({ showOrganizations, template }) => {
|
||||
}}
|
||||
>
|
||||
Create Workspace
|
||||
</Button>
|
||||
</MuiButton>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@ -195,14 +196,11 @@ export const TemplatesPageView: FC<TemplatesPageViewProps> = ({
|
||||
const navigate = useNavigate();
|
||||
|
||||
const createTemplateAction = showOrganizations ? (
|
||||
<Button
|
||||
startIcon={<AddIcon />}
|
||||
variant="contained"
|
||||
onClick={() => {
|
||||
navigate("/starter-templates");
|
||||
}}
|
||||
>
|
||||
Create Template
|
||||
<Button asChild size="lg">
|
||||
<Link to="/starter-templates">
|
||||
<PlusIcon />
|
||||
New template
|
||||
</Link>
|
||||
</Button>
|
||||
) : (
|
||||
<CreateTemplateButton onNavigate={navigate} />
|
||||
|
@ -100,14 +100,8 @@ export const UpdateBuildParametersDialog: FC<
|
||||
<Button fullWidth type="button" onClick={dialogProps.onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
color="primary"
|
||||
fullWidth
|
||||
type="submit"
|
||||
form="updateParameters"
|
||||
data-testid="form-submit"
|
||||
>
|
||||
Update
|
||||
<Button color="primary" fullWidth type="submit" form="updateParameters">
|
||||
Update parameters
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
@ -312,7 +312,9 @@ describe("WorkspacePage", () => {
|
||||
);
|
||||
await user.clear(secondParameterInput);
|
||||
await user.type(secondParameterInput, "2");
|
||||
await user.click(within(dialog).getByRole("button", { name: "Update" }));
|
||||
await user.click(
|
||||
within(dialog).getByRole("button", { name: /update parameters/i }),
|
||||
);
|
||||
|
||||
// Check if the update was called using the values from the form
|
||||
await waitFor(() => {
|
||||
|
@ -4,6 +4,7 @@ import type {
|
||||
WorkspaceBuildParameter,
|
||||
} from "api/typesGenerated";
|
||||
import { Alert } from "components/Alert/Alert";
|
||||
import { Button } from "components/Button/Button";
|
||||
import {
|
||||
FormFields,
|
||||
FormFooter,
|
||||
@ -11,6 +12,7 @@ import {
|
||||
HorizontalForm,
|
||||
} from "components/Form/Form";
|
||||
import { RichParameterInput } from "components/RichParameterInput/RichParameterInput";
|
||||
import { Spinner } from "components/Spinner/Spinner";
|
||||
import { useFormik } from "formik";
|
||||
import type { FC } from "react";
|
||||
import { getFormHelpers } from "utils/formUtils";
|
||||
@ -154,12 +156,19 @@ export const WorkspaceParametersForm: FC<WorkspaceParameterFormProps> = ({
|
||||
</FormSection>
|
||||
)}
|
||||
|
||||
<FormFooter
|
||||
onCancel={onCancel}
|
||||
isLoading={isSubmitting}
|
||||
submitLabel="Submit and restart"
|
||||
submitDisabled={disabled || !form.dirty}
|
||||
/>
|
||||
<FormFooter>
|
||||
<Button onClick={onCancel} variant="outline">
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isSubmitting || disabled || !form.dirty}
|
||||
>
|
||||
{isSubmitting && <Spinner />}
|
||||
Submit and restart
|
||||
</Button>
|
||||
</FormFooter>
|
||||
</HorizontalForm>
|
||||
</>
|
||||
);
|
||||
|
@ -416,7 +416,9 @@ test("form should be enabled when both auto stop and auto start features are dis
|
||||
/>,
|
||||
);
|
||||
|
||||
const submitButton = await screen.findByRole("button", { name: "Submit" });
|
||||
const submitButton = await screen.findByRole("button", {
|
||||
name: /save/i,
|
||||
});
|
||||
expect(submitButton).toBeEnabled();
|
||||
});
|
||||
|
||||
@ -432,6 +434,8 @@ test("form should be disabled when both auto stop and auto start features are di
|
||||
jest.spyOn(API, "getTemplateByName").mockResolvedValue(MockTemplate);
|
||||
render(<WorkspaceScheduleForm {...props} />);
|
||||
|
||||
const submitButton = await screen.findByRole("button", { name: "Submit" });
|
||||
const submitButton = await screen.findByRole("button", {
|
||||
name: /save/i,
|
||||
});
|
||||
expect(submitButton).toBeDisabled();
|
||||
});
|
||||
|
@ -8,12 +8,14 @@ import MenuItem from "@mui/material/MenuItem";
|
||||
import Switch from "@mui/material/Switch";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import type { Template } from "api/typesGenerated";
|
||||
import { Button } from "components/Button/Button";
|
||||
import {
|
||||
FormFields,
|
||||
FormFooter,
|
||||
FormSection,
|
||||
HorizontalForm,
|
||||
} from "components/Form/Form";
|
||||
import { Spinner } from "components/Spinner/Spinner";
|
||||
import { Stack } from "components/Stack/Stack";
|
||||
import {
|
||||
StackLabel,
|
||||
@ -441,15 +443,23 @@ export const WorkspaceScheduleForm: FC<WorkspaceScheduleFormProps> = ({
|
||||
/>
|
||||
</FormFields>
|
||||
</FormSection>
|
||||
<FormFooter
|
||||
onCancel={onCancel}
|
||||
isLoading={isLoading}
|
||||
// If both options, autostart and autostop, are disabled at the template
|
||||
// level, the form is disabled.
|
||||
submitDisabled={
|
||||
!template.allow_user_autostart && !template.allow_user_autostop
|
||||
|
||||
<FormFooter>
|
||||
<Button onClick={onCancel} variant="outline">
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={
|
||||
isLoading ||
|
||||
(!template.allow_user_autostart && !template.allow_user_autostop)
|
||||
}
|
||||
/>
|
||||
>
|
||||
{isLoading && <Spinner />}
|
||||
Save
|
||||
</Button>
|
||||
</FormFooter>
|
||||
</HorizontalForm>
|
||||
);
|
||||
};
|
||||
|
@ -288,7 +288,7 @@ describe("WorkspaceSchedulePage", () => {
|
||||
);
|
||||
await user.click(autostopToggle);
|
||||
const submitButton = await screen.findByRole("button", {
|
||||
name: /submit/i,
|
||||
name: /save/i,
|
||||
});
|
||||
await user.click(submitButton);
|
||||
const dialog = await screen.findByText("Restart workspace?");
|
||||
@ -309,7 +309,7 @@ describe("WorkspaceSchedulePage", () => {
|
||||
);
|
||||
await user.click(autostartToggle);
|
||||
const submitButton = await screen.findByRole("button", {
|
||||
name: /submit/i,
|
||||
name: /save/i,
|
||||
});
|
||||
await user.click(submitButton);
|
||||
const dialog = screen.queryByText("Restart workspace?");
|
||||
|
@ -6,12 +6,14 @@ import {
|
||||
AutomaticUpdateses,
|
||||
type Workspace,
|
||||
} from "api/typesGenerated";
|
||||
import { Button } from "components/Button/Button";
|
||||
import {
|
||||
FormFields,
|
||||
FormFooter,
|
||||
FormSection,
|
||||
HorizontalForm,
|
||||
} from "components/Form/Form";
|
||||
import { Spinner } from "components/Spinner/Spinner";
|
||||
import { useFormik } from "formik";
|
||||
import upperFirst from "lodash/upperFirst";
|
||||
import type { FC } from "react";
|
||||
@ -115,7 +117,16 @@ export const WorkspaceSettingsForm: FC<WorkspaceSettingsFormProps> = ({
|
||||
</FormFields>
|
||||
</FormSection>
|
||||
{formEnabled && (
|
||||
<FormFooter onCancel={onCancel} isLoading={form.isSubmitting} />
|
||||
<FormFooter>
|
||||
<Button onClick={onCancel} variant="outline">
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<Button type="submit" disabled={form.isSubmitting}>
|
||||
{form.isSubmitting && <Spinner />}
|
||||
Save
|
||||
</Button>
|
||||
</FormFooter>
|
||||
)}
|
||||
</HorizontalForm>
|
||||
);
|
||||
|
@ -31,7 +31,7 @@ test("Submit the workspace settings page successfully", async () => {
|
||||
const name = within(form).getByLabelText("Name");
|
||||
await user.clear(name);
|
||||
await user.type(within(form).getByLabelText("Name"), "new-name");
|
||||
await user.click(within(form).getByRole("button", { name: "Submit" }));
|
||||
await user.click(within(form).getByRole("button", { name: /save/i }));
|
||||
// Assert that the API calls were made with the correct data
|
||||
await waitFor(() => {
|
||||
expect(patchWorkspaceSpy).toHaveBeenCalledWith(MockWorkspace.id, {
|
||||
|
@ -1,9 +1,8 @@
|
||||
import AddIcon from "@mui/icons-material/AddOutlined";
|
||||
import OpenIcon from "@mui/icons-material/OpenInNewOutlined";
|
||||
import Button from "@mui/material/Button";
|
||||
import Link from "@mui/material/Link";
|
||||
import type { Template } from "api/typesGenerated";
|
||||
import { Avatar } from "components/Avatar/Avatar";
|
||||
import { Button } from "components/Button/Button";
|
||||
import { Loader } from "components/Loader/Loader";
|
||||
import { MenuSearch } from "components/Menu/MenuSearch";
|
||||
import { OverflowY } from "components/OverflowY/OverflowY";
|
||||
@ -13,6 +12,7 @@ import {
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "components/deprecated/Popover/Popover";
|
||||
import { ChevronDownIcon } from "lucide-react";
|
||||
import { linkToTemplate, useLinks } from "modules/navigation";
|
||||
import { type FC, type ReactNode, useState } from "react";
|
||||
import type { UseQueryResult } from "react-query";
|
||||
@ -56,8 +56,9 @@ export const WorkspacesButton: FC<WorkspacesButtonProps> = ({
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger>
|
||||
<Button startIcon={<AddIcon />} variant="contained">
|
||||
<Button size="lg">
|
||||
{children}
|
||||
<ChevronDownIcon />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
|
@ -38,7 +38,6 @@ export const Language = {
|
||||
yourWorkspacesButton: "Your workspaces",
|
||||
allWorkspacesButton: "All workspaces",
|
||||
runningWorkspacesButton: "Running workspaces",
|
||||
createWorkspace: <>Create Workspace…</>,
|
||||
seeAllTemplates: "See all templates",
|
||||
template: "Template",
|
||||
};
|
||||
@ -103,7 +102,7 @@ export const WorkspacesPageView: FC<WorkspacesPageViewProps> = ({
|
||||
templates={templates}
|
||||
templatesFetchStatus={templatesFetchStatus}
|
||||
>
|
||||
{Language.createWorkspace}
|
||||
New workspace
|
||||
</WorkspacesButton>
|
||||
}
|
||||
>
|
||||
|
@ -42,11 +42,11 @@ module.exports = {
|
||||
primary: "hsl(var(--surface-invert-primary))",
|
||||
secondary: "hsl(var(--surface-invert-secondary))",
|
||||
},
|
||||
error: "hsl(var(--surface-error))",
|
||||
destructive: "hsl(var(--surface-destructive))",
|
||||
},
|
||||
border: {
|
||||
DEFAULT: "hsl(var(--border-default))",
|
||||
error: "hsl(var(--border-error))",
|
||||
destructive: "hsl(var(--border-destructive))",
|
||||
},
|
||||
input: "hsl(var(--input))",
|
||||
ring: "hsl(var(--ring))",
|
||||
|
Reference in New Issue
Block a user