Skip to content

Commit 29f531f

Browse files
authored
feat: implemented improved filter Github Issues by label (#374)
* feature #319: implemented filter option by clicking on the label, the possibility of adding new label in filter * feature #319: implemented filtered labels resistance from refreshing page * implemented test for GhFinder components * added remove svg for selected labels * selected label which is not in filter-list is added automatically
1 parent b8dee64 commit 29f531f

File tree

4 files changed

+359
-55
lines changed

4 files changed

+359
-55
lines changed

__tests__/gh-finder/page.test.jsx

+103
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import React from "react";
2+
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
3+
import LabelButton from "@/app/gh-finder/label";
4+
import Pagination from "@/app/gh-finder/pagination";
5+
import BasicModal from "@/app/gh-finder/modal";
6+
7+
describe("Label component", () => {
8+
test("Check Label name", () => {
9+
render(
10+
<LabelButton
11+
key={1}
12+
labelId={1}
13+
name={"Test github label"}
14+
bgColor={"blue"}
15+
isDarkColor={() => {}}
16+
handleSelectedLabel={() => {}}
17+
/>,
18+
);
19+
20+
const findText = screen.getByText("Test github label");
21+
expect(findText).toBeInTheDocument();
22+
});
23+
24+
test("Check Label with random color", () => {
25+
const randomColor = "#123456";
26+
render(
27+
<LabelButton
28+
key={2}
29+
labelId={2}
30+
name={"Random Color Label"}
31+
bgColor={"123456"}
32+
isDarkColor={() => {}}
33+
handleSelectedLabel={() => {}}
34+
/>,
35+
);
36+
37+
const findText = screen.getByText("Random Color Label");
38+
39+
expect(findText).toBeInTheDocument();
40+
expect(findText).toHaveStyle(`background-color: ${randomColor}`);
41+
});
42+
});
43+
44+
describe("Pagination component", () => {
45+
test("Check if Pagination exist", () => {
46+
render(
47+
<Pagination
48+
currentPage={1}
49+
setCurrentPage={() => {}}
50+
resultsPerPage={10}
51+
totalResults={50}
52+
/>,
53+
);
54+
55+
const pagination = screen.getByRole("navigation");
56+
expect(pagination).toBeInTheDocument();
57+
});
58+
59+
test("Check if Pagination have 5 tabs", () => {
60+
render(
61+
<Pagination
62+
currentPage={1}
63+
setCurrentPage={() => {}}
64+
resultsPerPage={10}
65+
totalResults={50}
66+
/>,
67+
);
68+
const paginationTabs = screen.getAllByRole("button");
69+
expect(paginationTabs).toHaveLength(5);
70+
});
71+
});
72+
73+
describe("Modal component", () => {
74+
test("Check if Modal pops up and the correct checkboxes are checked", () => {
75+
const selectedLabels = ["enhancement", "Feature"];
76+
77+
render(
78+
<BasicModal
79+
isDarkMode={false}
80+
selectedLabels={selectedLabels}
81+
setSelectedLabels={() => {}}
82+
handleParametersURL={() => {}}
83+
/>,
84+
);
85+
86+
// simulate clicking the button that opens the modal
87+
const openModalButton = screen.getByRole("button", { name: /filter/i });
88+
fireEvent.click(openModalButton);
89+
90+
waitFor(() => {
91+
expect(screen.getByRole("dialog")).toBeInTheDocument();
92+
});
93+
94+
// check the boxes
95+
const enhancementCheckbox = screen.getByRole("checkbox", {
96+
name: /enhancement/i,
97+
});
98+
const featureCheckbox = screen.getByRole("checkbox", { name: /feature/i });
99+
100+
expect(enhancementCheckbox).toBeChecked();
101+
expect(featureCheckbox).toBeChecked();
102+
});
103+
});

src/app/gh-finder/label.jsx

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import React from "react";
2+
3+
export default function LabelButton(props) {
4+
return (
5+
<React.Fragment>
6+
<button
7+
onClick={() =>
8+
props.handleSelectedLabel({
9+
id: props.labelId,
10+
name: props.name,
11+
color: props.bgColor,
12+
})
13+
}
14+
className="inline-block px-2 py-1 text-xs p-9 font-semibold text-white rounded-full truncate
15+
hover:scale-105 hover:shadow-lg"
16+
style={{
17+
backgroundColor: `#${props.bgColor}`,
18+
color: props.isDarkColor(`#${props.bgColor}`) ? "white" : "black",
19+
}}
20+
>
21+
{props.name}
22+
{props.isRemovable && (
23+
<svg
24+
xmlns="http://www.w3.org/2000/svg"
25+
fill="none"
26+
viewBox="0 0 24 24"
27+
strokeWidth="2"
28+
stroke="currentColor"
29+
className="h-4 w-4 text-black font-bold ml-1 cursor-pointer inline-flex rounded-full"
30+
>
31+
<path
32+
strokeLinecap="round"
33+
strokeLinejoin="round"
34+
strokeWidth="3"
35+
d="M6 18L18 6M6 6l12 12"
36+
/>
37+
</svg>
38+
)}
39+
</button>
40+
</React.Fragment>
41+
);
42+
}

src/app/gh-finder/modal.jsx

+71-16
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import * as React from "react";
1+
import React, { useState, useEffect } from "react";
22
import Box from "@mui/material/Box";
33
import Button from "@mui/material/Button";
44
import Modal from "@mui/material/Modal";
@@ -20,14 +20,15 @@ const style = {
2020
};
2121

2222
export default function BasicModal({
23+
isDarkMode,
2324
selectedLabels,
2425
setSelectedLabels,
25-
isDarkMode,
26+
handleParametersURL,
2627
}) {
2728
const handleClose = () => setOpen(false);
28-
const [open, setOpen] = React.useState(false);
2929
const handleOpen = () => setOpen(true);
30-
const [labelsList, setLabelsList] = React.useState([
30+
const [open, setOpen] = useState(false);
31+
const [labelsList, setLabelsList] = useState([
3132
"bug",
3233
"documentation",
3334
"Eddiehub:good-first-issue",
@@ -36,26 +37,50 @@ export default function BasicModal({
3637
"good first issue",
3738
"help wanted",
3839
]);
40+
const [filterInput, setFilterInput] = useState("");
3941

40-
const handleChange = (label, event) => {
41-
const newSelectedLabels = [...selectedLabels];
42-
const index = newSelectedLabels.indexOf(label);
43-
if (index === -1) {
44-
newSelectedLabels.push(label);
42+
useEffect(() => {
43+
setLabelsList([...new Set([...labelsList, ...selectedLabels])]);
44+
}, [open]);
45+
46+
const handleChange = (inputLabel, event) => {
47+
if (!selectedLabels.includes(inputLabel)) {
48+
setSelectedLabels((prevSelectedLabels) => {
49+
const newSelectedLabels = [...prevSelectedLabels, inputLabel];
50+
handleParametersURL(newSelectedLabels, true);
51+
return newSelectedLabels;
52+
});
4553
} else {
46-
newSelectedLabels.splice(index, 1);
54+
setSelectedLabels((prevSelectedLabels) => {
55+
const newSelectedLabels = prevSelectedLabels.filter(
56+
(label) => label !== inputLabel,
57+
);
58+
handleParametersURL(newSelectedLabels, false);
59+
return newSelectedLabels;
60+
});
4761
}
48-
setSelectedLabels(newSelectedLabels);
4962
};
5063

5164
const handleClear = () => {
5265
setSelectedLabels([]);
5366
setOpen(false);
67+
handleParametersURL([], false);
68+
};
69+
70+
const handleFilterInput = () => {
71+
if (filterInput.length <= 1 || labelsList.includes(filterInput)) {
72+
setFilterInput("");
73+
} else {
74+
const newLabelsList = [...labelsList, filterInput];
75+
setLabelsList(newLabelsList);
76+
77+
setFilterInput("");
78+
}
5479
};
5580

5681
return (
5782
<div>
58-
<Button className="p-2rounded-full">
83+
<Button className="p-2rounded-full space-x-1" onClick={handleOpen}>
5984
<svg
6085
xmlns="http://www.w3.org/2000/svg"
6186
fill="none"
@@ -70,23 +95,45 @@ export default function BasicModal({
7095
d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2a1 1 0 01-.293.707L14 12.414V18a1 1 0 01-.553.894l-4 2A1 1 0 018 20v-7.586L3.293 6.707A1 1 0 013 6V4z"
7196
/>
7297
</svg>
73-
<Button onClick={handleOpen} className="sm:!text-sm !text-[12px]">
74-
Filter
75-
</Button>
98+
<span className="sm:!text-sm !text-[12px]">Filter</span>
7699
</Button>
77100
<Modal
78101
open={open}
79102
onClose={handleClose}
80103
aria-labelledby="modal-modal-title"
81104
aria-describedby="modal-modal-description"
105+
role="dialog"
82106
>
83107
<Box
84108
sx={style}
85-
className={`sm:!h-[300px] ${
109+
className={`sm:!h-[300px] !overflow-y-auto ${
86110
isDarkMode ? "bg-slate-800 text-white" : "bg-slate-200 text-black"
87111
}`}
88112
>
89113
<FormGroup className="!grid sm:grid-cols-2">
114+
<input
115+
type="text"
116+
className={`
117+
w-full md:col-span-2
118+
p-1
119+
rounded-lg
120+
border border-gray-300
121+
focus:ring-2 focus:ring-blue-500
122+
focus:outline-none
123+
transition duration-200 ease-in-out
124+
text-gray-700
125+
placeholder-gray-400
126+
shadow-sm
127+
hover:shadow-md
128+
${
129+
isDarkMode
130+
? "bg-gray-800 text-gray-400"
131+
: "bg-gray-200 text-gray-500"
132+
} `}
133+
placeholder="Search issues"
134+
value={filterInput}
135+
onChange={(e) => setFilterInput(e.target.value)}
136+
/>
90137
{labelsList.map((label, idx) => (
91138
<FormControlLabel
92139
key={idx}
@@ -100,6 +147,14 @@ export default function BasicModal({
100147
label={label}
101148
/>
102149
))}
150+
<button
151+
className="inline-block px-2 py-2 h-10 w-32 text-l font-medium text-white bg-green-500 rounded-full
152+
truncate hover:scale-105 hover:shadow-lg"
153+
style={{ backgroundColor: "#164a6e" }}
154+
onClick={() => handleFilterInput()}
155+
>
156+
Submit
157+
</button>
103158
</FormGroup>
104159
<Button
105160
onClick={handleClear}

0 commit comments

Comments
 (0)