Skip to content
This repository has been archived by the owner on Dec 18, 2024. It is now read-only.

Filter resources by topic #32

Closed
wants to merge 64 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
bd3e4c5
add topic filter
alinosratipour Jan 16, 2024
ba725b7
Tweak
alinosratipour Jan 17, 2024
127dd63
add className prop to select
alinosratipour Jan 17, 2024
1ce9c43
filter topic in all pages
alinosratipour Jan 18, 2024
7d622f2
Merge branch 'main' into FilterTopic
alinosratipour Jan 18, 2024
b5ea4b4
tweak
alinosratipour Jan 18, 2024
4d54098
format
alinosratipour Jan 18, 2024
3ab2919
Tweak
alinosratipour Jan 19, 2024
e61bcc4
changed test
alinosratipour Jan 19, 2024
9757d3c
removed unwanted changes
alinosratipour Jan 19, 2024
916d4a9
draft commit
alinosratipour Jan 20, 2024
2b2255f
fix test
alinosratipour Jan 22, 2024
edc2096
sort code format
alinosratipour Jan 22, 2024
b3d28ac
refactor
alinosratipour Jan 22, 2024
5f36e6d
add error handling
alinosratipour Jan 22, 2024
71fbf4f
Tweak
alinosratipour Jan 22, 2024
d39435e
revert change
alinosratipour Jan 23, 2024
821ec98
fix console error
alinosratipour Jan 23, 2024
b356b32
change in the way we handle topics
alinosratipour Jan 23, 2024
63ca74e
draft
alinosratipour Jan 26, 2024
aa2f9fb
removed unused code
alinosratipour Jan 26, 2024
23b73e1
pushed this to see how i will work in CI
alinosratipour Jan 27, 2024
92ab284
add settopcics
alinosratipour Jan 27, 2024
42e0923
address pr commnets
alinosratipour Feb 21, 2024
90d78d3
change css
alinosratipour Feb 22, 2024
6e1e63a
addressed pr
alinosratipour Feb 22, 2024
f725682
refactor
alinosratipour Feb 23, 2024
9a4e54b
remove commnet
alinosratipour Feb 23, 2024
5b02254
tweak
alinosratipour Feb 23, 2024
fb897da
tweak
alinosratipour Feb 24, 2024
6cd4325
wrote unit test for new feature filter resouce by topics
alinosratipour Feb 27, 2024
002b989
removed unused prop
alinosratipour Feb 29, 2024
f37172b
removed unused prop
alinosratipour Feb 29, 2024
77c0942
created a new component for the draft list
alinosratipour Feb 29, 2024
a2cf306
add new query to fetch all resources
alinosratipour Mar 4, 2024
23e6a69
renamed variable
alinosratipour Mar 4, 2024
7d5d0c9
refactored finction
alinosratipour Mar 4, 2024
ea0097c
made filter resource correspond with the number of filtered resources
alinosratipour Mar 5, 2024
c363fce
Tweak
alinosratipour Mar 5, 2024
f7ce556
add new hooks
alinosratipour Mar 6, 2024
94167bf
refacor code add some more hooks
alinosratipour Mar 6, 2024
73f1253
refactor hooks
alinosratipour Mar 7, 2024
20d072f
updated resourceList test
alinosratipour Mar 7, 2024
e7c20ca
add test displays only resources with the selected topic
alinosratipour Mar 7, 2024
42cbfc5
made small change to resource list to handle undifend
alinosratipour Mar 7, 2024
b02a0b5
removed unusfule commnets
alinosratipour Mar 7, 2024
3e73063
add new test to display resource
alinosratipour Mar 8, 2024
4d20a7c
removed Home test since there are no more logic exist in there
alinosratipour Mar 8, 2024
b838cdd
changed the way we query allResource
alinosratipour Mar 11, 2024
72f36f0
recover Home test
alinosratipour Mar 11, 2024
9a713fd
tweak
alinosratipour Mar 12, 2024
15d70cb
add e2e test for filtering resource by topic
alinosratipour Mar 14, 2024
e57c2d8
refactor some tests in suggest and topicservice
alinosratipour Mar 18, 2024
18e41c1
moved filter to server side
alinosratipour Mar 19, 2024
36a4e0a
tweak
alinosratipour Mar 19, 2024
5779266
fixed pagination
alinosratipour Mar 19, 2024
4207fe1
tweak
alinosratipour Mar 19, 2024
370d20f
removed comment
alinosratipour Mar 19, 2024
ea9fda5
ad usenavigate hook
alinosratipour Mar 19, 2024
73a1bf2
fix pagination display from server
alinosratipour Mar 20, 2024
20a7d9a
tweak
alinosratipour Mar 20, 2024
91853e0
removed comments
alinosratipour Mar 20, 2024
5859f54
fix test
alinosratipour Mar 20, 2024
81003a2
refactor test
alinosratipour Mar 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"editor.formatOnSave": false,
"editor.formatOnSave": true,
alinosratipour marked this conversation as resolved.
Show resolved Hide resolved
"eslint.options": {
"ignorePath": ".config/.lintignore",
"overrideConfigFile": ".config/.eslintrc.json"
Expand Down
1 change: 0 additions & 1 deletion client/setupTests.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { randomUUID } from "node:crypto";
import "@testing-library/jest-dom";
import "whatwg-fetch";
import { setupServer } from "msw/node";

alinosratipour marked this conversation as resolved.
Show resolved Hide resolved
export const resourceStub = (overrides = {}) => ({
accession: new Date(),
description: null,
Expand Down
6 changes: 6 additions & 0 deletions client/src/components/Form/Select.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ export default function Select({
options,
placeholder,
required,
onChange = () => {},
className,
alinosratipour marked this conversation as resolved.
Show resolved Hide resolved
}) {
return (
<Label required={required} text={label}>
Expand All @@ -17,6 +19,8 @@ export default function Select({
disabled={options === undefined}
name={name}
required={required}
onChange={onChange}
className={className}
>
<option disabled value="">
{placeholder}
Expand All @@ -34,6 +38,7 @@ export default function Select({

Select.propTypes = {
defaultValue: PropTypes.string,
className: PropTypes.string,
label: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
options: PropTypes.arrayOf(
Expand All @@ -44,4 +49,5 @@ Select.propTypes = {
),
placeholder: PropTypes.string.isRequired,
required: PropTypes.bool,
onChange: PropTypes.func,
};
9 changes: 9 additions & 0 deletions client/src/components/ResourceList/ResourceList.scss
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,12 @@ ul.resource-list {
word-break: break-word;
}
}

.custom-select {
background-color: white;
border: 1px solid palette.$light-grey;
border-radius: layout.$border-radius;
font: typography.$body-text;
padding: layout.$baseline;
width: 100%;
}
191 changes: 164 additions & 27 deletions client/src/components/ResourceList/ResourceList.test.js
Original file line number Diff line number Diff line change
@@ -1,47 +1,184 @@
import { render, screen } from "@testing-library/react";
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { rest } from "msw";
import React from "react";
import { MemoryRouter } from "react-router-dom";

import { resourceStub } from "../../../setupTests";
import { resourceStub, server } from "../../../setupTests";
import * as useFetchPublishedResourcesModule from "../../hooks";

import TopicSelector from "./TopicSelector";

import ResourceList from "./index";

server.use(
rest.get("/api/topics", (req, res, ctx) => {
return res(
ctx.json([
{
id: "199a529b-22c1-460f-bc51-387cb12225e8",
name: "Git",
},
{
id: "84b099a4-8acd-4659-b5bd-1b89796fb924",
name: "HTML/CSS",
},
])
);
})
);

jest.mock("../../hooks/useFetchPublishedResources", () => ({
useFetchPublishedResources: jest.fn(),
}));

jest.mock("../../hooks/useFetchTopics", () => ({
useFetchTopics: jest.fn(),
}));
alinosratipour marked this conversation as resolved.
Show resolved Hide resolved

describe("ResourceList", () => {
it("shows the resource", () => {
const resource = resourceStub({
description:
'Comprehensive guide to setting up the various types of inputs with React (a.k.a. "data binding")',
title: "Data Binding in React",
beforeEach(() => {
jest.clearAllMocks();
});

const topics = [
{ id: "199a529b-22c1-460f-bc51-387cb12225e8", name: "Git" },
{ id: "84b099a4-8acd-4659-b5bd-1b89796fb924", name: "HTML/CSS" },
];

const resourceData = [
resourceStub({
id: "1",
title: "A Complete Guide to Flexbox",
url: "https://www.joshwcomeau.com/react/data-binding/",
topics: "84b099a4-8acd-4659-b5bd-1b89796fb924",
topic_name: "HTML/CSS",
}),
resourceStub({
id: "2",
title: "React Hooks Tutorial",
url: "https://reactjs.org/docs/hooks-intro.html",
topics: "199a529b-22c1-460f-bc51-387cb12225e8",
topic_name: "React",
}),
];

it("shows resources", async () => {
const resource = resourceStub({
description: "This is a very useful resource I found",
id: "abc123",
title: "Hello",
url: "https://example.com",
});
render(<ResourceList resources={[resource]} />);
expect(screen.getByRole("heading", { level: 3 })).toHaveTextContent(
resource.title

useFetchPublishedResourcesModule.useFetchPublishedResources.mockReturnValueOnce(
{
perPage: 20,
page: 1,
resources: [resource],
}
);

render(
<MemoryRouter>
<ResourceList />
</MemoryRouter>
);

await expect(
screen.findByRole("heading", { name: resource.title })
).resolves.toBeInTheDocument();

expect(screen.getByRole("link", { name: "example.com" })).toHaveAttribute(
"href",
resource.url
);

expect(
screen.getByRole("link", { name: "joshwcomeau.com" })
).toHaveAttribute("href", resource.url);
expect(screen.getByText(/comprehensive guide/i)).toBeInTheDocument();
screen.getByText(new RegExp(resource.description))
).toBeInTheDocument();
});

it("shows a message if no resources are available", () => {
render(<ResourceList resources={[]} />);
expect(screen.getByText(/no resources to show/i)).toBeInTheDocument();
it("shows a message if no resources are available", async () => {
useFetchPublishedResourcesModule.useFetchPublishedResources.mockReturnValueOnce(
{
perPage: 10,
page: 1,
resources: [],
}
);
useFetchPublishedResourcesModule.useFetchTopics.mockReturnValueOnce([]);
render(
<MemoryRouter>
<ResourceList />
</MemoryRouter>
);

await waitFor(() => {
expect(screen.getByText(/no resources to show/i)).toBeInTheDocument();
});
});

it("shows a publish button if enabled", async () => {
const publish = jest.fn();
const resource = resourceStub();
const user = userEvent.setup();
render(<ResourceList publish={publish} resources={[resource]} />);
it("shows the topic if available", async () => {
useFetchPublishedResourcesModule.useFetchPublishedResources.mockReturnValueOnce(
{
perPage: 20,
page: 1,
resources: resourceData,
}
);

await user.click(screen.getByRole("button", { name: /publish/i }));
render(
<MemoryRouter>
<ResourceList />
</MemoryRouter>
);

expect(publish).toHaveBeenCalledWith(resource.id);
await waitFor(() => {
expect(screen.getByText(resourceData[0].topic_name)).toBeInTheDocument();
});
});

it("shows the topic if available", () => {
const resource = resourceStub({ topic_name: "My Topic" });
render(<ResourceList resources={[resource]} />);
expect(screen.getByText(resource.topic_name)).toBeInTheDocument();
it("displays only resources with the selected topic", async () => {
const setSelectedTopicMock = jest.fn();

server.use(
rest.get("/api/resources", (req, res, ctx) => {
return res(ctx.json({ resources: resourceData }));
})
);

useFetchPublishedResourcesModule.useFetchPublishedResources.mockReturnValueOnce(
{
perPage: 20,
page: 1,
resources: resourceData.filter(
(resource) => resource.topic_name === "HTML/CSS"
),
}
);

render(
<MemoryRouter>
<TopicSelector
topics={topics}
setSelectedTopic={setSelectedTopicMock}
/>
</MemoryRouter>
);

await userEvent.selectOptions(
screen.getByRole("combobox", { name: /filter topic/i }),
"HTML/CSS"
);

render(
<MemoryRouter>
<ResourceList />
</MemoryRouter>
);

expect(screen.getByText("A Complete Guide to Flexbox")).toBeInTheDocument();
expect(screen.queryByText("React Hooks Tutorial")).not.toBeInTheDocument();
});
});
29 changes: 29 additions & 0 deletions client/src/components/ResourceList/TopicSelector.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import PropTypes from "prop-types";
import React from "react";
import { useNavigate } from "react-router-dom";

import { FormControls } from "..";

export default function TopicSelector({ topics, setSelectedTopic }) {
const navigate = useNavigate();
const handleChange = (event) => {
setSelectedTopic(event.target.value);
navigate("/");
};
return (
<div>
<FormControls.Select
label="Filter Topic"
placeholder="Select a topic"
name="filter topic"
options={topics}
onChange={handleChange}
className="custom-select"
/>
</div>
);
}
TopicSelector.propTypes = {
topics: PropTypes.array.isRequired,
setSelectedTopic: PropTypes.func.isRequired,
};
81 changes: 38 additions & 43 deletions client/src/components/ResourceList/index.js
Original file line number Diff line number Diff line change
@@ -1,49 +1,44 @@
import PropTypes from "prop-types";
import { useState } from "react";

import { Pagination } from "../../components";
import { useFetchPublishedResources, useFetchTopics } from "../../hooks";
import { formatUrl } from "../../utils/utils";

import "./ResourceList.scss";
import TopicSelector from "./TopicSelector";

export default function ResourceList({ publish, resources }) {
return (
<ul className="resource-list">
{resources.length === 0 && (
<li className="no-resources">
<em>No resources to show.</em>
</li>
)}
{resources.map(({ description, id, title, topic_name, url }) => (
<li key={id}>
<div>
<h3>{title}</h3>
{topic_name && <span className="topic">{topic_name}</span>}
</div>
{description && <p className="resource-description">{description}</p>}
<div>
<a href={url}>{formatUrl(url)}</a>
{publish && <button onClick={() => publish(id)}>Publish</button>}
</div>
</li>
))}
</ul>
);
}
export default function ResourceList() {
const [selectedTopic, setSelectedTopic] = useState(undefined);
const topics = useFetchTopics();
const { lastPage, resources } = useFetchPublishedResources(selectedTopic);

ResourceList.propTypes = {
publish: PropTypes.func,
resources: PropTypes.arrayOf(
PropTypes.shape({
description: PropTypes.string,
id: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
topic_name: PropTypes.string,
url: PropTypes.string.isRequired,
})
).isRequired,
};
return (
<>
<div>
{topics && (
<TopicSelector setSelectedTopic={setSelectedTopic} topics={topics} />
)}
</div>

function formatUrl(url) {
const host = new URL(url).host;
if (host.startsWith("www.")) {
return host.slice(4);
}
return host;
<ul className="resource-list">
{resources && resources.length === 0 && (
<li className="no-resources">
<em>No resources to show.</em>
</li>
)}
{resources &&
resources.map(({ description, id, title, topic_name, url }) => (
<li key={id}>
<div>
<h3>{title}</h3>
{topic_name && <span className="topic">{topic_name}</span>}
</div>
{description && <p>{description}</p>}
<a href={url}>{formatUrl(url)}</a>
</li>
))}
</ul>
<Pagination lastPage={lastPage ?? 1} />
</>
);
}
3 changes: 3 additions & 0 deletions client/src/hooks/index.js
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
export { default as useSearchParams } from "./useSearchParams";
export { usePagination } from "./usePagination";
export { useFetchTopics } from "./useFetchTopics";
export { useFetchPublishedResources } from "./useFetchPublishedResources";
Loading