Skip to content

Commit ccc008e

Browse files
authored
fix: redesign schedule bumper to handle multiple hours of change at once (#4535)
* Start sketching out new design * Working but ugly * Highlight chosen mode * Format * Set hours field width * Alignment on desktop * Use primary button color * Make 1 the default change * Add stepper max * Fix storybook * Handle undefined deadline * Access deadline correctly * Format * Fix overflow on mobile
1 parent d898737 commit ccc008e

11 files changed

+260
-73
lines changed

site/src/components/Workspace/Workspace.tsx

+6-2
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,12 @@ export interface WorkspaceProps {
3434
onExtend: () => void
3535
}
3636
scheduleProps: {
37-
onDeadlinePlus: () => void
38-
onDeadlineMinus: () => void
37+
onDeadlinePlus: (hours: number) => void
38+
onDeadlineMinus: (hours: number) => void
3939
deadlinePlusEnabled: () => boolean
4040
deadlineMinusEnabled: () => boolean
41+
maxDeadlineIncrease: number
42+
maxDeadlineDecrease: number
4143
}
4244
handleStart: () => void
4345
handleStop: () => void
@@ -121,6 +123,8 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
121123
onDeadlinePlus={scheduleProps.onDeadlinePlus}
122124
deadlineMinusEnabled={scheduleProps.deadlineMinusEnabled}
123125
deadlinePlusEnabled={scheduleProps.deadlinePlusEnabled}
126+
maxDeadlineDecrease={scheduleProps.maxDeadlineDecrease}
127+
maxDeadlineIncrease={scheduleProps.maxDeadlineIncrease}
124128
canUpdateWorkspace={canUpdateWorkspace}
125129
/>
126130
<WorkspaceActions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import Button from "@material-ui/core/Button"
2+
import { makeStyles } from "@material-ui/core/styles"
3+
import TextField from "@material-ui/core/TextField"
4+
import { Stack } from "components/Stack/Stack"
5+
import { useState } from "react"
6+
import { useTranslation } from "react-i18next"
7+
8+
interface EditHoursProps {
9+
handleSubmit: (hours: number) => void
10+
max: number
11+
}
12+
13+
export const EditHours = ({
14+
handleSubmit,
15+
max,
16+
}: EditHoursProps): JSX.Element => {
17+
const { t } = useTranslation("workspacePage")
18+
const [hours, setHours] = useState(1)
19+
const styles = useStyles()
20+
21+
return (
22+
<form onSubmit={() => handleSubmit(hours)}>
23+
<Stack direction="row" alignItems="baseline" spacing={1}>
24+
<TextField
25+
className={styles.inputField}
26+
inputProps={{ min: 0, max, step: 1 }}
27+
label={t("workspaceScheduleButton.hours")}
28+
value={hours}
29+
onChange={(e) => setHours(parseInt(e.target.value))}
30+
type="number"
31+
/>
32+
<Button className={styles.button} type="submit" color="primary">
33+
{t("workspaceScheduleButton.submitDeadline")}
34+
</Button>
35+
</Stack>
36+
</form>
37+
)
38+
}
39+
40+
const useStyles = makeStyles(() => ({
41+
inputField: {
42+
width: "70px",
43+
"& .MuiOutlinedInput-root": {
44+
height: "30px",
45+
},
46+
},
47+
button: {
48+
"&.MuiButton-root": {
49+
minHeight: "30px",
50+
height: "30px",
51+
},
52+
},
53+
}))

site/src/components/WorkspaceScheduleButton/WorkspaceScheduleButton.stories.tsx

+7
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ export default {
1616
canUpdateWorkspace: {
1717
defaultValue: true,
1818
},
19+
deadlineMinusEnabled: {
20+
defaultValue: (): boolean => false,
21+
},
22+
deadlinePlusEnabled: {
23+
defaultValue: (): boolean => false,
24+
},
1925
},
2026
}
2127

@@ -64,6 +70,7 @@ WorkspaceOffShort.args = {
6470

6571
export const WorkspaceOffLong = Template.bind({})
6672
WorkspaceOffLong.args = {
73+
deadlinePlusEnabled: () => true,
6774
workspace: {
6875
...Mocks.MockWorkspace,
6976

site/src/components/WorkspaceScheduleButton/WorkspaceScheduleButton.test.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import dayjs from "dayjs"
22
import utc from "dayjs/plugin/utc"
33
import * as TypesGen from "../../api/typesGenerated"
44
import * as Mocks from "../../testHelpers/entities"
5-
import { shouldDisplayPlusMinus } from "./WorkspaceScheduleButton"
5+
import { canEditDeadline } from "./WorkspaceScheduleButton"
66

77
dayjs.extend(utc)
88

@@ -13,15 +13,15 @@ describe("WorkspaceScheduleButton", () => {
1313
const workspace: TypesGen.Workspace = Mocks.MockStoppedWorkspace
1414

1515
// Then: shouldDisplayPlusMinus should be false
16-
expect(shouldDisplayPlusMinus(workspace)).toBeFalsy()
16+
expect(canEditDeadline(workspace)).toBeFalsy()
1717
})
1818

1919
it("should display if the workspace is running", () => {
2020
// Given: a stopped workspace
2121
const workspace: TypesGen.Workspace = Mocks.MockWorkspace
2222

2323
// Then: shouldDisplayPlusMinus should be false
24-
expect(shouldDisplayPlusMinus(workspace)).toBeTruthy()
24+
expect(canEditDeadline(workspace)).toBeTruthy()
2525
})
2626
})
2727
})

site/src/components/WorkspaceScheduleButton/WorkspaceScheduleButton.tsx

+105-41
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import Button from "@material-ui/core/Button"
22
import IconButton from "@material-ui/core/IconButton"
33
import Popover from "@material-ui/core/Popover"
4-
import { makeStyles } from "@material-ui/core/styles"
4+
import { makeStyles, Theme } from "@material-ui/core/styles"
55
import Tooltip from "@material-ui/core/Tooltip"
66
import AddIcon from "@material-ui/icons/Add"
77
import RemoveIcon from "@material-ui/icons/Remove"
88
import ScheduleIcon from "@material-ui/icons/Schedule"
9+
import { Maybe } from "components/Conditionals/Maybe"
10+
import { Stack } from "components/Stack/Stack"
911
import dayjs from "dayjs"
1012
import advancedFormat from "dayjs/plugin/advancedFormat"
1113
import duration from "dayjs/plugin/duration"
@@ -17,6 +19,7 @@ import { useTranslation } from "react-i18next"
1719
import { Workspace } from "../../api/typesGenerated"
1820
import { isWorkspaceOn } from "../../util/workspace"
1921
import { WorkspaceSchedule } from "../WorkspaceSchedule/WorkspaceSchedule"
22+
import { EditHours } from "./EditHours"
2023
import { WorkspaceScheduleLabel } from "./WorkspaceScheduleLabel"
2124

2225
// REMARK: some plugins depend on utc, so it's listed first. Otherwise they're
@@ -27,12 +30,12 @@ dayjs.extend(duration)
2730
dayjs.extend(relativeTime)
2831
dayjs.extend(timezone)
2932

30-
export const shouldDisplayPlusMinus = (workspace: Workspace): boolean => {
33+
export const canEditDeadline = (workspace: Workspace): boolean => {
3134
return isWorkspaceOn(workspace) && Boolean(workspace.latest_build.deadline)
3235
}
3336

3437
export const shouldDisplayScheduleLabel = (workspace: Workspace): boolean => {
35-
if (shouldDisplayPlusMinus(workspace)) {
38+
if (canEditDeadline(workspace)) {
3639
return true
3740
}
3841
if (isWorkspaceOn(workspace)) {
@@ -43,13 +46,17 @@ export const shouldDisplayScheduleLabel = (workspace: Workspace): boolean => {
4346

4447
export interface WorkspaceScheduleButtonProps {
4548
workspace: Workspace
46-
onDeadlinePlus: () => void
47-
onDeadlineMinus: () => void
49+
onDeadlinePlus: (hours: number) => void
50+
onDeadlineMinus: (hours: number) => void
4851
deadlineMinusEnabled: () => boolean
4952
deadlinePlusEnabled: () => boolean
53+
maxDeadlineIncrease: number
54+
maxDeadlineDecrease: number
5055
canUpdateWorkspace: boolean
5156
}
5257

58+
export type EditMode = "add" | "subtract" | "off"
59+
5360
export const WorkspaceScheduleButton: React.FC<
5461
WorkspaceScheduleButtonProps
5562
> = ({
@@ -58,49 +65,93 @@ export const WorkspaceScheduleButton: React.FC<
5865
onDeadlineMinus,
5966
deadlinePlusEnabled,
6067
deadlineMinusEnabled,
68+
maxDeadlineDecrease,
69+
maxDeadlineIncrease,
6170
canUpdateWorkspace,
6271
}) => {
6372
const { t } = useTranslation("workspacePage")
6473
const anchorRef = useRef<HTMLButtonElement>(null)
6574
const [isOpen, setIsOpen] = useState(false)
75+
const [editMode, setEditMode] = useState<EditMode>("off")
6676
const id = isOpen ? "schedule-popover" : undefined
67-
const styles = useStyles()
77+
const styles = useStyles({ editMode })
6878

6979
const onClose = () => {
7080
setIsOpen(false)
7181
}
7282

83+
const handleSubmitHours = (hours: number) => {
84+
if (hours !== 0) {
85+
if (editMode === "add") {
86+
onDeadlinePlus(hours)
87+
}
88+
if (editMode === "subtract") {
89+
onDeadlineMinus(hours)
90+
}
91+
}
92+
setEditMode("off")
93+
}
94+
7395
return (
7496
<span className={styles.wrapper}>
75-
{shouldDisplayScheduleLabel(workspace) && (
76-
<span className={styles.label}>
77-
<WorkspaceScheduleLabel workspace={workspace} />
78-
{canUpdateWorkspace && shouldDisplayPlusMinus(workspace) && (
79-
<span className={styles.actions}>
80-
<IconButton
81-
className={styles.iconButton}
82-
size="small"
83-
disabled={!deadlineMinusEnabled()}
84-
onClick={onDeadlineMinus}
85-
>
86-
<Tooltip title={t("workspaceScheduleButton.editDeadlineMinus")}>
87-
<RemoveIcon />
88-
</Tooltip>
89-
</IconButton>
90-
<IconButton
91-
className={styles.iconButton}
92-
size="small"
93-
disabled={!deadlinePlusEnabled()}
94-
onClick={onDeadlinePlus}
95-
>
96-
<Tooltip title={t("workspaceScheduleButton.editDeadlinePlus")}>
97-
<AddIcon />
98-
</Tooltip>
99-
</IconButton>
100-
</span>
101-
)}
102-
</span>
103-
)}
97+
<Maybe condition={shouldDisplayScheduleLabel(workspace)}>
98+
<Stack
99+
className={styles.label}
100+
spacing={1}
101+
direction="row"
102+
alignItems="center"
103+
>
104+
<Stack spacing={1} direction="row" alignItems="center">
105+
<WorkspaceScheduleLabel workspace={workspace} />
106+
<Maybe condition={canUpdateWorkspace && canEditDeadline(workspace)}>
107+
<span className={styles.actions}>
108+
<IconButton
109+
className={styles.subtractButton}
110+
size="small"
111+
disabled={!deadlineMinusEnabled()}
112+
onClick={() => {
113+
setEditMode("subtract")
114+
}}
115+
>
116+
<Tooltip
117+
title={t("workspaceScheduleButton.editDeadlineMinus")}
118+
>
119+
<RemoveIcon />
120+
</Tooltip>
121+
</IconButton>
122+
<IconButton
123+
className={styles.addButton}
124+
size="small"
125+
disabled={!deadlinePlusEnabled()}
126+
onClick={() => {
127+
setEditMode("add")
128+
}}
129+
>
130+
<Tooltip
131+
title={t("workspaceScheduleButton.editDeadlinePlus")}
132+
>
133+
<AddIcon />
134+
</Tooltip>
135+
</IconButton>
136+
</span>
137+
</Maybe>
138+
</Stack>
139+
<Maybe
140+
condition={
141+
canUpdateWorkspace &&
142+
canEditDeadline(workspace) &&
143+
editMode !== "off"
144+
}
145+
>
146+
<EditHours
147+
handleSubmit={handleSubmitHours}
148+
max={
149+
editMode === "add" ? maxDeadlineIncrease : maxDeadlineDecrease
150+
}
151+
/>
152+
</Maybe>
153+
</Stack>
154+
</Maybe>
104155
<>
105156
<Button
106157
ref={anchorRef}
@@ -139,7 +190,11 @@ export const WorkspaceScheduleButton: React.FC<
139190
)
140191
}
141192

142-
const useStyles = makeStyles((theme) => ({
193+
interface StyleProps {
194+
editMode: EditMode
195+
}
196+
197+
const useStyles = makeStyles<Theme, StyleProps>((theme) => ({
143198
wrapper: {
144199
display: "inline-flex",
145200
alignItems: "center",
@@ -151,15 +206,13 @@ const useStyles = makeStyles((theme) => ({
151206
},
152207
},
153208
label: {
154-
borderRight: 0,
155-
padding: "0 8px 0 16px",
209+
padding: theme.spacing(0, 2),
156210
color: theme.palette.text.secondary,
157211

158212
[theme.breakpoints.down("sm")]: {
159213
width: "100%",
160-
display: "flex",
161-
alignItems: "center",
162214
padding: theme.spacing(1.5, 2),
215+
flexDirection: "column",
163216
},
164217
},
165218
actions: {
@@ -190,8 +243,19 @@ const useStyles = makeStyles((theme) => ({
190243
},
191244
},
192245
},
193-
iconButton: {
246+
addButton: {
247+
borderRadius: theme.shape.borderRadius,
248+
border: ({ editMode }) =>
249+
editMode === "add"
250+
? `2px solid ${theme.palette.primary.main}`
251+
: "2px solid transparent",
252+
},
253+
subtractButton: {
194254
borderRadius: theme.shape.borderRadius,
255+
border: ({ editMode }) =>
256+
editMode === "subtract"
257+
? `2px solid ${theme.palette.primary.main}`
258+
: "2px solid transparent",
195259
},
196260
popoverPaper: {
197261
padding: `${theme.spacing(2)}px ${theme.spacing(3)}px ${theme.spacing(

0 commit comments

Comments
 (0)