mirror of
https://github.com/coder/coder.git
synced 2026-09-21 20:51:01 +08:00
feat: add default workspace name to Template Embed form (#19688)
Fixes: https://github.com/coder/coder/issues/15798
This commit is contained in:
@@ -30,6 +30,10 @@ test("Users can fill the parameters and copy the open in coder url", async () =>
|
||||
await waitForLoaderToBeRemoved();
|
||||
|
||||
const user = userEvent.setup();
|
||||
const workspaceName = screen.getByRole("textbox", {
|
||||
name: "Workspace name",
|
||||
});
|
||||
await user.type(workspaceName, "my-first-workspace");
|
||||
const firstParameterField = screen.getByLabelText(
|
||||
parameter1.display_name ?? parameter1.name,
|
||||
{ exact: false },
|
||||
@@ -47,6 +51,6 @@ test("Users can fill the parameters and copy the open in coder url", async () =>
|
||||
const copyButton = screen.getByRole("button", { name: /copy/i });
|
||||
await userEvent.click(copyButton);
|
||||
expect(window.navigator.clipboard.writeText).toBeCalledWith(
|
||||
`[](http://localhost/templates/${MockTemplate.organization_name}/${MockTemplate.name}/workspace?mode=manual¶m.first_parameter=firstParameterValue¶m.second_parameter=123456)`,
|
||||
`[](http://localhost/templates/${MockTemplate.organization_name}/${MockTemplate.name}/workspace?mode=manual&name=my-first-workspace¶m.first_parameter=firstParameterValue¶m.second_parameter=123456)`,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -5,17 +5,22 @@ import RadioGroup from "@mui/material/RadioGroup";
|
||||
import { API } from "api/api";
|
||||
import type { Template, TemplateVersionParameter } from "api/typesGenerated";
|
||||
import { FormSection, VerticalForm } from "components/Form/Form";
|
||||
import { Input } from "components/Input/Input";
|
||||
import { Label } from "components/Label/Label";
|
||||
import { Loader } from "components/Loader/Loader";
|
||||
import { RichParameterInput } from "components/RichParameterInput/RichParameterInput";
|
||||
import { useDebouncedFunction } from "hooks/debounce";
|
||||
import { useClipboard } from "hooks/useClipboard";
|
||||
import { CheckIcon, CopyIcon } from "lucide-react";
|
||||
import { useTemplateLayoutContext } from "pages/TemplatePage/TemplateLayout";
|
||||
import { type FC, useEffect, useState } from "react";
|
||||
import { type FC, useEffect, useId, useState } from "react";
|
||||
import { Helmet } from "react-helmet-async";
|
||||
import { useQuery } from "react-query";
|
||||
import { nameValidator } from "utils/formUtils";
|
||||
import { pageTitle } from "utils/page";
|
||||
import { getInitialRichParameterValues } from "utils/richParameters";
|
||||
import { paramsUsedToCreateWorkspace } from "utils/workspace";
|
||||
import { ValidationError } from "yup";
|
||||
|
||||
type ButtonValues = Record<string, string>;
|
||||
|
||||
@@ -47,19 +52,25 @@ interface TemplateEmbedPageViewProps {
|
||||
templateParameters?: TemplateVersionParameter[];
|
||||
}
|
||||
|
||||
const deploymentUrl = `${window.location.protocol}//${window.location.host}`;
|
||||
|
||||
function getClipboardCopyContent(
|
||||
templateName: string,
|
||||
organization: string,
|
||||
buttonValues: ButtonValues | undefined,
|
||||
): string {
|
||||
const deploymentUrl = `${window.location.protocol}//${window.location.host}`;
|
||||
const createWorkspaceUrl = `${deploymentUrl}/templates/${organization}/${templateName}/workspace`;
|
||||
const createWorkspaceParams = new URLSearchParams(buttonValues);
|
||||
if (createWorkspaceParams.get("name") === "") {
|
||||
createWorkspaceParams.delete("name"); // no default workspace name if empty
|
||||
}
|
||||
const buttonUrl = `${createWorkspaceUrl}?${createWorkspaceParams.toString()}`;
|
||||
|
||||
return `[](${buttonUrl})`;
|
||||
}
|
||||
|
||||
const workspaceNameValidator = nameValidator("Workspace name");
|
||||
|
||||
export const TemplateEmbedPageView: FC<TemplateEmbedPageViewProps> = ({
|
||||
template,
|
||||
templateParameters,
|
||||
@@ -79,6 +90,7 @@ export const TemplateEmbedPageView: FC<TemplateEmbedPageViewProps> = ({
|
||||
if (templateParameters && !buttonValues) {
|
||||
const buttonValues: ButtonValues = {
|
||||
mode: "manual",
|
||||
name: "",
|
||||
};
|
||||
for (const parameter of getInitialRichParameterValues(
|
||||
templateParameters,
|
||||
@@ -89,6 +101,27 @@ export const TemplateEmbedPageView: FC<TemplateEmbedPageViewProps> = ({
|
||||
}
|
||||
}, [buttonValues, templateParameters]);
|
||||
|
||||
const [workspaceNameError, setWorkspaceNameError] = useState("");
|
||||
const validateWorkspaceName = (workspaceName: string) => {
|
||||
try {
|
||||
if (workspaceName) {
|
||||
workspaceNameValidator.validateSync(workspaceName);
|
||||
}
|
||||
setWorkspaceNameError("");
|
||||
} catch (e) {
|
||||
if (e instanceof ValidationError) {
|
||||
setWorkspaceNameError(e.message);
|
||||
}
|
||||
}
|
||||
};
|
||||
const { debounced: debouncedValidateWorkspaceName } = useDebouncedFunction(
|
||||
validateWorkspaceName,
|
||||
500,
|
||||
);
|
||||
|
||||
const hookId = useId();
|
||||
const defaultWorkspaceNameID = `${hookId}-default-workspace-name`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
@@ -126,6 +159,29 @@ export const TemplateEmbedPageView: FC<TemplateEmbedPageViewProps> = ({
|
||||
</RadioGroup>
|
||||
</FormSection>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<Label className="text-md" htmlFor={defaultWorkspaceNameID}>
|
||||
Workspace name
|
||||
</Label>
|
||||
<div className="text-sm text-content-secondary pb-3">
|
||||
Default name for the new workspace
|
||||
</div>
|
||||
<Input
|
||||
id={defaultWorkspaceNameID}
|
||||
value={buttonValues.name}
|
||||
onChange={(event) => {
|
||||
debouncedValidateWorkspaceName(event.target.value);
|
||||
setButtonValues((buttonValues) => ({
|
||||
...buttonValues,
|
||||
name: event.target.value,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
<div className="text-sm text-highlight-red mt-1" role="alert">
|
||||
{workspaceNameError}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{templateParameters.length > 0 && (
|
||||
<div
|
||||
css={{ display: "flex", flexDirection: "column", gap: 36 }}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
MockTemplateVersionParameter4,
|
||||
} from "testHelpers/entities";
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { screen, userEvent } from "storybook/test";
|
||||
import { TemplateEmbedPageView } from "./TemplateEmbedPage";
|
||||
|
||||
const meta: Meta<typeof TemplateEmbedPageView> = {
|
||||
@@ -35,3 +36,15 @@ export const WithParameters: Story = {
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const WrongWorkspaceName: Story = {
|
||||
args: {
|
||||
templateParameters: [MockTemplateVersionParameter1],
|
||||
},
|
||||
play: async () => {
|
||||
const workspaceName = await screen.findByRole("textbox", {
|
||||
name: "Workspace name",
|
||||
});
|
||||
await userEvent.type(workspaceName, "b@d");
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user