Skip to content

Commit df3c002

Browse files
committed
Add filters
1 parent 8ae036c commit df3c002

32 files changed

+526
-82
lines changed

.storybook/preview.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Story as StoryType } from "@storybook/react";
33
import { ThemeProvider } from "styled-components";
44
import { GlobalStyle } from "../styles/global-style";
55
import { theme } from "../styles/theme";
6-
import { NavigationProvider } from "../features/layout";
6+
import { NavigationProvider } from "../features/ui";
77
import { decorator as mockRouterDecorator } from "../__mocks__/next/router";
88

99
export const decorators = [

api/issues.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
import { axios } from "./axios";
2-
import type { Issue } from "./issues.types";
2+
import type { Issue, IssueFilters } from "./issues.types";
33
import type { Page } from "@typings/page.types";
44

55
const ENDPOINT = "/issue";
66

77
export async function getIssues(
88
page: number,
9+
filters: IssueFilters,
910
options?: { signal?: AbortSignal }
1011
) {
1112
const { data } = await axios.get<Page<Issue>>(ENDPOINT, {
12-
params: { page },
13+
params: { page, ...filters },
1314
signal: options?.signal,
1415
});
1516
return data;

api/issues.types.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
export enum IssueStatus {
2+
open = "open",
3+
resolved = "resolved",
4+
}
5+
16
export enum IssueLevel {
27
info = "info",
38
warning = "warning",
@@ -11,5 +16,12 @@ export type Issue = {
1116
message: string;
1217
stack: string;
1318
level: IssueLevel;
19+
status: IssueStatus;
1420
numEvents: number;
1521
};
22+
23+
export type IssueFilters = {
24+
level?: IssueLevel;
25+
status?: IssueStatus;
26+
project?: string;
27+
};

features/issues/api/use-get-issues.tsx

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,32 +2,34 @@ import { useEffect } from "react";
22
import { useQuery, useQueryClient } from "@tanstack/react-query";
33
import { getIssues } from "@api/issues";
44
import type { Page } from "@typings/page.types";
5-
import type { Issue } from "@api/issues.types";
5+
import type { Issue, IssueFilters } from "@api/issues.types";
6+
import { useFilters } from "../hooks/use-filters";
67

78
const QUERY_KEY = "issues";
89

9-
export function getQueryKey(page?: number) {
10+
export function getQueryKey(page?: number, filters?: IssueFilters) {
1011
if (page === undefined) {
1112
return [QUERY_KEY];
1213
}
13-
return [QUERY_KEY, page];
14+
return [QUERY_KEY, page, filters];
1415
}
1516

1617
export function useGetIssues(page: number) {
18+
const { filters } = useFilters();
1719
const query = useQuery<Page<Issue>, Error>(
18-
getQueryKey(page),
19-
({ signal }) => getIssues(page, { signal }),
20+
getQueryKey(page, filters),
21+
({ signal }) => getIssues(page, filters, { signal }),
2022
{ keepPreviousData: true }
2123
);
2224

2325
// Prefetch the next page!
2426
const queryClient = useQueryClient();
2527
useEffect(() => {
2628
if (query.data?.meta.hasNextPage) {
27-
queryClient.prefetchQuery(getQueryKey(page + 1), ({ signal }) =>
28-
getIssues(page + 1, { signal })
29+
queryClient.prefetchQuery(getQueryKey(page + 1, filters), ({ signal }) =>
30+
getIssues(page + 1, filters, { signal })
2931
);
3032
}
31-
}, [query.data, page, queryClient]);
33+
}, [query.data, page, filters, queryClient]);
3234
return query;
3335
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import styled from "styled-components";
2+
import { breakpoint } from "@styles/theme";
3+
4+
export const Container = styled.div`
5+
display: flex;
6+
flex-direction: column;
7+
justify-content: center;
8+
margin-block: 1rem;
9+
gap: 1rem;
10+
width: 100%;
11+
@media (min-width: ${breakpoint("desktop")}) {
12+
flex-direction: row;
13+
justify-content: flex-end;
14+
order: initial;
15+
gap: 3rem;
16+
flex-wrap: wrap;
17+
}
18+
`;
19+
20+
export const RightContainer = styled.div`
21+
margin-bottom: 1rem;
22+
display: flex;
23+
flex-direction: column;
24+
gap: 1rem;
25+
order: -1;
26+
@media (min-width: ${breakpoint("desktop")}) {
27+
flex-direction: row;
28+
gap: 3rem;
29+
order: initial;
30+
}
31+
`;
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import React, {
2+
useState,
3+
useEffect,
4+
useCallback,
5+
useRef,
6+
useContext,
7+
} from "react";
8+
import { useRouter } from "next/router";
9+
import { useWindowSize } from "react-use";
10+
import { Select, Option, Input, NavigationContext } from "@features/ui";
11+
import { useFilters } from "../../hooks/use-filters";
12+
import { IssueLevel, IssueStatus } from "@api/issues.types";
13+
import { useProjects } from "@features/projects";
14+
import * as S from "./filters.styled";
15+
16+
export function Filters() {
17+
const { handleFilters, filters } = useFilters();
18+
const { data: projects } = useProjects();
19+
const router = useRouter();
20+
const routerQueryProjectName =
21+
(router.query.projectName as string)?.toLowerCase() || undefined;
22+
const [inputValue, setInputValue] = useState<string>("");
23+
const projectNames = projects?.map((project) => project.name.toLowerCase());
24+
const isFirst = useRef(true);
25+
const { width } = useWindowSize();
26+
const isMobileScreen = width <= 1023;
27+
const { isMobileMenuOpen } = useContext(NavigationContext);
28+
const handleChange = (input: string) => {
29+
setInputValue(input);
30+
31+
if (inputValue?.length < 2) {
32+
handleProjectName(undefined);
33+
return;
34+
}
35+
36+
const name = projectNames?.find((name) =>
37+
name?.toLowerCase().includes(inputValue.toLowerCase())
38+
);
39+
40+
if (name) {
41+
handleProjectName(name);
42+
}
43+
};
44+
45+
const handleLevel = (level?: string) => {
46+
if (level) {
47+
level = level.toLowerCase();
48+
}
49+
handleFilters({ level: level as IssueLevel });
50+
};
51+
52+
const handleStatus = (status?: string) => {
53+
if (status === "Unresolved") {
54+
status = "open";
55+
}
56+
if (status) {
57+
status = status.toLowerCase();
58+
}
59+
handleFilters({ status: status as IssueStatus });
60+
};
61+
62+
const handleProjectName = useCallback(
63+
(projectName?: string) =>
64+
handleFilters({ project: projectName?.toLowerCase() }),
65+
[handleFilters]
66+
);
67+
68+
useEffect(() => {
69+
const newObj: { [key: string]: string } = {
70+
...filters,
71+
};
72+
73+
Object.keys(newObj).forEach((key) => {
74+
if (newObj[key] === undefined) {
75+
delete newObj[key];
76+
}
77+
});
78+
79+
const url = {
80+
pathname: router.pathname,
81+
query: {
82+
page: router.query.page || 1,
83+
...newObj,
84+
},
85+
};
86+
87+
if (routerQueryProjectName && isFirst) {
88+
handleProjectName(routerQueryProjectName);
89+
setInputValue(routerQueryProjectName || "");
90+
isFirst.current = false;
91+
}
92+
93+
router.push(url, undefined, { shallow: false });
94+
}, [filters.level, filters.status, filters.project, router.query.page]);
95+
96+
return (
97+
<S.Container>
98+
<Select
99+
placeholder="Status"
100+
defaultValue="Status"
101+
width={isMobileScreen ? "97%" : "8rem"}
102+
data-cy="filter-by-status"
103+
style={{
104+
...(isMobileMenuOpen && {
105+
opacity: 0,
106+
}),
107+
}}
108+
>
109+
<Option value={undefined} handleCallback={handleStatus}>
110+
--None--
111+
</Option>
112+
<Option value="Unresolved" handleCallback={handleStatus}>
113+
Unresolved
114+
</Option>
115+
<Option value="Resolved" handleCallback={handleStatus}>
116+
Resolved
117+
</Option>
118+
</Select>
119+
120+
<Select
121+
placeholder="Level"
122+
defaultValue="Level"
123+
width={isMobileScreen ? "97%" : "8rem"}
124+
data-cy="filter-by-level"
125+
style={{
126+
...(isMobileMenuOpen && {
127+
opacity: 0,
128+
}),
129+
}}
130+
>
131+
<Option value={undefined} handleCallback={handleLevel}>
132+
--None--
133+
</Option>
134+
<Option value="Error" handleCallback={handleLevel}>
135+
Error
136+
</Option>
137+
<Option value="Warning" handleCallback={handleLevel}>
138+
Warning
139+
</Option>
140+
<Option value="Info" handleCallback={handleLevel}>
141+
Info
142+
</Option>
143+
</Select>
144+
145+
<Input
146+
handleChange={handleChange}
147+
value={inputValue}
148+
label="project name"
149+
placeholder="Project Name"
150+
iconSrc="/icons/search-icon.svg"
151+
data-cy="filter-by-project"
152+
style={{
153+
...(isMobileScreen && { width: "94%", marginRight: "3rem" }),
154+
...(isMobileMenuOpen && {
155+
opacity: 0,
156+
}),
157+
}}
158+
/>
159+
</S.Container>
160+
);
161+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from "./filters";

features/issues/components/issue-list/issue-list.tsx

Lines changed: 45 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { ProjectLanguage } from "@api/projects.types";
55
import { useProjects } from "@features/projects";
66
import { useGetIssues } from "../../api";
77
import { IssueRow } from "./issue-row";
8+
import { Filters } from "../filters";
89

910
const Container = styled.div`
1011
background: white;
@@ -98,46 +99,49 @@ export function IssueList() {
9899
const { items, meta } = issuesPage.data || {};
99100

100101
return (
101-
<Container>
102-
<Table>
103-
<thead>
104-
<HeaderRow>
105-
<HeaderCell>Issue</HeaderCell>
106-
<HeaderCell>Level</HeaderCell>
107-
<HeaderCell>Events</HeaderCell>
108-
<HeaderCell>Users</HeaderCell>
109-
</HeaderRow>
110-
</thead>
111-
<tbody>
112-
{(items || []).map((issue) => (
113-
<IssueRow
114-
key={issue.id}
115-
issue={issue}
116-
projectLanguage={projectIdToLanguage[issue.projectId]}
117-
/>
118-
))}
119-
</tbody>
120-
</Table>
121-
<PaginationContainer>
122-
<div>
123-
<PaginationButton
124-
onClick={() => navigateToPage(page - 1)}
125-
disabled={page === 1}
126-
>
127-
Previous
128-
</PaginationButton>
129-
<PaginationButton
130-
onClick={() => navigateToPage(page + 1)}
131-
disabled={page === meta?.totalPages}
132-
>
133-
Next
134-
</PaginationButton>
135-
</div>
136-
<PageInfo>
137-
Page <PageNumber>{meta?.currentPage}</PageNumber> of{" "}
138-
<PageNumber>{meta?.totalPages}</PageNumber>
139-
</PageInfo>
140-
</PaginationContainer>
141-
</Container>
102+
<>
103+
<Filters />
104+
<Container>
105+
<Table>
106+
<thead>
107+
<HeaderRow>
108+
<HeaderCell>Issue</HeaderCell>
109+
<HeaderCell>Level</HeaderCell>
110+
<HeaderCell>Events</HeaderCell>
111+
<HeaderCell>Users</HeaderCell>
112+
</HeaderRow>
113+
</thead>
114+
<tbody>
115+
{(items || []).map((issue) => (
116+
<IssueRow
117+
key={issue.id}
118+
issue={issue}
119+
projectLanguage={projectIdToLanguage[issue.projectId]}
120+
/>
121+
))}
122+
</tbody>
123+
</Table>
124+
<PaginationContainer>
125+
<div>
126+
<PaginationButton
127+
onClick={() => navigateToPage(page - 1)}
128+
disabled={page === 1}
129+
>
130+
Previous
131+
</PaginationButton>
132+
<PaginationButton
133+
onClick={() => navigateToPage(page + 1)}
134+
disabled={page === meta?.totalPages}
135+
>
136+
Next
137+
</PaginationButton>
138+
</div>
139+
<PageInfo>
140+
Page <PageNumber>{meta?.currentPage}</PageNumber> of{" "}
141+
<PageNumber>{meta?.totalPages}</PageNumber>
142+
</PageInfo>
143+
</PaginationContainer>
144+
</Container>
145+
</>
142146
);
143147
}

0 commit comments

Comments
 (0)