Skip to content

Commit 4f75291

Browse files
authored
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.
1 parent b29a2df commit 4f75291

File tree

5 files changed

+318
-5
lines changed

5 files changed

+318
-5
lines changed

site/src/components/Stack/Stack.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import { makeStyles } from "@material-ui/core/styles"
22
import React from "react"
3+
import { combineClasses } from "../../util/combineClasses"
34

45
type Direction = "column" | "row"
56

67
interface StyleProps {
7-
spacing: number
88
direction: Direction
9+
spacing: number
910
}
1011

1112
const useStyles = makeStyles((theme) => ({
@@ -17,11 +18,13 @@ const useStyles = makeStyles((theme) => ({
1718
}))
1819

1920
export interface StackProps {
20-
spacing?: number
21+
className?: string
2122
direction?: Direction
23+
spacing?: number
2224
}
2325

24-
export const Stack: React.FC<StackProps> = ({ children, spacing = 2, direction = "column" }) => {
26+
export const Stack: React.FC<StackProps> = ({ children, className, direction = "column", spacing = 2 }) => {
2527
const styles = useStyles({ spacing, direction })
26-
return <div className={styles.stack}>{children}</div>
28+
29+
return <div className={combineClasses([styles.stack, className])}>{children}</div>
2730
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { action } from "@storybook/addon-actions"
2+
import { Story } from "@storybook/react"
3+
import React from "react"
4+
import { WorkspaceScheduleForm, WorkspaceScheduleFormProps } from "./WorkspaceScheduleForm"
5+
6+
export default {
7+
title: "components/WorkspaceScheduleForm",
8+
component: WorkspaceScheduleForm,
9+
}
10+
11+
const Template: Story<WorkspaceScheduleFormProps> = (args) => <WorkspaceScheduleForm {...args} />
12+
13+
export const Example = Template.bind({})
14+
Example.args = {
15+
onCancel: () => action("onCancel"),
16+
onSubmit: () => {
17+
action("onSubmit")
18+
return Promise.resolve()
19+
},
20+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { Language, validationSchema, WorkspaceScheduleFormValues } from "./WorkspaceScheduleForm"
2+
3+
const valid: WorkspaceScheduleFormValues = {
4+
sunday: false,
5+
monday: true,
6+
tuesday: true,
7+
wednesday: true,
8+
thursday: true,
9+
friday: true,
10+
saturday: false,
11+
12+
startTime: "09:30",
13+
ttl: 120,
14+
}
15+
16+
describe("validationSchema", () => {
17+
it("allows everything to be falsy", () => {
18+
const values: WorkspaceScheduleFormValues = {
19+
sunday: false,
20+
monday: false,
21+
tuesday: false,
22+
wednesday: false,
23+
thursday: false,
24+
friday: false,
25+
saturday: false,
26+
27+
startTime: "",
28+
ttl: 0,
29+
}
30+
const validate = () => validationSchema.validateSync(values)
31+
expect(validate).not.toThrow()
32+
})
33+
34+
it("disallows ttl to be negative", () => {
35+
const values = {
36+
...valid,
37+
ttl: -1,
38+
}
39+
const validate = () => validationSchema.validateSync(values)
40+
expect(validate).toThrow()
41+
})
42+
43+
it("disallows all days-of-week to be false when startTime is set", () => {
44+
const values = {
45+
...valid,
46+
sunday: false,
47+
monday: false,
48+
tuesday: false,
49+
wednesday: false,
50+
thursday: false,
51+
friday: false,
52+
saturday: false,
53+
}
54+
const validate = () => validationSchema.validateSync(values)
55+
expect(validate).toThrowError(Language.errorNoDayOfWeek)
56+
})
57+
})
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
import Checkbox from "@material-ui/core/Checkbox"
2+
import FormControl from "@material-ui/core/FormControl"
3+
import FormControlLabel from "@material-ui/core/FormControlLabel"
4+
import FormGroup from "@material-ui/core/FormGroup"
5+
import FormHelperText from "@material-ui/core/FormHelperText"
6+
import FormLabel from "@material-ui/core/FormLabel"
7+
import makeStyles from "@material-ui/core/styles/makeStyles"
8+
import TextField from "@material-ui/core/TextField"
9+
import { useFormik } from "formik"
10+
import React from "react"
11+
import * as Yup from "yup"
12+
import { getFormHelpers } from "../../util/formUtils"
13+
import { FormFooter } from "../FormFooter/FormFooter"
14+
import { FullPageForm } from "../FullPageForm/FullPageForm"
15+
import { Stack } from "../Stack/Stack"
16+
17+
export const Language = {
18+
errorNoDayOfWeek: "Must set at least one day of week",
19+
daysOfWeekLabel: "Days of Week",
20+
daySundayLabel: "Sunday",
21+
dayMondayLabel: "Monday",
22+
dayTuesdayLabel: "Tuesday",
23+
dayWednesdayLabel: "Wednesday",
24+
dayThursdayLabel: "Thursday",
25+
dayFridayLabel: "Friday",
26+
daySaturdayLabel: "Saturday",
27+
startTimeLabel: "Start time",
28+
startTimeHelperText: "Your workspace will automatically start at this time.",
29+
ttlLabel: "Runtime (minutes)",
30+
ttlHelperText: "Your workspace will automatically shutdown after the runtime.",
31+
}
32+
33+
export interface WorkspaceScheduleFormProps {
34+
onCancel: () => void
35+
onSubmit: (values: WorkspaceScheduleFormValues) => Promise<void>
36+
}
37+
38+
export interface WorkspaceScheduleFormValues {
39+
sunday: boolean
40+
monday: boolean
41+
tuesday: boolean
42+
wednesday: boolean
43+
thursday: boolean
44+
friday: boolean
45+
saturday: boolean
46+
47+
startTime: string
48+
ttl: number
49+
}
50+
51+
export const validationSchema = Yup.object({
52+
sunday: Yup.boolean(),
53+
monday: Yup.boolean().test("at-least-one-day", Language.errorNoDayOfWeek, function (value) {
54+
const parent = this.parent as WorkspaceScheduleFormValues
55+
56+
if (!parent.startTime) {
57+
return true
58+
} else {
59+
return ![
60+
parent.sunday,
61+
value,
62+
parent.tuesday,
63+
parent.wednesday,
64+
parent.thursday,
65+
parent.friday,
66+
parent.saturday,
67+
].every((day) => day === false)
68+
}
69+
}),
70+
tuesday: Yup.boolean(),
71+
wednesday: Yup.boolean(),
72+
thursday: Yup.boolean(),
73+
friday: Yup.boolean(),
74+
saturday: Yup.boolean(),
75+
76+
startTime: Yup.string(),
77+
ttl: Yup.number().min(0).integer(),
78+
})
79+
80+
export const WorkspaceScheduleForm: React.FC<WorkspaceScheduleFormProps> = ({ onCancel, onSubmit }) => {
81+
const styles = useStyles()
82+
83+
const form = useFormik<WorkspaceScheduleFormValues>({
84+
initialValues: {
85+
sunday: false,
86+
monday: true,
87+
tuesday: true,
88+
wednesday: true,
89+
thursday: true,
90+
friday: true,
91+
saturday: false,
92+
93+
startTime: "09:30",
94+
ttl: 120,
95+
},
96+
onSubmit,
97+
validationSchema,
98+
})
99+
const formHelpers = getFormHelpers<WorkspaceScheduleFormValues>(form)
100+
101+
return (
102+
<FullPageForm onCancel={onCancel} title="Workspace Schedule">
103+
<form className={styles.form} onSubmit={form.handleSubmit}>
104+
<Stack className={styles.stack}>
105+
<TextField
106+
{...formHelpers("startTime", Language.startTimeHelperText)}
107+
InputLabelProps={{
108+
shrink: true,
109+
}}
110+
label={Language.startTimeLabel}
111+
type="time"
112+
variant="standard"
113+
/>
114+
115+
<FormControl component="fieldset" error={Boolean(form.errors.monday)}>
116+
<FormLabel className={styles.daysOfWeekLabel} component="legend">
117+
{Language.daysOfWeekLabel}
118+
</FormLabel>
119+
120+
<FormGroup>
121+
<FormControlLabel
122+
control={
123+
<Checkbox
124+
checked={form.values.sunday}
125+
disabled={!form.values.startTime}
126+
onChange={form.handleChange}
127+
name="sunday"
128+
/>
129+
}
130+
label={Language.daySundayLabel}
131+
/>
132+
<FormControlLabel
133+
control={
134+
<Checkbox
135+
checked={form.values.monday}
136+
disabled={!form.values.startTime}
137+
onChange={form.handleChange}
138+
name="monday"
139+
/>
140+
}
141+
label={Language.dayMondayLabel}
142+
/>
143+
<FormControlLabel
144+
control={
145+
<Checkbox
146+
checked={form.values.tuesday}
147+
disabled={!form.values.startTime}
148+
onChange={form.handleChange}
149+
name="tuesday"
150+
/>
151+
}
152+
label={Language.dayTuesdayLabel}
153+
/>
154+
<FormControlLabel
155+
control={
156+
<Checkbox
157+
checked={form.values.wednesday}
158+
disabled={!form.values.startTime}
159+
onChange={form.handleChange}
160+
name="wednesday"
161+
/>
162+
}
163+
label={Language.dayWednesdayLabel}
164+
/>
165+
<FormControlLabel
166+
control={
167+
<Checkbox
168+
checked={form.values.thursday}
169+
disabled={!form.values.startTime}
170+
onChange={form.handleChange}
171+
name="thursday"
172+
/>
173+
}
174+
label={Language.dayThursdayLabel}
175+
/>
176+
<FormControlLabel
177+
control={
178+
<Checkbox
179+
checked={form.values.friday}
180+
disabled={!form.values.startTime}
181+
onChange={form.handleChange}
182+
name="friday"
183+
/>
184+
}
185+
label={Language.dayFridayLabel}
186+
/>
187+
<FormControlLabel
188+
control={
189+
<Checkbox
190+
checked={form.values.saturday}
191+
disabled={!form.values.startTime}
192+
onChange={form.handleChange}
193+
name="saturday"
194+
/>
195+
}
196+
label={Language.daySaturdayLabel}
197+
/>
198+
</FormGroup>
199+
{form.errors.monday && <FormHelperText>{Language.errorNoDayOfWeek}</FormHelperText>}
200+
</FormControl>
201+
202+
<TextField
203+
{...formHelpers("ttl", Language.ttlHelperText)}
204+
inputProps={{ min: 0, step: 30 }}
205+
label={Language.ttlLabel}
206+
type="number"
207+
variant="standard"
208+
/>
209+
210+
<FormFooter onCancel={onCancel} isLoading={form.isSubmitting} />
211+
</Stack>
212+
</form>
213+
</FullPageForm>
214+
)
215+
}
216+
217+
const useStyles = makeStyles({
218+
form: {
219+
display: "flex",
220+
justifyContent: "center",
221+
},
222+
stack: {
223+
// REMARK: 360 is 'arbitrary' in that it gives the helper text enough room
224+
// to render on one line. If we change the text, we might want to
225+
// adjust these. Without constraining the width, the date picker
226+
// and number inputs aren't visually appealing or maximally usable.
227+
maxWidth: 360,
228+
minWidth: 360,
229+
},
230+
daysOfWeekLabel: {
231+
fontSize: 12,
232+
},
233+
})

site/src/util/formUtils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export const getFormHelpers =
2828
...form.getFieldProps(name),
2929
id: name,
3030
error: touched && Boolean(error),
31-
helperText: touched ? error : helperText,
31+
helperText: touched ? error || helperText : helperText,
3232
}
3333
}
3434

0 commit comments

Comments
 (0)