Skip to content

Commit 9c8079b

Browse files
authored
refactor: Extract workspace filter into a separate component (#2601)
1 parent 929227d commit 9c8079b

File tree

3 files changed

+172
-113
lines changed

3 files changed

+172
-113
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { ComponentMeta, Story } from "@storybook/react"
2+
import { workspaceFilterQuery } from "../../util/workspace"
3+
import { SearchBarWithFilter, SearchBarWithFilterProps } from "./SearchBarWithFilter"
4+
5+
export default {
6+
title: "components/SearchBarWithFilter",
7+
component: SearchBarWithFilter,
8+
argTypes: {
9+
filter: {
10+
defaultValue: workspaceFilterQuery.me,
11+
},
12+
},
13+
} as ComponentMeta<typeof SearchBarWithFilter>
14+
15+
const Template: Story<SearchBarWithFilterProps> = (args) => <SearchBarWithFilter {...args} />
16+
17+
export const WithoutPresetFilters = Template.bind({})
18+
19+
export const WithPresetFilters = Template.bind({})
20+
WithPresetFilters.args = {
21+
presetFilters: [
22+
{ query: workspaceFilterQuery.me, name: "Your workspaces" },
23+
{ query: "random query", name: "Random query" },
24+
],
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import Button from "@material-ui/core/Button"
2+
import Fade from "@material-ui/core/Fade"
3+
import InputAdornment from "@material-ui/core/InputAdornment"
4+
import Menu from "@material-ui/core/Menu"
5+
import MenuItem from "@material-ui/core/MenuItem"
6+
import { makeStyles } from "@material-ui/core/styles"
7+
import TextField from "@material-ui/core/TextField"
8+
import SearchIcon from "@material-ui/icons/Search"
9+
import { FormikErrors, useFormik } from "formik"
10+
import { useState } from "react"
11+
import { getFormHelpers, onChangeTrimmed } from "../../util/formUtils"
12+
import { CloseDropdown, OpenDropdown } from "../DropdownArrows/DropdownArrows"
13+
import { Stack } from "../Stack/Stack"
14+
15+
export const Language = {
16+
filterName: "Filters",
17+
}
18+
19+
export interface SearchBarWithFilterProps {
20+
filter?: string
21+
onFilter: (query: string) => void
22+
presetFilters?: PresetFilter[]
23+
}
24+
25+
export interface PresetFilter {
26+
name: string
27+
query: string
28+
}
29+
30+
interface FilterFormValues {
31+
query: string
32+
}
33+
34+
export type FilterFormErrors = FormikErrors<FilterFormValues>
35+
36+
export const SearchBarWithFilter: React.FC<SearchBarWithFilterProps> = ({ filter, onFilter, presetFilters }) => {
37+
const styles = useStyles()
38+
39+
const form = useFormik<FilterFormValues>({
40+
enableReinitialize: true,
41+
initialValues: {
42+
query: filter ?? "",
43+
},
44+
onSubmit: ({ query }) => {
45+
onFilter(query)
46+
},
47+
})
48+
49+
const getFieldHelpers = getFormHelpers<FilterFormValues>(form)
50+
51+
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null)
52+
53+
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
54+
setAnchorEl(event.currentTarget)
55+
}
56+
57+
const handleClose = () => {
58+
setAnchorEl(null)
59+
}
60+
61+
const setPresetFilter = (query: string) => () => {
62+
void form.setFieldValue("query", query)
63+
void form.submitForm()
64+
handleClose()
65+
}
66+
67+
return (
68+
<Stack direction="row" spacing={0} className={styles.filterContainer}>
69+
{presetFilters && presetFilters.length > 0 && (
70+
<Button aria-controls="filter-menu" aria-haspopup="true" onClick={handleClick} className={styles.buttonRoot}>
71+
{Language.filterName} {anchorEl ? <CloseDropdown /> : <OpenDropdown />}
72+
</Button>
73+
)}
74+
75+
<form onSubmit={form.handleSubmit} className={styles.filterForm}>
76+
<TextField
77+
{...getFieldHelpers("query")}
78+
className={styles.textFieldRoot}
79+
onChange={onChangeTrimmed(form)}
80+
fullWidth
81+
variant="outlined"
82+
InputProps={{
83+
startAdornment: (
84+
<InputAdornment position="start">
85+
<SearchIcon fontSize="small" />
86+
</InputAdornment>
87+
),
88+
}}
89+
/>
90+
</form>
91+
92+
{presetFilters && presetFilters.length > 0 && (
93+
<Menu
94+
id="filter-menu"
95+
anchorEl={anchorEl}
96+
keepMounted
97+
open={Boolean(anchorEl)}
98+
onClose={handleClose}
99+
TransitionComponent={Fade}
100+
anchorOrigin={{
101+
vertical: "bottom",
102+
horizontal: "left",
103+
}}
104+
transformOrigin={{
105+
vertical: "top",
106+
horizontal: "left",
107+
}}
108+
>
109+
{presetFilters.map((presetFilter) => (
110+
<MenuItem key={presetFilter.name} onClick={setPresetFilter(presetFilter.query)}>
111+
{presetFilter.name}
112+
</MenuItem>
113+
))}
114+
</Menu>
115+
)}
116+
</Stack>
117+
)
118+
}
119+
120+
const useStyles = makeStyles((theme) => ({
121+
filterContainer: {
122+
border: `1px solid ${theme.palette.divider}`,
123+
borderRadius: theme.shape.borderRadius,
124+
marginBottom: theme.spacing(2),
125+
},
126+
filterForm: {
127+
width: "100%",
128+
},
129+
buttonRoot: {
130+
border: "none",
131+
borderRight: `1px solid ${theme.palette.divider}`,
132+
borderRadius: `${theme.shape.borderRadius}px 0px 0px ${theme.shape.borderRadius}px`,
133+
},
134+
textFieldRoot: {
135+
margin: "0px",
136+
"& fieldset": {
137+
border: "none",
138+
},
139+
},
140+
}))

site/src/pages/WorkspacesPage/WorkspacesPageView.tsx

+7-113
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,25 @@
11
import Button from "@material-ui/core/Button"
2-
import Fade from "@material-ui/core/Fade"
3-
import InputAdornment from "@material-ui/core/InputAdornment"
42
import Link from "@material-ui/core/Link"
5-
import Menu from "@material-ui/core/Menu"
6-
import MenuItem from "@material-ui/core/MenuItem"
73
import { fade, makeStyles, Theme } from "@material-ui/core/styles"
84
import Table from "@material-ui/core/Table"
95
import TableBody from "@material-ui/core/TableBody"
106
import TableCell from "@material-ui/core/TableCell"
117
import TableHead from "@material-ui/core/TableHead"
128
import TableRow from "@material-ui/core/TableRow"
13-
import TextField from "@material-ui/core/TextField"
149
import AddCircleOutline from "@material-ui/icons/AddCircleOutline"
1510
import KeyboardArrowRight from "@material-ui/icons/KeyboardArrowRight"
1611
import RefreshIcon from "@material-ui/icons/Refresh"
17-
import SearchIcon from "@material-ui/icons/Search"
1812
import useTheme from "@material-ui/styles/useTheme"
1913
import { useActor } from "@xstate/react"
2014
import dayjs from "dayjs"
2115
import relativeTime from "dayjs/plugin/relativeTime"
22-
import { FormikErrors, useFormik } from "formik"
23-
import { FC, useState } from "react"
16+
import { FC } from "react"
2417
import { Link as RouterLink, useNavigate } from "react-router-dom"
2518
import { AvatarData } from "../../components/AvatarData/AvatarData"
26-
import { CloseDropdown, OpenDropdown } from "../../components/DropdownArrows/DropdownArrows"
2719
import { EmptyState } from "../../components/EmptyState/EmptyState"
2820
import { Margins } from "../../components/Margins/Margins"
2921
import { PageHeader, PageHeaderSubtitle, PageHeaderTitle } from "../../components/PageHeader/PageHeader"
22+
import { SearchBarWithFilter } from "../../components/SearchBarWithFilter/SearchBarWithFilter"
3023
import { Stack } from "../../components/Stack/Stack"
3124
import { TableCellLink } from "../../components/TableCellLink/TableCellLink"
3225
import { TableLoader } from "../../components/TableLoader/TableLoader"
@@ -38,7 +31,6 @@ import {
3831
HelpTooltipText,
3932
HelpTooltipTitle,
4033
} from "../../components/Tooltips/HelpTooltip/HelpTooltip"
41-
import { getFormHelpers, onChangeTrimmed } from "../../util/formUtils"
4234
import { getDisplayStatus, workspaceFilterQuery } from "../../util/workspace"
4335
import { WorkspaceItemMachineRef } from "../../xServices/workspaces/workspacesXService"
4436

@@ -49,7 +41,6 @@ export const Language = {
4941
emptyCreateWorkspaceMessage: "Create your first workspace",
5042
emptyCreateWorkspaceDescription: "Start editing your source code and building your software",
5143
emptyResultsMessage: "No results matched your search",
52-
filterName: "Filters",
5344
yourWorkspacesButton: "Your workspaces",
5445
allWorkspacesButton: "All workspaces",
5546
workspaceTooltipTitle: "What is a workspace?",
@@ -154,12 +145,6 @@ const WorkspaceRow: React.FC<{ workspaceRef: WorkspaceItemMachineRef }> = ({ wor
154145
)
155146
}
156147

157-
interface FilterFormValues {
158-
query: string
159-
}
160-
161-
export type FilterFormErrors = FormikErrors<FilterFormValues>
162-
163148
export interface WorkspacesPageViewProps {
164149
loading?: boolean
165150
workspaceRefs?: WorkspaceItemMachineRef[]
@@ -168,41 +153,10 @@ export interface WorkspacesPageViewProps {
168153
}
169154

170155
export const WorkspacesPageView: FC<WorkspacesPageViewProps> = ({ loading, workspaceRefs, filter, onFilter }) => {
171-
const styles = useStyles()
172-
173-
const form = useFormik<FilterFormValues>({
174-
enableReinitialize: true,
175-
initialValues: {
176-
query: filter ?? "",
177-
},
178-
onSubmit: ({ query }) => {
179-
onFilter(query)
180-
},
181-
})
182-
183-
const getFieldHelpers = getFormHelpers<FilterFormValues>(form)
184-
185-
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null)
186-
187-
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
188-
setAnchorEl(event.currentTarget)
189-
}
190-
191-
const handleClose = () => {
192-
setAnchorEl(null)
193-
}
194-
195-
const setYourWorkspaces = () => {
196-
void form.setFieldValue("query", "owner:me")
197-
void form.submitForm()
198-
handleClose()
199-
}
200-
201-
const setAllWorkspaces = () => {
202-
void form.setFieldValue("query", "")
203-
void form.submitForm()
204-
handleClose()
205-
}
156+
const presetFilters = [
157+
{ query: workspaceFilterQuery.me, name: Language.yourWorkspacesButton },
158+
{ query: workspaceFilterQuery.all, name: Language.allWorkspacesButton },
159+
]
206160

207161
return (
208162
<Margins>
@@ -223,48 +177,7 @@ export const WorkspacesPageView: FC<WorkspacesPageViewProps> = ({ loading, works
223177
</PageHeaderSubtitle>
224178
</PageHeader>
225179

226-
<Stack direction="row" spacing={0} className={styles.filterContainer}>
227-
<Button aria-controls="filter-menu" aria-haspopup="true" onClick={handleClick} className={styles.buttonRoot}>
228-
{Language.filterName} {anchorEl ? <CloseDropdown /> : <OpenDropdown />}
229-
</Button>
230-
231-
<form onSubmit={form.handleSubmit} className={styles.filterForm}>
232-
<TextField
233-
{...getFieldHelpers("query")}
234-
className={styles.textFieldRoot}
235-
onChange={onChangeTrimmed(form)}
236-
fullWidth
237-
variant="outlined"
238-
InputProps={{
239-
startAdornment: (
240-
<InputAdornment position="start">
241-
<SearchIcon fontSize="small" />
242-
</InputAdornment>
243-
),
244-
}}
245-
/>
246-
</form>
247-
248-
<Menu
249-
id="filter-menu"
250-
anchorEl={anchorEl}
251-
keepMounted
252-
open={Boolean(anchorEl)}
253-
onClose={handleClose}
254-
TransitionComponent={Fade}
255-
anchorOrigin={{
256-
vertical: "bottom",
257-
horizontal: "left",
258-
}}
259-
transformOrigin={{
260-
vertical: "top",
261-
horizontal: "left",
262-
}}
263-
>
264-
<MenuItem onClick={setYourWorkspaces}>{Language.yourWorkspacesButton}</MenuItem>
265-
<MenuItem onClick={setAllWorkspaces}>{Language.allWorkspacesButton}</MenuItem>
266-
</Menu>
267-
</Stack>
180+
<SearchBarWithFilter filter={filter} onFilter={onFilter} presetFilters={presetFilters} />
268181

269182
<Table>
270183
<TableHead>
@@ -328,25 +241,6 @@ const useStyles = makeStyles((theme) => ({
328241
lineHeight: `${theme.spacing(3)}px`,
329242
},
330243
},
331-
filterContainer: {
332-
border: `1px solid ${theme.palette.divider}`,
333-
borderRadius: theme.shape.borderRadius,
334-
marginBottom: theme.spacing(2),
335-
},
336-
filterForm: {
337-
width: "100%",
338-
},
339-
buttonRoot: {
340-
border: "none",
341-
borderRight: `1px solid ${theme.palette.divider}`,
342-
borderRadius: `${theme.shape.borderRadius}px 0px 0px ${theme.shape.borderRadius}px`,
343-
},
344-
textFieldRoot: {
345-
margin: "0px",
346-
"& fieldset": {
347-
border: "none",
348-
},
349-
},
350244
clickableTableRow: {
351245
"&:hover td": {
352246
backgroundColor: fade(theme.palette.primary.light, 0.1),

0 commit comments

Comments
 (0)