Skip to content

Commit 5c1a708

Browse files
authored
feat: add default workspace name to Template Embed form (#19688)
Fixes: #15798
1 parent 0b460b8 commit 5c1a708

File tree

3 files changed

+76
-3
lines changed

3 files changed

+76
-3
lines changed

site/src/pages/TemplatePage/TemplateEmbedPage/TemplateEmbedPage.test.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ test("Users can fill the parameters and copy the open in coder url", async () =>
3030
await waitForLoaderToBeRemoved();
3131

3232
const user = userEvent.setup();
33+
const workspaceName = screen.getByRole("textbox", {
34+
name: "Workspace name",
35+
});
36+
await user.type(workspaceName, "my-first-workspace");
3337
const firstParameterField = screen.getByLabelText(
3438
parameter1.display_name ?? parameter1.name,
3539
{ exact: false },
@@ -47,6 +51,6 @@ test("Users can fill the parameters and copy the open in coder url", async () =>
4751
const copyButton = screen.getByRole("button", { name: /copy/i });
4852
await userEvent.click(copyButton);
4953
expect(window.navigator.clipboard.writeText).toBeCalledWith(
50-
`[![Open in Coder](http://localhost/open-in-coder.svg)](http://localhost/templates/${MockTemplate.organization_name}/${MockTemplate.name}/workspace?mode=manual&param.first_parameter=firstParameterValue&param.second_parameter=123456)`,
54+
`[![Open in Coder](http://localhost/open-in-coder.svg)](http://localhost/templates/${MockTemplate.organization_name}/${MockTemplate.name}/workspace?mode=manual&name=my-first-workspace&param.first_parameter=firstParameterValue&param.second_parameter=123456)`,
5155
);
5256
});

site/src/pages/TemplatePage/TemplateEmbedPage/TemplateEmbedPage.tsx

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,22 @@ import RadioGroup from "@mui/material/RadioGroup";
55
import { API } from "api/api";
66
import type { Template, TemplateVersionParameter } from "api/typesGenerated";
77
import { FormSection, VerticalForm } from "components/Form/Form";
8+
import { Input } from "components/Input/Input";
9+
import { Label } from "components/Label/Label";
810
import { Loader } from "components/Loader/Loader";
911
import { RichParameterInput } from "components/RichParameterInput/RichParameterInput";
12+
import { useDebouncedFunction } from "hooks/debounce";
1013
import { useClipboard } from "hooks/useClipboard";
1114
import { CheckIcon, CopyIcon } from "lucide-react";
1215
import { useTemplateLayoutContext } from "pages/TemplatePage/TemplateLayout";
13-
import { type FC, useEffect, useState } from "react";
16+
import { type FC, useEffect, useId, useState } from "react";
1417
import { Helmet } from "react-helmet-async";
1518
import { useQuery } from "react-query";
19+
import { nameValidator } from "utils/formUtils";
1620
import { pageTitle } from "utils/page";
1721
import { getInitialRichParameterValues } from "utils/richParameters";
1822
import { paramsUsedToCreateWorkspace } from "utils/workspace";
23+
import { ValidationError } from "yup";
1924

2025
type ButtonValues = Record<string, string>;
2126

@@ -47,19 +52,25 @@ interface TemplateEmbedPageViewProps {
4752
templateParameters?: TemplateVersionParameter[];
4853
}
4954

55+
const deploymentUrl = `${window.location.protocol}//${window.location.host}`;
56+
5057
function getClipboardCopyContent(
5158
templateName: string,
5259
organization: string,
5360
buttonValues: ButtonValues | undefined,
5461
): string {
55-
const deploymentUrl = `${window.location.protocol}//${window.location.host}`;
5662
const createWorkspaceUrl = `${deploymentUrl}/templates/${organization}/${templateName}/workspace`;
5763
const createWorkspaceParams = new URLSearchParams(buttonValues);
64+
if (createWorkspaceParams.get("name") === "") {
65+
createWorkspaceParams.delete("name"); // no default workspace name if empty
66+
}
5867
const buttonUrl = `${createWorkspaceUrl}?${createWorkspaceParams.toString()}`;
5968

6069
return `[![Open in Coder](${deploymentUrl}/open-in-coder.svg)](${buttonUrl})`;
6170
}
6271

72+
const workspaceNameValidator = nameValidator("Workspace name");
73+
6374
export const TemplateEmbedPageView: FC<TemplateEmbedPageViewProps> = ({
6475
template,
6576
templateParameters,
@@ -79,6 +90,7 @@ export const TemplateEmbedPageView: FC<TemplateEmbedPageViewProps> = ({
7990
if (templateParameters && !buttonValues) {
8091
const buttonValues: ButtonValues = {
8192
mode: "manual",
93+
name: "",
8294
};
8395
for (const parameter of getInitialRichParameterValues(
8496
templateParameters,
@@ -89,6 +101,27 @@ export const TemplateEmbedPageView: FC<TemplateEmbedPageViewProps> = ({
89101
}
90102
}, [buttonValues, templateParameters]);
91103

104+
const [workspaceNameError, setWorkspaceNameError] = useState("");
105+
const validateWorkspaceName = (workspaceName: string) => {
106+
try {
107+
if (workspaceName) {
108+
workspaceNameValidator.validateSync(workspaceName);
109+
}
110+
setWorkspaceNameError("");
111+
} catch (e) {
112+
if (e instanceof ValidationError) {
113+
setWorkspaceNameError(e.message);
114+
}
115+
}
116+
};
117+
const { debounced: debouncedValidateWorkspaceName } = useDebouncedFunction(
118+
validateWorkspaceName,
119+
500,
120+
);
121+
122+
const hookId = useId();
123+
const defaultWorkspaceNameID = `${hookId}-default-workspace-name`;
124+
92125
return (
93126
<>
94127
<Helmet>
@@ -126,6 +159,29 @@ export const TemplateEmbedPageView: FC<TemplateEmbedPageViewProps> = ({
126159
</RadioGroup>
127160
</FormSection>
128161

162+
<div className="flex flex-col gap-1">
163+
<Label className="text-md" htmlFor={defaultWorkspaceNameID}>
164+
Workspace name
165+
</Label>
166+
<div className="text-sm text-content-secondary pb-3">
167+
Default name for the new workspace
168+
</div>
169+
<Input
170+
id={defaultWorkspaceNameID}
171+
value={buttonValues.name}
172+
onChange={(event) => {
173+
debouncedValidateWorkspaceName(event.target.value);
174+
setButtonValues((buttonValues) => ({
175+
...buttonValues,
176+
name: event.target.value,
177+
}));
178+
}}
179+
/>
180+
<div className="text-sm text-highlight-red mt-1" role="alert">
181+
{workspaceNameError}
182+
</div>
183+
</div>
184+
129185
{templateParameters.length > 0 && (
130186
<div
131187
css={{ display: "flex", flexDirection: "column", gap: 36 }}

site/src/pages/TemplatePage/TemplateEmbedPage/TemplateEmbedPageView.stories.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
MockTemplateVersionParameter4,
77
} from "testHelpers/entities";
88
import type { Meta, StoryObj } from "@storybook/react-vite";
9+
import { screen, userEvent } from "storybook/test";
910
import { TemplateEmbedPageView } from "./TemplateEmbedPage";
1011

1112
const meta: Meta<typeof TemplateEmbedPageView> = {
@@ -35,3 +36,15 @@ export const WithParameters: Story = {
3536
],
3637
},
3738
};
39+
40+
export const WrongWorkspaceName: Story = {
41+
args: {
42+
templateParameters: [MockTemplateVersionParameter1],
43+
},
44+
play: async () => {
45+
const workspaceName = await screen.findByRole("textbox", {
46+
name: "Workspace name",
47+
});
48+
await userEvent.type(workspaceName, "b@d");
49+
},
50+
};

0 commit comments

Comments
 (0)