Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
d4f7da2
update nav
codeEmpress1 May 6, 2025
f5abd8e
remove search functionality
codeEmpress1 Jun 17, 2025
b3b8f90
update banner test
codeEmpress1 Jun 17, 2025
cdfac87
Merge branch 'main' into update-nav-and-banner
codeEmpress1 Jun 17, 2025
97f0eda
fix lints
codeEmpress1 Jun 17, 2025
1b98b37
fix broken pagination
codeEmpress1 Jun 17, 2025
ad1f546
Merge pull request #34 from codeEmpress1/update-nav-and-banner
codeEmpress1 Jun 18, 2025
bff08fd
feat:consume rocks find and info APIs
codeEmpress1 Jun 19, 2025
302820b
remove mock data
codeEmpress1 Jun 19, 2025
645e3ba
chore: remove unused blueprint
codeEmpress1 Jun 19, 2025
5cbc0a9
fix lint
codeEmpress1 Jun 19, 2025
7b538f4
add page params for fetching packages
codeEmpress1 Jun 19, 2025
e0ef609
update details page
codeEmpress1 Jun 19, 2025
e141da2
add icons
codeEmpress1 Jun 19, 2025
5ac99fd
update details header
codeEmpress1 Jun 19, 2025
b1257b9
update details side-nav
codeEmpress1 Jun 19, 2025
9f6dc37
fix js lint
codeEmpress1 Jun 19, 2025
4743490
add unit tests for store logic
codeEmpress1 Jun 19, 2025
20b67f7
update docstrings for store logic
codeEmpress1 Jun 19, 2025
97b6e7a
add unittests for store routes
codeEmpress1 Jun 19, 2025
04c90e2
fix python lints
codeEmpress1 Jun 19, 2025
957c60a
Merge pull request #38 from codeEmpress1/WD-22734-replace-mock-data-w…
codeEmpress1 Jun 19, 2025
e91588f
address comments on previous PR
codeEmpress1 Jun 20, 2025
00f1efe
Merge pull request #39 from codeEmpress1/address-comments-on-PR-WD-22734
codeEmpress1 Jun 20, 2025
5795b9f
chore(deps): update internal dependencies
renovate[bot] Jun 20, 2025
3c73943
Merge pull request #33 from canonical/renovate/internal
alvaromateo Jun 24, 2025
90f2850
add cache to API call for rock find endpoint
codeEmpress1 Jun 26, 2025
5b16754
fix lint
codeEmpress1 Jun 26, 2025
15d59b3
Merge pull request #41 from codeEmpress1/WD-23261
codeEmpress1 Jun 27, 2025
b413e97
chore(deps): update internal dependencies
renovate[bot] Jun 27, 2025
eb45b82
Merge pull request #40 from canonical/renovate/internal
alvaromateo Jun 27, 2025
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
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@
"dependencies": {
"@canonical/cookie-policy": "3.6.5",
"@canonical/global-nav": "3.6.4",
"@canonical/react-components": "2.7.1",
"@canonical/store-components": "0.53.0",
"@canonical/react-components": "2.7.4",
"@canonical/store-components": "0.54.0",
"autoprefixer": "10.4.21",
"date-fns": "4.1.0",
"formik": "^2.4.5",
Expand Down
6 changes: 3 additions & 3 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
canonicalwebteam.flask-base==2.5.0
canonicalwebteam.flask-base==2.6.0
beautifulsoup4==4.12.3
canonicalwebteam.candid==0.9.0
canonicalwebteam.discourse==6.2.0
canonicalwebteam.image-template==1.5.0
canonicalwebteam.store-api==6.3.0
canonicalwebteam.image-template==1.6.0
canonicalwebteam.store-api==6.4.0
canonicalwebteam.docstring-extractor==1.2.0

Flask-WTF==1.2.2
Expand Down
104 changes: 0 additions & 104 deletions static/js/src/search/App.tsx

This file was deleted.

55 changes: 7 additions & 48 deletions static/js/src/store/components/Banner/Banner.tsx
Original file line number Diff line number Diff line change
@@ -1,56 +1,15 @@
import { RefObject } from "react";
import { useSearchParams } from "react-router-dom";
import { Strip, Row, Col } from "@canonical/react-components";

type Props = {
searchRef: RefObject<HTMLInputElement>;
};

function Banner({ searchRef }: Props) {
const [searchParams, setSearchParams] = useSearchParams();

function Banner() {
return (
<Strip type="dark">
<Row>
<Col size={6} className="col-start-large-4">
<h1 className="p-heading--2">The Rocks Collection</h1>
<form className="p-search-box" action="/all-search">
<label className="u-off-screen" htmlFor="search">
Search Rocks
</label>
<input
type="search"
id="search"
className="p-search-box__input"
name="q"
placeholder="Search Rocks"
defaultValue={searchParams.get("q") || ""}
ref={searchRef}
/>
<button
type="reset"
className="p-search-box__reset"
onClick={() => {
searchParams.delete("q");
setSearchParams(searchParams);
}}
>
<i className="p-icon--close">Close</i>
</button>
<button
type="submit"
className="p-search-box__button"
onClick={() => {
if (searchRef.current) {
const newSearch = new URLSearchParams();
newSearch.set("q", searchRef.current.value);
setSearchParams(newSearch);
}
}}
>
<i className="p-icon--search">Search</i>
</button>
</form>
<Col size={6}>
<h1 className="p-heading--2">
The Ubuntu-based
<br />
container images store
</h1>
</Col>
</Row>
</Strip>
Expand Down
79 changes: 5 additions & 74 deletions static/js/src/store/components/Banner/__tests__/Banner.test.tsx
Original file line number Diff line number Diff line change
@@ -1,88 +1,19 @@
import React from "react";
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import { render, screen } from "@testing-library/react";
import "@testing-library/jest-dom";
import { useSearchParams } from "react-router-dom";
import Banner from "../Banner";

jest.mock("react-router-dom", () => ({
useSearchParams: jest.fn(),
}));

describe("Banner Component", () => {
let mockSetSearchParams: jest.Mock;
let mockSearchRef: React.RefObject<HTMLInputElement>;

beforeEach(() => {
mockSetSearchParams = jest.fn();
(useSearchParams as jest.Mock).mockReturnValue([
new URLSearchParams(),
mockSetSearchParams,
]);

mockSearchRef = {
current: document.createElement("input"),
};
});

test("should render the Banner component", () => {
render(<Banner searchRef={mockSearchRef} />);

render(<Banner />);
expect(
screen.getByRole("heading", { name: /The Rocks Collection/i })
screen.getByRole("heading", {
name: /The Ubuntu-based container images store/i,
})
).toBeInTheDocument();

expect(screen.getByPlaceholderText("Search Rocks")).toBeInTheDocument();

expect(screen.getByRole("button", { name: /Search/i })).toBeInTheDocument();
expect(screen.getByRole("button", { name: /Close/i })).toBeInTheDocument();
});

test("should update search params", async () => {
render(<Banner searchRef={mockSearchRef} />);

const input = screen.getByLabelText("Search Rocks");
fireEvent.change(input, { target: { value: "kubernetes" } });
fireEvent.click(screen.getByRole("button", { name: /Search/i }));

await waitFor(() => {
expect(mockSetSearchParams).toHaveBeenCalledWith(
new URLSearchParams({ q: "kubernetes" })
);
});
});

test("should clear search params on reset button click", () => {
render(<Banner searchRef={mockSearchRef} />);

fireEvent.click(screen.getByRole("button", { name: /Close/i }));

expect(mockSetSearchParams).toHaveBeenCalledWith(new URLSearchParams());
});

test("should not update search params when input is empty", () => {
render(<Banner searchRef={mockSearchRef} />);

if (mockSearchRef.current) {
mockSearchRef.current.value = "";
}

fireEvent.submit(screen.getByRole("button", { name: /Search/i }));

expect(mockSetSearchParams).not.toHaveBeenCalled();
});

test("should preserve query parameter in input field", () => {
const searchParams = new URLSearchParams({ q: "test" });
(useSearchParams as jest.Mock).mockReturnValue([
searchParams,
mockSetSearchParams,
]);

render(<Banner searchRef={mockSearchRef} />);

const searchInput = screen.getByPlaceholderText(
"Search Rocks"
) as HTMLInputElement;
expect(searchInput.value).toBe("test");
});
});
71 changes: 19 additions & 52 deletions static/js/src/store/components/Packages/Packages.tsx
Original file line number Diff line number Diff line change
@@ -1,86 +1,53 @@
import { useEffect, useRef } from "react";
import { useQuery } from "react-query";
import { useSearchParams } from "react-router-dom";
import {
Strip,
Row,
Col,
Pagination,
Button,
} from "@canonical/react-components";
import { Strip, Row, Col, Pagination } from "@canonical/react-components";
import { CharmCard, LoadingCard } from "@canonical/store-components";

import Banner from "../Banner";
import { Package, Publisher } from "../../types";

function Packages() {
const ITEMS_PER_PAGE = 12;
const [searchParams, setSearchParams] = useSearchParams();
const currentPage = searchParams.get("page") || "1";

const getData = async () => {
const response = await fetch(`/store.json`);
const response = await fetch(`/store.json?page=${currentPage}`);
const data = await response.json();

const packagesWithId = data.packages.map((item: string[]) => {
return {
...item,
id: crypto.randomUUID(),
};
});
const packagesWithId = data.packages.map((item: string[]) => ({
...item,
id: crypto.randomUUID(),
}));

return {
total_items: data.total_items,
total_pages: data.total_pages,
packages: packagesWithId,
};
};

const [searchParams, setSearchParams] = useSearchParams();
const currentPage = searchParams.get("page") || "1";
const { data, status, refetch, isFetching } = useQuery("data", getData);
const searchRef = useRef<HTMLInputElement | null>(null);
const searchSummaryRef = useRef<HTMLDivElement>(null);
useEffect(() => {
refetch();
}, [searchParams]);
const { data, status, isFetching } = useQuery(["data", currentPage], getData);

const firstResultNumber = (parseInt(currentPage) - 1) * ITEMS_PER_PAGE + 1;
const lastResultNumber =
(parseInt(currentPage) - 1) * ITEMS_PER_PAGE + data?.packages.length;
(parseInt(currentPage) - 1) * ITEMS_PER_PAGE + (data?.packages.length || 0);

return (
<>
<Banner searchRef={searchRef} />
<Banner />
<Strip>
<Row>
<Col size={12}>
{data?.packages && data?.packages.length > 0 && (
<div className="u-fixed-width" ref={searchSummaryRef}>
{searchParams.get("q") ? (
<p>
Showing {currentPage === "1" ? "1" : firstResultNumber} to{" "}
{lastResultNumber} of {data?.total_items} results for{" "}
<strong>"{searchParams.get("q")}"</strong>.{" "}
<Button
appearance="link"
onClick={() => {
searchParams.delete("q");
searchParams.delete("page");
setSearchParams(searchParams);

if (searchRef.current) {
searchRef.current.value = "";
}
}}
>
Clear search
</Button>
</p>
) : (
<p>
Showing {currentPage === "1" ? "1" : firstResultNumber} to{" "}
{lastResultNumber} of {data?.total_items} items
</p>
)}
<div className="u-fixed-width">
<p>
Showing {currentPage === "1" ? "1" : firstResultNumber} to{" "}
{lastResultNumber} of {data?.total_items} items
</p>
</div>
)}

<Row>
{isFetching &&
[...Array(ITEMS_PER_PAGE)].map((_item, index) => (
Expand Down
Loading