-
Notifications
You must be signed in to change notification settings - Fork 930
feat: UX - Initial create form flow #33
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
Changes from 25 commits
18dd18f
7329cca
6726f40
3f932c2
6fdb43f
97b2383
086d533
82989d3
c305c8d
cc411d0
907a523
56f7515
0a5c48a
aea58ae
c92e454
be4c90f
308de2b
d0b0ef1
797c82b
5016b9a
8da382b
6fc0a54
e57d1b6
08010c3
8800445
496b42e
646206e
4b6f1c5
961a44b
eb46f4c
840a796
f20171f
03236df
61cd37f
99d0a95
d52bd38
76d11d4
ffac475
e95d75f
3e8069b
9492e8e
e5e02f9
5e4a44f
6fe7141
8343ef0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
import { wait } from "./util" | ||
|
||
// TEMPORARY | ||
// This is all placeholder / stub code until we have a real API to work with! | ||
// | ||
// The implementations below that are hard-coded will switch to using `fetch` | ||
// once the routes are available. | ||
// TEMPORARY | ||
|
||
export type ProjectParameterType = "string" | "number" | ||
|
||
export interface ProjectParameter { | ||
id: string | ||
name: string | ||
description: string | ||
defaultValue?: string | ||
type: ProjectParameterType | ||
} | ||
|
||
export interface Project { | ||
id: string | ||
icon?: string | ||
name: string | ||
description: string | ||
parameters: ProjectParameter[] | ||
} | ||
|
||
export namespace Project { | ||
const testProject1: Project = { | ||
id: "test-terraform-1", | ||
icon: "https://www.datocms-assets.com/2885/1620155117-brandhcterraformverticalcolorwhite.svg", | ||
name: "Terraform Project 1", | ||
description: "Simple terraform project that deploys a kubernetes provider", | ||
parameters: [ | ||
{ | ||
id: "namespace", | ||
name: "Namespace", | ||
description: "The kubernetes namespace that will own the workspace pod.", | ||
defaultValue: "default", | ||
type: "string", | ||
}, | ||
{ | ||
id: "cpu_cores", | ||
name: "CPU Cores", | ||
description: "Number of CPU cores to allocate for pod.", | ||
type: "number", | ||
}, | ||
], | ||
} | ||
|
||
const testProject2: Project = { | ||
id: "test-echo-1", | ||
name: "Echo Project", | ||
description: "Project built on echo provisioner", | ||
parameters: [ | ||
{ | ||
id: "echo_string", | ||
name: "Echo string", | ||
description: "String that will be echoed out during build.", | ||
type: "string", | ||
}, | ||
], | ||
} | ||
|
||
const allProjects = [testProject1, testProject2] | ||
|
||
export const getAllProjectsInOrg = async (_org: string): Promise<Project[]> => { | ||
await wait(250) | ||
return allProjects | ||
} | ||
|
||
export const getProject = async (_org: string, projectId: string): Promise<Project> => { | ||
await wait(250) | ||
|
||
const matchingProjects = allProjects.filter((p) => p.id === projectId) | ||
|
||
if (matchingProjects.length === 0) { | ||
throw new Error(`No project matching ${projectId} found`) | ||
} | ||
|
||
return matchingProjects[0] | ||
} | ||
|
||
export const createWorkspace = async (name: string): Promise<string> => { | ||
await wait(250) | ||
return "test-workspace" | ||
} | ||
} | ||
|
||
export namespace Workspace { | ||
export type WorkspaceId = string | ||
|
||
export const createWorkspace = ( | ||
name: string, | ||
projectTemplate: string, | ||
parameters: Record<string, string>, | ||
): Promise<WorkspaceId> => { | ||
alert( | ||
`Creating workspace named ${name} for project ${projectTemplate} with parameters: ${JSON.stringify(parameters)}`, | ||
) | ||
|
||
return Promise.resolve("test-workspace") | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import { makeStyles } from "@material-ui/core/styles" | ||
import React from "react" | ||
|
||
const useStyles = makeStyles((theme) => ({ | ||
row: { | ||
marginTop: theme.spacing(2), | ||
marginBottom: theme.spacing(2), | ||
}, | ||
})) | ||
|
||
export const FormRow: React.FC = ({ children }) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we believe it's safe to say all of our Form instances will follow a similar format? I have lots of hesitancy over a top-level form component. If we even have a single form that doesn't consume it, this top-level component abstraction has broken. I'm not sure of an alternative though, so maybe this is required! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this is a good point for this component - it's only used in one places so I can bring it inline. |
||
const styles = useStyles() | ||
return <div className={styles.row}>{children}</div> | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
import FormHelperText from "@material-ui/core/FormHelperText" | ||
import { makeStyles } from "@material-ui/core/styles" | ||
import Typography from "@material-ui/core/Typography" | ||
import { Style } from "@material-ui/icons" | ||
import React from "react" | ||
|
||
export interface FormSectionProps { | ||
title: string | ||
description?: string | ||
} | ||
|
||
export const useStyles = makeStyles((theme) => ({ | ||
root: { | ||
display: "flex", | ||
flexDirection: "row", | ||
// Borrowed from PaperForm styles | ||
maxWidth: "852px", | ||
width: "100%", | ||
borderBottom: `1px solid ${theme.palette.divider}`, | ||
}, | ||
descriptionContainer: { | ||
maxWidth: "200px", | ||
flex: "0 0 200px", | ||
display: "flex", | ||
flexDirection: "column", | ||
justifyContent: "flex-start", | ||
alignItems: "flex-start", | ||
marginTop: theme.spacing(5), | ||
marginBottom: theme.spacing(2), | ||
}, | ||
descriptionText: { | ||
fontSize: "0.9em", | ||
lineHeight: "1em", | ||
color: theme.palette.text.secondary, | ||
marginTop: theme.spacing(1), | ||
}, | ||
contents: { | ||
flex: 1, | ||
marginTop: theme.spacing(2), | ||
marginBottom: theme.spacing(2), | ||
}, | ||
})) | ||
|
||
export const FormSection: React.FC<FormSectionProps> = ({ title, description, children }) => { | ||
const styles = useStyles() | ||
|
||
return ( | ||
<div className={styles.root}> | ||
<div className={styles.descriptionContainer}> | ||
<Typography variant="h5" color="textPrimary"> | ||
{title} | ||
</Typography> | ||
{description && ( | ||
<Typography className={styles.descriptionText} variant="body2" color="textSecondary"> | ||
{description} | ||
</Typography> | ||
)} | ||
</div> | ||
<div className={styles.contents}>{children}</div> | ||
</div> | ||
) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,153 @@ | ||
import TextField, { TextFieldProps } from "@material-ui/core/TextField" | ||
bryphe-coder marked this conversation as resolved.
Show resolved
Hide resolved
|
||
import { FormikLike } from "../../util/formik" | ||
import React from "react" | ||
import { PasswordField } from "./PasswordField" | ||
import { FormFieldProps } from "./types" | ||
|
||
/** | ||
* FormTextFieldProps extends form-related MUI TextFieldProps with Formik | ||
* props. The passed in form is used to compute error states and configure | ||
* change handlers. `formFieldName` represents the key of a Formik value | ||
* that's associated to this component. | ||
*/ | ||
export interface FormTextFieldProps<T> | ||
extends Pick< | ||
TextFieldProps, | ||
| "autoComplete" | ||
| "autoFocus" | ||
| "children" | ||
| "className" | ||
| "disabled" | ||
| "fullWidth" | ||
| "helperText" | ||
| "id" | ||
| "InputLabelProps" | ||
| "InputProps" | ||
| "inputProps" | ||
| "label" | ||
| "margin" | ||
| "multiline" | ||
| "onChange" | ||
| "placeholder" | ||
| "required" | ||
| "rows" | ||
| "select" | ||
| "SelectProps" | ||
| "style" | ||
| "type" | ||
| "variant" | ||
>, | ||
FormFieldProps<T> { | ||
/** | ||
* eventTransform is an optional transformer on the event data before it is | ||
* processed by formik. | ||
* | ||
* @example | ||
* <FormTextField | ||
* eventTransformer={(str) => { | ||
* return str.replace(" ", "-") | ||
* }} | ||
* /> | ||
*/ | ||
eventTransform?: (value: string) => unknown | ||
/** | ||
* isPassword uses a PasswordField component when `true`; otherwise a | ||
* TextField component is used. | ||
*/ | ||
isPassword?: boolean | ||
/** | ||
* displayValueOverride allows displaying a different value in the field | ||
* without changing the actual underlying value. | ||
*/ | ||
displayValueOverride?: string | ||
} | ||
|
||
/** | ||
* Factory function for creating a formik TextField | ||
* | ||
* @example | ||
* interface FormValues { | ||
* username: string | ||
* } | ||
* | ||
* // Use the factory to create a FormTextField associated to this form | ||
* const FormTextField = formTextFieldFactory<FormValues>() | ||
* | ||
* const MyComponent: React.FC = () => { | ||
* const form = useFormik<FormValues>() | ||
* | ||
* return ( | ||
* <FormTextField | ||
* form={form} | ||
* formFieldName="username" | ||
* fullWidth | ||
* helperText="A unique name" | ||
* label="Username" | ||
* placeholder="Lorem Ipsum" | ||
* required | ||
* /> | ||
* ) | ||
* } | ||
*/ | ||
export const formTextFieldFactory = <T,>(): React.FC<FormTextFieldProps<T>> => { | ||
const component: React.FC<FormTextFieldProps<T>> = ({ | ||
children, | ||
disabled, | ||
displayValueOverride, | ||
eventTransform, | ||
form, | ||
formFieldName, | ||
helperText, | ||
isPassword = false, | ||
InputProps, | ||
onChange, | ||
type, | ||
...rest | ||
}) => { | ||
const isError = form.touched[formFieldName] && Boolean(form.errors[formFieldName]) | ||
|
||
// Conversion to a string primitive is necessary as formFieldName is an in | ||
// indexable type such as a string, number or enum. | ||
const fieldId = FormikLike.getFieldId<T>(form, String(formFieldName)) | ||
|
||
const Component = isPassword ? PasswordField : TextField | ||
const inputType = isPassword ? undefined : type | ||
|
||
return ( | ||
<Component | ||
{...rest} | ||
disabled={disabled || form.isSubmitting} | ||
error={isError} | ||
helperText={isError ? form.errors[formFieldName] : helperText} | ||
id={fieldId} | ||
InputProps={isPassword ? undefined : InputProps} | ||
name={fieldId} | ||
onBlur={form.handleBlur} | ||
onChange={(e) => { | ||
if (typeof onChange !== "undefined") { | ||
onChange(e) | ||
} | ||
|
||
const event = e | ||
if (typeof eventTransform !== "undefined") { | ||
// TODO(Grey): Asserting the type as a string here is not quite | ||
// right in that when an input is of type="number", the value will | ||
// be a number. Type asserting is better than conversion for this | ||
// reason, but perhaps there's a better way to do this without any | ||
// assertions. | ||
event.target.value = eventTransform(e.target.value) as string | ||
} | ||
form.handleChange(event) | ||
}} | ||
type={inputType} | ||
value={displayValueOverride || form.values[formFieldName]} | ||
> | ||
{children} | ||
</Component> | ||
) | ||
} | ||
|
||
// Required when using an anonymous factory function | ||
component.displayName = "FormTextField" | ||
return component | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import { makeStyles } from "@material-ui/core/styles" | ||
import Typography from "@material-ui/core/Typography" | ||
import React from "react" | ||
|
||
export interface TitleProps { | ||
title: string | ||
detail: React.ReactNode | ||
} | ||
|
||
const useStyles = makeStyles((theme) => ({ | ||
title: { | ||
textAlign: "center", | ||
marginTop: theme.spacing(5), | ||
marginBottom: theme.spacing(5), | ||
|
||
"& h3": { | ||
marginBottom: theme.spacing(1), | ||
}, | ||
}, | ||
})) | ||
|
||
export const Title: React.FC<TitleProps> = ({ title, detail }) => { | ||
const styles = useStyles() | ||
|
||
return ( | ||
<div className={styles.title}> | ||
<Typography variant="h3">{title}</Typography> | ||
<Typography variant="caption">{detail}</Typography> | ||
</div> | ||
) | ||
} |
Uh oh!
There was an error while loading. Please reload this page.