Skip to content

Commit 7cf686c

Browse files
authored
feat: Add Create Workspace Form (#73)
Fixes #38 This adds a create-workspace form (with only 1 field, probably the simplest form ever 😄 ) ![image](https://user-images.githubusercontent.com/88213859/151108220-8a540c75-e55b-49af-8199-c69394508700.png) It currently redirects to a path `/workspaces/<unique id>`, but that isn't implemented yet - but you can see the workspace show up on the projects page.
1 parent b586a35 commit 7cf686c

File tree

4 files changed

+203
-0
lines changed

4 files changed

+203
-0
lines changed

site/api.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,11 @@ export namespace Project {
6767
}
6868
}
6969

70+
export interface CreateWorkspaceRequest {
71+
name: string
72+
project_id: string
73+
}
74+
7075
// Must be kept in sync with backend Workspace struct
7176
export interface Workspace {
7277
id: string
@@ -77,6 +82,31 @@ export interface Workspace {
7782
name: string
7883
}
7984

85+
export namespace Workspace {
86+
export const create = async (request: CreateWorkspaceRequest): Promise<Workspace> => {
87+
const response = await fetch(`/api/v2/workspaces/me`, {
88+
method: "POST",
89+
headers: {
90+
"Content-Type": "application/json",
91+
},
92+
body: JSON.stringify(request),
93+
})
94+
95+
const body = await response.json()
96+
if (!response.ok) {
97+
throw new Error(body.message)
98+
}
99+
100+
// Let SWR know that both the /api/v2/workspaces/* and /api/v2/projects/*
101+
// endpoints will need to fetch new data.
102+
const mutateWorkspacesPromise = mutate("/api/v2/workspaces")
103+
const mutateProjectsPromise = mutate("/api/v2/projects")
104+
await Promise.all([mutateWorkspacesPromise, mutateProjectsPromise])
105+
106+
return body
107+
}
108+
}
109+
80110
export const login = async (email: string, password: string): Promise<LoginResponse> => {
81111
const response = await fetch("/api/v2/login", {
82112
method: "POST",
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { render, screen } from "@testing-library/react"
2+
import React from "react"
3+
import { CreateWorkspaceForm } from "./CreateWorkspaceForm"
4+
import { MockProject, MockWorkspace } from "./../test_helpers"
5+
6+
describe("CreateWorkspaceForm", () => {
7+
it("renders", async () => {
8+
// Given
9+
const onSubmit = () => Promise.resolve(MockWorkspace)
10+
const onCancel = () => Promise.resolve()
11+
12+
// When
13+
render(<CreateWorkspaceForm project={MockProject} onSubmit={onSubmit} onCancel={onCancel} />)
14+
15+
// Then
16+
// Simple smoke test to verify form renders
17+
const element = await screen.findByText("Create Workspace")
18+
expect(element).toBeDefined()
19+
})
20+
})

site/forms/CreateWorkspaceForm.tsx

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import Button from "@material-ui/core/Button"
2+
import { makeStyles } from "@material-ui/core/styles"
3+
import { FormikContextType, useFormik } from "formik"
4+
import React from "react"
5+
import * as Yup from "yup"
6+
7+
import { FormTextField, FormTitle, FormSection } from "../components/Form"
8+
import { LoadingButton } from "../components/Button"
9+
import { Project, Workspace, CreateWorkspaceRequest } from "../api"
10+
11+
export interface CreateWorkspaceForm {
12+
project: Project
13+
onSubmit: (request: CreateWorkspaceRequest) => Promise<Workspace>
14+
onCancel: () => void
15+
}
16+
17+
const validationSchema = Yup.object({
18+
name: Yup.string().required("Name is required"),
19+
})
20+
21+
export const CreateWorkspaceForm: React.FC<CreateWorkspaceForm> = ({ project, onSubmit, onCancel }) => {
22+
const styles = useStyles()
23+
24+
const form: FormikContextType<{ name: string }> = useFormik<{ name: string }>({
25+
initialValues: {
26+
name: "",
27+
},
28+
enableReinitialize: true,
29+
validationSchema: validationSchema,
30+
onSubmit: ({ name }) => {
31+
return onSubmit({
32+
project_id: project.id,
33+
name: name,
34+
})
35+
},
36+
})
37+
38+
return (
39+
<div className={styles.root}>
40+
<FormTitle
41+
title="Create Workspace"
42+
detail={
43+
<span>
44+
for project <strong>{project.name}</strong>
45+
</span>
46+
}
47+
/>
48+
<FormSection title="Name">
49+
<FormTextField
50+
form={form}
51+
formFieldName="name"
52+
fullWidth
53+
helperText="A unique name describing your workspace."
54+
label="Workspace Name"
55+
placeholder="my-workspace"
56+
required
57+
/>
58+
</FormSection>
59+
60+
<div className={styles.footer}>
61+
<Button className={styles.button} onClick={onCancel} variant="outlined">
62+
Cancel
63+
</Button>
64+
<LoadingButton
65+
loading={form.isSubmitting}
66+
className={styles.button}
67+
onClick={form.submitForm}
68+
variant="contained"
69+
color="primary"
70+
type="submit"
71+
>
72+
Submit
73+
</LoadingButton>
74+
</div>
75+
</div>
76+
)
77+
}
78+
79+
const useStyles = makeStyles(() => ({
80+
root: {
81+
maxWidth: "1380px",
82+
width: "100%",
83+
display: "flex",
84+
flexDirection: "column",
85+
alignItems: "center",
86+
},
87+
footer: {
88+
display: "flex",
89+
flex: "0",
90+
flexDirection: "row",
91+
justifyContent: "center",
92+
alignItems: "center",
93+
},
94+
button: {
95+
margin: "1em",
96+
},
97+
}))
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import React from "react"
2+
import { makeStyles } from "@material-ui/core/styles"
3+
import { useRouter } from "next/router"
4+
import useSWR from "swr"
5+
6+
import * as API from "../../../../api"
7+
import { useUser } from "../../../../contexts/UserContext"
8+
import { ErrorSummary } from "../../../../components/ErrorSummary"
9+
import { FullScreenLoader } from "../../../../components/Loader/FullScreenLoader"
10+
import { CreateWorkspaceForm } from "../../../../forms/CreateWorkspaceForm"
11+
12+
const CreateWorkspacePage: React.FC = () => {
13+
const router = useRouter()
14+
const styles = useStyles()
15+
const { me } = useUser(/* redirectOnError */ true)
16+
const { organization, project: projectName } = router.query
17+
const { data: project, error: projectError } = useSWR<API.Project, Error>(
18+
`/api/v2/projects/${organization}/${projectName}`,
19+
)
20+
21+
if (projectError) {
22+
return <ErrorSummary error={projectError} />
23+
}
24+
25+
if (!me || !project) {
26+
return <FullScreenLoader />
27+
}
28+
29+
const onCancel = async () => {
30+
await router.push(`/projects/${organization}/${project}`)
31+
}
32+
33+
const onSubmit = async (req: API.CreateWorkspaceRequest) => {
34+
const workspace = await API.Workspace.create(req)
35+
await router.push(`/workspaces/${workspace.id}`)
36+
return workspace
37+
}
38+
39+
return (
40+
<div className={styles.root}>
41+
<CreateWorkspaceForm onCancel={onCancel} onSubmit={onSubmit} project={project} />
42+
</div>
43+
)
44+
}
45+
46+
const useStyles = makeStyles((theme) => ({
47+
root: {
48+
display: "flex",
49+
flexDirection: "column",
50+
alignItems: "center",
51+
height: "100vh",
52+
backgroundColor: theme.palette.background.paper,
53+
},
54+
}))
55+
56+
export default CreateWorkspacePage

0 commit comments

Comments
 (0)