feat: form for editing ws schedule (#1634)

* feat: ui for editing ws schedule

Summary:

This presents a form component and storybook. The UI will be a routed
page and added into the dashboard in a separate PR. It is likely a
XService will be used at the page level to supply errors and actions to
this form.

Impact of Change:

Further progress on #1455

Squashed Commits:

* refactor: add className prop to Stack

combine classes with internal classes and an optional external className
to better control the Stack.

* fix: getFormHelpers helperText

the helperText logic was incorrect, the helperText would only show if not touched.
This commit is contained in:
G r e y
2022-05-20 20:26:43 +00:00
committed by GitHub
parent b29a2dfdde
commit 4f75291446
5 changed files with 318 additions and 5 deletions
+7 -4
View File
@@ -1,11 +1,12 @@
import { makeStyles } from "@material-ui/core/styles"
import React from "react"
import { combineClasses } from "../../util/combineClasses"
type Direction = "column" | "row"
interface StyleProps {
spacing: number
direction: Direction
spacing: number
}
const useStyles = makeStyles((theme) => ({
@@ -17,11 +18,13 @@ const useStyles = makeStyles((theme) => ({
}))
export interface StackProps {
spacing?: number
className?: string
direction?: Direction
spacing?: number
}
export const Stack: React.FC<StackProps> = ({ children, spacing = 2, direction = "column" }) => {
export const Stack: React.FC<StackProps> = ({ children, className, direction = "column", spacing = 2 }) => {
const styles = useStyles({ spacing, direction })
return <div className={styles.stack}>{children}</div>
return <div className={combineClasses([styles.stack, className])}>{children}</div>
}
@@ -0,0 +1,20 @@
import { action } from "@storybook/addon-actions"
import { Story } from "@storybook/react"
import React from "react"
import { WorkspaceScheduleForm, WorkspaceScheduleFormProps } from "./WorkspaceScheduleForm"
export default {
title: "components/WorkspaceScheduleForm",
component: WorkspaceScheduleForm,
}
const Template: Story<WorkspaceScheduleFormProps> = (args) => <WorkspaceScheduleForm {...args} />
export const Example = Template.bind({})
Example.args = {
onCancel: () => action("onCancel"),
onSubmit: () => {
action("onSubmit")
return Promise.resolve()
},
}
@@ -0,0 +1,57 @@
import { Language, validationSchema, WorkspaceScheduleFormValues } from "./WorkspaceScheduleForm"
const valid: WorkspaceScheduleFormValues = {
sunday: false,
monday: true,
tuesday: true,
wednesday: true,
thursday: true,
friday: true,
saturday: false,
startTime: "09:30",
ttl: 120,
}
describe("validationSchema", () => {
it("allows everything to be falsy", () => {
const values: WorkspaceScheduleFormValues = {
sunday: false,
monday: false,
tuesday: false,
wednesday: false,
thursday: false,
friday: false,
saturday: false,
startTime: "",
ttl: 0,
}
const validate = () => validationSchema.validateSync(values)
expect(validate).not.toThrow()
})
it("disallows ttl to be negative", () => {
const values = {
...valid,
ttl: -1,
}
const validate = () => validationSchema.validateSync(values)
expect(validate).toThrow()
})
it("disallows all days-of-week to be false when startTime is set", () => {
const values = {
...valid,
sunday: false,
monday: false,
tuesday: false,
wednesday: false,
thursday: false,
friday: false,
saturday: false,
}
const validate = () => validationSchema.validateSync(values)
expect(validate).toThrowError(Language.errorNoDayOfWeek)
})
})
@@ -0,0 +1,233 @@
import Checkbox from "@material-ui/core/Checkbox"
import FormControl from "@material-ui/core/FormControl"
import FormControlLabel from "@material-ui/core/FormControlLabel"
import FormGroup from "@material-ui/core/FormGroup"
import FormHelperText from "@material-ui/core/FormHelperText"
import FormLabel from "@material-ui/core/FormLabel"
import makeStyles from "@material-ui/core/styles/makeStyles"
import TextField from "@material-ui/core/TextField"
import { useFormik } from "formik"
import React from "react"
import * as Yup from "yup"
import { getFormHelpers } from "../../util/formUtils"
import { FormFooter } from "../FormFooter/FormFooter"
import { FullPageForm } from "../FullPageForm/FullPageForm"
import { Stack } from "../Stack/Stack"
export const Language = {
errorNoDayOfWeek: "Must set at least one day of week",
daysOfWeekLabel: "Days of Week",
daySundayLabel: "Sunday",
dayMondayLabel: "Monday",
dayTuesdayLabel: "Tuesday",
dayWednesdayLabel: "Wednesday",
dayThursdayLabel: "Thursday",
dayFridayLabel: "Friday",
daySaturdayLabel: "Saturday",
startTimeLabel: "Start time",
startTimeHelperText: "Your workspace will automatically start at this time.",
ttlLabel: "Runtime (minutes)",
ttlHelperText: "Your workspace will automatically shutdown after the runtime.",
}
export interface WorkspaceScheduleFormProps {
onCancel: () => void
onSubmit: (values: WorkspaceScheduleFormValues) => Promise<void>
}
export interface WorkspaceScheduleFormValues {
sunday: boolean
monday: boolean
tuesday: boolean
wednesday: boolean
thursday: boolean
friday: boolean
saturday: boolean
startTime: string
ttl: number
}
export const validationSchema = Yup.object({
sunday: Yup.boolean(),
monday: Yup.boolean().test("at-least-one-day", Language.errorNoDayOfWeek, function (value) {
const parent = this.parent as WorkspaceScheduleFormValues
if (!parent.startTime) {
return true
} else {
return ![
parent.sunday,
value,
parent.tuesday,
parent.wednesday,
parent.thursday,
parent.friday,
parent.saturday,
].every((day) => day === false)
}
}),
tuesday: Yup.boolean(),
wednesday: Yup.boolean(),
thursday: Yup.boolean(),
friday: Yup.boolean(),
saturday: Yup.boolean(),
startTime: Yup.string(),
ttl: Yup.number().min(0).integer(),
})
export const WorkspaceScheduleForm: React.FC<WorkspaceScheduleFormProps> = ({ onCancel, onSubmit }) => {
const styles = useStyles()
const form = useFormik<WorkspaceScheduleFormValues>({
initialValues: {
sunday: false,
monday: true,
tuesday: true,
wednesday: true,
thursday: true,
friday: true,
saturday: false,
startTime: "09:30",
ttl: 120,
},
onSubmit,
validationSchema,
})
const formHelpers = getFormHelpers<WorkspaceScheduleFormValues>(form)
return (
<FullPageForm onCancel={onCancel} title="Workspace Schedule">
<form className={styles.form} onSubmit={form.handleSubmit}>
<Stack className={styles.stack}>
<TextField
{...formHelpers("startTime", Language.startTimeHelperText)}
InputLabelProps={{
shrink: true,
}}
label={Language.startTimeLabel}
type="time"
variant="standard"
/>
<FormControl component="fieldset" error={Boolean(form.errors.monday)}>
<FormLabel className={styles.daysOfWeekLabel} component="legend">
{Language.daysOfWeekLabel}
</FormLabel>
<FormGroup>
<FormControlLabel
control={
<Checkbox
checked={form.values.sunday}
disabled={!form.values.startTime}
onChange={form.handleChange}
name="sunday"
/>
}
label={Language.daySundayLabel}
/>
<FormControlLabel
control={
<Checkbox
checked={form.values.monday}
disabled={!form.values.startTime}
onChange={form.handleChange}
name="monday"
/>
}
label={Language.dayMondayLabel}
/>
<FormControlLabel
control={
<Checkbox
checked={form.values.tuesday}
disabled={!form.values.startTime}
onChange={form.handleChange}
name="tuesday"
/>
}
label={Language.dayTuesdayLabel}
/>
<FormControlLabel
control={
<Checkbox
checked={form.values.wednesday}
disabled={!form.values.startTime}
onChange={form.handleChange}
name="wednesday"
/>
}
label={Language.dayWednesdayLabel}
/>
<FormControlLabel
control={
<Checkbox
checked={form.values.thursday}
disabled={!form.values.startTime}
onChange={form.handleChange}
name="thursday"
/>
}
label={Language.dayThursdayLabel}
/>
<FormControlLabel
control={
<Checkbox
checked={form.values.friday}
disabled={!form.values.startTime}
onChange={form.handleChange}
name="friday"
/>
}
label={Language.dayFridayLabel}
/>
<FormControlLabel
control={
<Checkbox
checked={form.values.saturday}
disabled={!form.values.startTime}
onChange={form.handleChange}
name="saturday"
/>
}
label={Language.daySaturdayLabel}
/>
</FormGroup>
{form.errors.monday && <FormHelperText>{Language.errorNoDayOfWeek}</FormHelperText>}
</FormControl>
<TextField
{...formHelpers("ttl", Language.ttlHelperText)}
inputProps={{ min: 0, step: 30 }}
label={Language.ttlLabel}
type="number"
variant="standard"
/>
<FormFooter onCancel={onCancel} isLoading={form.isSubmitting} />
</Stack>
</form>
</FullPageForm>
)
}
const useStyles = makeStyles({
form: {
display: "flex",
justifyContent: "center",
},
stack: {
// REMARK: 360 is 'arbitrary' in that it gives the helper text enough room
// to render on one line. If we change the text, we might want to
// adjust these. Without constraining the width, the date picker
// and number inputs aren't visually appealing or maximally usable.
maxWidth: 360,
minWidth: 360,
},
daysOfWeekLabel: {
fontSize: 12,
},
})
+1 -1
View File
@@ -28,7 +28,7 @@ export const getFormHelpers =
...form.getFieldProps(name),
id: name,
error: touched && Boolean(error),
helperText: touched ? error : helperText,
helperText: touched ? error || helperText : helperText,
}
}