diff --git a/cypress/integration/issue-list.spec.ts b/cypress/integration/issue-list.spec.ts index b1b62ba..39a7092 100644 --- a/cypress/integration/issue-list.spec.ts +++ b/cypress/integration/issue-list.spec.ts @@ -132,6 +132,7 @@ describe("Issue List", () => { // type 'back', which is partial for "backend" cy.get("@filter-input").type("back"); + cy.wait(2000); cy.dataCy("filter-by-level").click(); // set level to warning @@ -154,38 +155,35 @@ describe("Issue List", () => { cy.validateIssues(mockIssuesByErrorLevel); }); - it("should update URL with the correct project filters/url parameters", () => { + it.only("should update URL with the correct project filters/url parameters", () => { // Click select component cy.dataCy("filter-by-status").click(); // Select Resolved cy.contains("Resolved").click(); - cy.url().should("include", "/dashboard/issues?page=1&status=resolved"); + cy.url().should("include", "/dashboard/issues?status=resolved"); cy.dataCy("filter-by-level").click(); // Error query param is added to the URL cy.contains("Error").click(); cy.url().should( "include", - "/dashboard/issues?page=1&status=resolved&level=error" + "/dashboard/issues?status=resolved&level=error" ); cy.dataCy("filter-by-status").click(); // Removes status filter cy.contains("--None--").click(); - cy.url().should("include", "/dashboard/issues?page=1&level=error"); + cy.url().should("include", "/dashboard/issues?level=error"); cy.dataCy("filter-by-level").click(); cy.contains("Warning").click(); - cy.url().should("include", "/issues?page=1&level=warning"); + cy.url().should("include", "/issues?level=warning"); cy.dataCy("filter-by-project").within(() => { cy.get("input").type("Back"); }); - cy.url().should( - "include", - "/issues?page=1&level=warning&project=backend" - ); + cy.url().should("include", "/issues?level=warning&project=backend"); - // Adds page 2 too the url + // Adds page 2 to the url cy.get("@next-button").click(); cy.url().should( "include", diff --git a/features/issues/api/use-issues.tsx b/features/issues/api/use-issues.tsx index 3a4a33b..c3bf95a 100644 --- a/features/issues/api/use-issues.tsx +++ b/features/issues/api/use-issues.tsx @@ -6,34 +6,57 @@ import type { Issue } from "../types/issue.types"; import { useFilters } from "@features/issues"; import { IssueFilters } from "@features/issues"; -async function getIssues(page: number, filters: IssueFilters) { - const { data } = await axios.get( - `https://prolog-api.profy.dev/issue?page=${page}`, - { params: filters } +export async function getIssues( + page: number, + filters: IssueFilters, + options?: { signal?: AbortSignal } +) { + const { data } = await axios.get>( + "https://prolog-api.profy.dev/issue", + { + params: { page, ...filters }, + signal: options?.signal, + } + ); + return data; +} + +export async function resolveIssue(issueId: string) { + const { data } = await axios.patch( + `https://prolog-api.profy.dev/issue/${issueId}`, + { + status: "resolved", + } ); return data; } +const QUERY_KEY = "issues"; + +export function getQueryKey(page?: number, filters?: IssueFilters) { + if (page === undefined) { + return [QUERY_KEY]; + } + return [QUERY_KEY, page, filters]; +} + export function useIssues(page: number) { const { filters } = useFilters(); - + // console.log('filters', filters); const query = useQuery, Error>( - ["issues", page, filters], - () => getIssues(page, filters), - { - keepPreviousData: true, - staleTime: 60000, - } + getQueryKey(page, filters), + ({ signal }) => getIssues(page, filters, { signal }), + { keepPreviousData: true } ); + // Prefetch the next page! const queryClient = useQueryClient(); useEffect(() => { if (query.data?.meta.hasNextPage) { - queryClient.prefetchQuery(["projects", page + 1, filters], () => - getIssues(page + 1, filters) + queryClient.prefetchQuery(getQueryKey(page + 1, filters), ({ signal }) => + getIssues(page + 1, filters, { signal }) ); } - }, [query.data, page, queryClient, filters]); - + }, [query.data, page, filters, queryClient]); return query; } diff --git a/features/issues/components/filters/filters.tsx b/features/issues/components/filters/filters.tsx index 07a0f26..44cd35b 100644 --- a/features/issues/components/filters/filters.tsx +++ b/features/issues/components/filters/filters.tsx @@ -1,41 +1,49 @@ -import React, { - useState, - useEffect, - useCallback, - useRef, - useContext, -} from "react"; +import React, { useState, useCallback } from "react"; import styled from "styled-components"; +import { capitalize } from "lodash"; + +import { Select, Option, Input, Button, IconOptions } from "@features/ui"; import { - Select, - Option, - Input, - Button, - IconOptions, - NavigationContext, -} from "@features/ui"; -import { useFilters, IssueLevel, IssueStatus } from "@features/issues"; -import { useProjects } from "@features/projects"; + useFilters, + IssueLevel, + IssueStatus, + IssueFilters, +} from "@features/issues"; +import { useDebouncedCallback } from "use-debounce"; + import { breakpoint } from "@styles/theme"; import { useWindowSize } from "react-use"; -import { useRouter } from "next/router"; - const Container = styled.div` display: flex; flex-direction: column; justify-content: center; margin-block: 1rem; + gap: 1rem; width: 100%; @media (min-width: ${breakpoint("desktop")}) { flex-direction: row; justify-content: space-between; order: initial; - gap: 1rem; + gap: 3rem; flex-wrap: wrap; } `; +export const FilterSelect = styled(Select)` + width: 100%; + @media (min-width: ${breakpoint("desktop")}) { + width: 10rem; + } +`; + +export const FilterInput = styled(Input)` + width: 100%; + @media (min-width: ${breakpoint("desktop")}) { + width: 17.5rem; + } +`; + const RightContainer = styled.div` margin-bottom: 1rem; display: flex; @@ -49,34 +57,33 @@ const RightContainer = styled.div` } `; +const getStatusDefaultValue = (filters: IssueFilters) => { + if (!filters.status) { + return "Status"; + } + if (filters.status === IssueStatus.open) { + return "Unresolved"; + } + return "Resolved"; +}; + +const getLevelDefaultValue = (filters: IssueFilters) => { + if (!filters.level) { + return "Level"; + } + return capitalize(filters.level); +}; + export function Filters() { const { handleFilters, filters } = useFilters(); - const { data: projects } = useProjects(); - const router = useRouter(); - const routerQueryProjectName = - (router.query.projectName as string)?.toLowerCase() || undefined; - const [inputValue, setInputValue] = useState(""); - const projectNames = projects?.map((project) => project.name.toLowerCase()); - const isFirst = useRef(true); + const debouncedHandleFilters = useDebouncedCallback(handleFilters, 300); + const [inputValue, setInputValue] = useState(filters.project || ""); const { width } = useWindowSize(); const isMobileScreen = width <= 1023; - const { isMobileMenuOpen } = useContext(NavigationContext); - - const handleChange = (input: string) => { - setInputValue(input); - - if (inputValue?.length < 2) { - handleProjectName(undefined); - return; - } - - const name = projectNames?.find((name) => - name?.toLowerCase().includes(inputValue.toLowerCase()) - ); - if (name) { - handleProjectName(name); - } + const handleChange = (project: string) => { + setInputValue(project); + debouncedHandleFilters({ project }); }; const handleLevel = (level?: string) => { @@ -96,64 +103,26 @@ export function Filters() { handleFilters({ status: status as IssueStatus }); }; - const handleProjectName = useCallback( - (projectName) => handleFilters({ project: projectName?.toLowerCase() }), - [handleFilters] - ); - - useEffect(() => { - const newObj: { [key: string]: string } = { - ...filters, - }; - - Object.keys(newObj).forEach((key) => { - if (newObj[key] === undefined) { - delete newObj[key]; - } - }); - - const url = { - pathname: router.pathname, - query: { - page: router.query.page || 1, - ...newObj, - }, - }; - - if (routerQueryProjectName && isFirst) { - handleProjectName(routerQueryProjectName); - setInputValue(routerQueryProjectName || ""); - isFirst.current = false; - } - - router.push(url, undefined, { shallow: false }); - }, [filters.level, filters.status, filters.project, router.query.page]); + // const handleProjectName = useCallback( + // (projectName) => handleFilters({ project: projectName?.toLowerCase() }), + // [handleFilters] + // ); return ( - + - + - diff --git a/features/issues/hooks/use-filters.tsx b/features/issues/hooks/use-filters.tsx index 8ebce02..2b6c5be 100644 --- a/features/issues/hooks/use-filters.tsx +++ b/features/issues/hooks/use-filters.tsx @@ -1,4 +1,19 @@ -import { useContext } from "react"; -import { FiltersContext } from "../context/filters-context"; +import { useRouter } from "next/router"; +import { IssueFilters } from "../types/issue.types"; -export const useFilters = () => useContext(FiltersContext); +export const useFilters = () => { + const router = useRouter(); + + const filters = { + status: router.query.status, + level: router.query.level, + project: router.query.project, + } as IssueFilters; + + const handleFilters = (newFilters: IssueFilters) => { + const query = { ...router.query, ...newFilters }; + router.push({ query }); + }; + + return { filters, handleFilters }; +}; diff --git a/features/ui/form/input/input.tsx b/features/ui/form/input/input.tsx index dad88cc..b33ebd7 100644 --- a/features/ui/form/input/input.tsx +++ b/features/ui/form/input/input.tsx @@ -31,6 +31,7 @@ const InputContainer = styled.input<{ border-radius: 7px; width: calc(${space(20)} * 4 - ${space(6)}); padding: ${space(2, 3)}; + box-sizing: border-box; letter-spacing: 0.05rem; color: ${color("gray", 900)}; ${textFont("md", "regular")}; diff --git a/features/ui/page-container/page-container.tsx b/features/ui/page-container/page-container.tsx index 71c5a91..2d79014 100644 --- a/features/ui/page-container/page-container.tsx +++ b/features/ui/page-container/page-container.tsx @@ -3,7 +3,6 @@ import Head from "next/head"; import styled from "styled-components"; import { SidebarNavigation, Footer } from "@features/ui"; import { color, displayFont, space, breakpoint, textFont } from "@styles/theme"; -import { FiltersProvider } from "@features/issues"; type PageContainerProps = { children: React.ReactNode; @@ -60,23 +59,21 @@ const Info = styled.div` export function PageContainer({ children, title, info }: PageContainerProps) { return ( - - - ProLog - {title} - - - + + ProLog - {title} + + + - -
- - {title} - {info} - {children} - -
-
-
+ +
+ + {title} + {info} + {children} + +
+
); } diff --git a/features/ui/sidebar-navigation/sidebar-navigation.tsx b/features/ui/sidebar-navigation/sidebar-navigation.tsx index b7816df..1f3db95 100644 --- a/features/ui/sidebar-navigation/sidebar-navigation.tsx +++ b/features/ui/sidebar-navigation/sidebar-navigation.tsx @@ -48,6 +48,7 @@ const Container = styled.div<{ isCollapsed: boolean }>` const FixedContainer = styled.div` ${containerStyles} position: fixed; + z-index: 1; `; const Header = styled.header` diff --git a/package-lock.json b/package-lock.json index 73ccddd..6b6117a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,8 @@ "react-query": "^3.34.19", "react-use": "^17.4.0", "styled-components": "^5.3.3", - "styled-normalize": "^8.0.7" + "styled-normalize": "^8.0.7", + "use-debounce": "^9.0.2" }, "devDependencies": { "@babel/core": "^7.17.5", @@ -25014,6 +25015,17 @@ "react": "^16.8.0 || ^17.0.0" } }, + "node_modules/use-debounce": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-9.0.2.tgz", + "integrity": "sha512-QLyB0sxt9F5AisGDrUybCRJSLE60bTQR0yXc+IebNGUu1GCXwii1zsZl82mPGdWqDVQy7+1FKMLHQUixxf5Nbw==", + "engines": { + "node": ">= 10.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/use-isomorphic-layout-effect": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.1.tgz", @@ -45627,6 +45639,12 @@ "dev": true, "requires": {} }, + "use-debounce": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-9.0.2.tgz", + "integrity": "sha512-QLyB0sxt9F5AisGDrUybCRJSLE60bTQR0yXc+IebNGUu1GCXwii1zsZl82mPGdWqDVQy7+1FKMLHQUixxf5Nbw==", + "requires": {} + }, "use-isomorphic-layout-effect": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.1.tgz", diff --git a/package.json b/package.json index 839cf7b..28f1d8d 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,8 @@ "react-query": "^3.34.19", "react-use": "^17.4.0", "styled-components": "^5.3.3", - "styled-normalize": "^8.0.7" + "styled-normalize": "^8.0.7", + "use-debounce": "^9.0.2" }, "devDependencies": { "@babel/core": "^7.17.5", diff --git a/pages/dashboard/issues.tsx b/pages/dashboard/issues.tsx index 0281713..73a91ce 100644 --- a/pages/dashboard/issues.tsx +++ b/pages/dashboard/issues.tsx @@ -1,18 +1,15 @@ import { PageContainer } from "@features/ui"; import { IssueList } from "@features/issues"; import type { NextPage } from "next"; -import { FiltersProvider } from "@features/issues"; const IssuesPage: NextPage = () => { return ( - - - - - + + + ); };