Skip to content

feat: Add Create Workspace Form #73

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Jan 31, 2022
30 changes: 30 additions & 0 deletions site/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ export namespace Project {
}
}

export interface CreateWorkspaceRequest {
name: string
project_id: string
}

// Must be kept in sync with backend Workspace struct
export interface Workspace {
id: string
Expand All @@ -77,6 +82,31 @@ export interface Workspace {
name: string
}

export namespace Workspace {
export const create = async (request: CreateWorkspaceRequest): Promise<Workspace> => {
const response = await fetch(`/api/v2/workspaces/me`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(request),
})

const body = await response.json()
if (!response.ok) {
throw new Error(body.message)
}

// Let SWR know that both the /api/v2/workspaces/* and /api/v2/projects/*
// endpoints will need to fetch new data.
const mutateWorkspacesPromise = mutate("/api/v2/workspaces")
const mutateProjectsPromise = mutate("/api/v2/projects")
await Promise.all([mutateWorkspacesPromise, mutateProjectsPromise])

return body
}
}

export const login = async (email: string, password: string): Promise<LoginResponse> => {
const response = await fetch("/api/v2/login", {
method: "POST",
Expand Down
20 changes: 20 additions & 0 deletions site/forms/CreateWorkspaceForm.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { render, screen } from "@testing-library/react"
import React from "react"
import { CreateWorkspaceForm } from "./CreateWorkspaceForm"
import { MockProject, MockWorkspace } from "./../test_helpers"

describe("CreateWorkspaceForm", () => {
it("renders", async () => {
// Given
const onSubmit = () => Promise.resolve(MockWorkspace)
const onCancel = () => Promise.resolve()

// When
render(<CreateWorkspaceForm project={MockProject} onSubmit={onSubmit} onCancel={onCancel} />)

// Then
// Simple smoke test to verify form renders
const element = await screen.findByText("Create Workspace")
expect(element).toBeDefined()
})
})
97 changes: 97 additions & 0 deletions site/forms/CreateWorkspaceForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import Button from "@material-ui/core/Button"
import { makeStyles } from "@material-ui/core/styles"
import { FormikContextType, useFormik } from "formik"
import React from "react"
import * as Yup from "yup"

import { FormTextField, FormTitle, FormSection } from "../components/Form"
import { LoadingButton } from "../components/Button"
import { Project, Workspace, CreateWorkspaceRequest } from "../api"

export interface CreateWorkspaceForm {
project: Project
onSubmit: (request: CreateWorkspaceRequest) => Promise<Workspace>
onCancel: () => void
}

const validationSchema = Yup.object({
name: Yup.string().required("Name is required"),
})

export const CreateWorkspaceForm: React.FC<CreateWorkspaceForm> = ({ project, onSubmit, onCancel }) => {
const styles = useStyles()

const form: FormikContextType<{ name: string }> = useFormik<{ name: string }>({
initialValues: {
name: "",
},
enableReinitialize: true,
validationSchema: validationSchema,
onSubmit: ({ name }) => {
return onSubmit({
project_id: project.id,
name: name,
})
},
})

return (
<div className={styles.root}>
<FormTitle
title="Create Workspace"
detail={
<span>
for project <strong>{project.name}</strong>
</span>
}
/>
<FormSection title="Name">
<FormTextField
form={form}
formFieldName="name"
fullWidth
helperText="A unique name describing your workspace."
label="Workspace Name"
placeholder="my-workspace"
required
/>
</FormSection>

<div className={styles.footer}>
<Button className={styles.button} onClick={onCancel} variant="outlined">
Cancel
</Button>
<LoadingButton
loading={form.isSubmitting}
className={styles.button}
onClick={form.submitForm}
variant="contained"
color="primary"
type="submit"
>
Submit
</LoadingButton>
</div>
</div>
)
}

const useStyles = makeStyles(() => ({
root: {
maxWidth: "1380px",
width: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
},
footer: {
display: "flex",
flex: "0",
flexDirection: "row",
justifyContent: "center",
alignItems: "center",
},
button: {
margin: "1em",
},
}))
56 changes: 56 additions & 0 deletions site/pages/projects/[organization]/[project]/create.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import React from "react"
import { makeStyles } from "@material-ui/core/styles"
import { useRouter } from "next/router"
import useSWR from "swr"

import * as API from "../../../../api"
import { useUser } from "../../../../contexts/UserContext"
import { ErrorSummary } from "../../../../components/ErrorSummary"
import { FullScreenLoader } from "../../../../components/Loader/FullScreenLoader"
import { CreateWorkspaceForm } from "../../../../forms/CreateWorkspaceForm"

const CreateWorkspacePage: React.FC = () => {
const router = useRouter()
const styles = useStyles()
const { me } = useUser(/* redirectOnError */ true)
const { organization, project: projectName } = router.query
const { data: project, error: projectError } = useSWR<API.Project, Error>(
`/api/v2/projects/${organization}/${projectName}`,
)

if (projectError) {
return <ErrorSummary error={projectError} />
}

if (!me || !project) {
return <FullScreenLoader />
}

const onCancel = async () => {
await router.push(`/projects/${organization}/${project}`)
}

const onSubmit = async (req: API.CreateWorkspaceRequest) => {
const workspace = await API.Workspace.create(req)
await router.push(`/workspaces/${workspace.id}`)
return workspace
}

return (
<div className={styles.root}>
<CreateWorkspaceForm onCancel={onCancel} onSubmit={onSubmit} project={project} />
</div>
)
}

const useStyles = makeStyles((theme) => ({
root: {
display: "flex",
flexDirection: "column",
alignItems: "center",
height: "100vh",
backgroundColor: theme.palette.background.paper,
},
}))

export default CreateWorkspacePage