mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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:
@@ -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,
|
||||
},
|
||||
})
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user