Skip to content

Commit bcb400b

Browse files
committed
feat: Use Store API pagination for remodels table
1 parent 32ef03d commit bcb400b

7 files changed

Lines changed: 339 additions & 46 deletions

File tree

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ canonicalwebteam.discourse==7.2.0
66
canonicalwebteam.blog==6.8.4
77
canonicalwebteam.search==2.1.2
88
canonicalwebteam.image-template==1.9.0
9-
canonicalwebteam.store-api==7.8.1
9+
canonicalwebteam.store-api==7.8.2
1010
canonicalwebteam.launchpad==0.9.0
1111
django-openid-auth==0.17
1212
Flask-OpenID==1.3.1

static/js/publisher/hooks/__tests__/useRemodels.test.tsx

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,74 @@ describe("useRemodels", () => {
8989
});
9090
});
9191

92+
test("returns remodels data with pageSize param", async () => {
93+
server.use(
94+
http.get(
95+
"/api/store/test-brand-id/models/test-model/remodel",
96+
({ request }) => {
97+
const url = new URL(request.url);
98+
99+
expect(url.searchParams.get("page-size")).toBe("10");
100+
return HttpResponse.json({
101+
data: remodelsResponse,
102+
success: true,
103+
});
104+
},
105+
),
106+
);
107+
});
108+
109+
test("returns remodels data with page param", async () => {
110+
server.use(
111+
http.get(
112+
"/api/store/test-brand-id/models/test-model/remodel",
113+
({ request }) => {
114+
const url = new URL(request.url);
115+
116+
expect(url.searchParams.get("page")).toBe("next_cursor_value");
117+
return HttpResponse.json({
118+
data: remodelsResponse,
119+
success: true,
120+
});
121+
},
122+
),
123+
);
124+
});
125+
126+
test("returns remodels data with fromModel param", async () => {
127+
server.use(
128+
http.get(
129+
"/api/store/test-brand-id/models/test-model/remodel",
130+
({ request }) => {
131+
const url = new URL(request.url);
132+
133+
expect(url.searchParams.get("from-model")).toBe("test-from-model");
134+
return HttpResponse.json({
135+
data: remodelsResponse,
136+
success: true,
137+
});
138+
},
139+
),
140+
);
141+
});
142+
143+
test("returns remodels data with pageSize, page and fromModel param", async () => {
144+
server.use(
145+
http.get(
146+
"/api/store/test-brand-id/models/test-model/remodel",
147+
({ request }) => {
148+
const url = new URL(request.url);
149+
150+
expect(url.searchParams.get("from-model")).toBe("test-from-model");
151+
return HttpResponse.json({
152+
data: remodelsResponse,
153+
success: true,
154+
});
155+
},
156+
),
157+
);
158+
});
159+
92160
test("returns error if request fails", async () => {
93161
const { result } = renderHook(
94162
() => useRemodels("test-brand-id-fail", "test-to-model"),

static/js/publisher/hooks/useRemodels.ts

Lines changed: 27 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,41 @@
11
import { useQuery, UseQueryResult } from "react-query";
2-
import type { Remodel, RemodelResponse, ApiResponse } from "../types/shared";
2+
import type { RemodelResponse, ApiResponse } from "../types/shared";
33

44
const useRemodels = (
55
brandId: string | undefined,
66
modelId: string | undefined,
7+
urlSearchParams?: {
8+
page?: string | null;
9+
pageSize?: number;
10+
fromModel?: string;
11+
},
712
): UseQueryResult<ApiResponse<RemodelResponse>, Error> => {
8-
return useQuery<ApiResponse<RemodelResponse>, Error>({
9-
queryKey: ["remodels", brandId, modelId],
10-
queryFn: async () => {
11-
const response = await fetch(
12-
`/api/store/${brandId}/models/remodel-allowlist`,
13-
);
14-
15-
const responseData = await response.json();
13+
const url = new URL(
14+
`/api/store/${brandId}/models/remodel-allowlist`,
15+
window.location.origin,
16+
);
1617

17-
if (responseData.data?.allowlist) {
18-
const remodelsForCurrentModel = responseData.data.allowlist.filter(
19-
(remodel: Remodel) => {
20-
return (
21-
remodel["from-model"] === modelId ||
22-
remodel["to-model"] === modelId
23-
);
24-
},
25-
);
18+
if (urlSearchParams) {
19+
const { page, pageSize, fromModel } = urlSearchParams;
2620

27-
responseData.data.allowlist = remodelsForCurrentModel.sort(
28-
(a: Remodel, b: Remodel) => {
29-
if (a["created-at"] > b["created-at"]) {
30-
return -1;
31-
}
21+
if (page) {
22+
url.searchParams.set("page", page);
23+
}
3224

33-
if (a["created-at"] < b["created-at"]) {
34-
return 1;
35-
}
25+
if (pageSize) {
26+
url.searchParams.set("page-size", pageSize.toString());
27+
}
3628

37-
return 0;
38-
},
39-
);
40-
}
29+
if (fromModel) {
30+
url.searchParams.set("from-model", fromModel);
31+
}
32+
}
4133

34+
return useQuery<ApiResponse<RemodelResponse>, Error>({
35+
queryKey: ["remodels", brandId, modelId, urlSearchParams],
36+
queryFn: async () => {
37+
const response = await fetch(url);
38+
const responseData = await response.json();
4239
return responseData;
4340
},
4441
enabled: !!brandId,

static/js/publisher/pages/Remodel/Remodel.tsx

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import { useEffect, useState } from "react";
22
import { useAtomValue, useSetAtom } from "jotai";
3-
import { useParams, Link, useNavigate, useLocation } from "react-router-dom";
3+
import {
4+
useParams,
5+
Link,
6+
useNavigate,
7+
useLocation,
8+
useSearchParams,
9+
} from "react-router-dom";
410
import { Notification, Icon, Row, Col } from "@canonical/react-components";
511

612
import { useRemodels } from "../../hooks";
@@ -17,8 +23,16 @@ import type { Remodel, RemodelResponse, ApiResponse } from "../../types/shared";
1723

1824
function Remodel(): React.JSX.Element {
1925
const { id, modelId } = useParams();
26+
const [searchParams, setSearchParams] = useSearchParams();
2027
const location = useLocation();
2128
const brandId = useAtomValue(brandIdState);
29+
const [currentCursor, setCurrentCursor] = useState<string | null>(null);
30+
const [nextCursor, setNextCursor] = useState<string | null>(null);
31+
const [cursorHistory, setCursorHistory] = useState<Array<string | null>>([]);
32+
33+
const pageSizeParam = searchParams.get("page-size");
34+
const pageSize = pageSizeParam ? parseInt(pageSizeParam) : 25;
35+
2236
const {
2337
isLoading,
2438
isError,
@@ -28,6 +42,11 @@ function Remodel(): React.JSX.Element {
2842
}: UseQueryResult<ApiResponse<RemodelResponse>, Error> = useRemodels(
2943
brandId,
3044
modelId,
45+
{
46+
fromModel: modelId,
47+
pageSize: pageSize,
48+
page: currentCursor,
49+
},
3150
);
3251
const setRemodels = useSetAtom(remodelsListState);
3352
const [showNotification, setShowNotification] = useState(false);
@@ -36,13 +55,33 @@ function Remodel(): React.JSX.Element {
3655
const brandStore = useAtomValue(brandStoreState(id));
3756
const navigate = useNavigate();
3857

58+
const handlePageForward = () => {
59+
setCursorHistory((prev) => {
60+
return [...prev, currentCursor];
61+
});
62+
setCurrentCursor(nextCursor);
63+
};
64+
65+
const handlePageBack = () => {
66+
const lastCursor = cursorHistory[cursorHistory.length - 1];
67+
setCurrentCursor(lastCursor);
68+
setCursorHistory((prev) => {
69+
return prev.filter((c) => c !== lastCursor);
70+
});
71+
};
72+
3973
brandStore
4074
? setPageTitle(`Remodels in ${brandStore.name}`)
4175
: setPageTitle("Remodels");
4276

4377
useEffect(() => {
44-
if (!isLoading && !isError && data) {
78+
if (isLoading || isError) {
79+
return;
80+
}
81+
82+
if (data) {
4583
setRemodels(data.data?.allowlist || []);
84+
setNextCursor(data.data?.["next-cursor"] || null);
4685
}
4786
}, [isLoading, isError, data, brandId, id]);
4887

@@ -76,7 +115,18 @@ function Remodel(): React.JSX.Element {
76115
</Col>
77116
</Row>
78117
<div className="u-flex-column u-flex-grow">
79-
<RemodelTable />
118+
{data && (
119+
<RemodelTable
120+
handlePageForward={handlePageForward}
121+
handlePageBack={handlePageBack}
122+
forwardDisabled={!nextCursor}
123+
backDisabled={
124+
cursorHistory.length < 1 || currentCursor === null
125+
}
126+
pageSize={pageSize}
127+
setSearchParams={setSearchParams}
128+
/>
129+
)}
80130
</div>
81131
</>
82132
)}

static/js/publisher/pages/Remodel/RemodelTable.tsx

Lines changed: 68 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,55 @@
11
import { useAtomValue } from "jotai";
2-
import { MainTable, TablePagination } from "@canonical/react-components";
2+
import {
3+
MainTable,
4+
Pagination,
5+
Row,
6+
Col,
7+
Select,
8+
} from "@canonical/react-components";
39
import { format } from "date-fns";
410

511
import { remodelsListState } from "../../state/remodelsState";
612

713
import type { Remodel } from "../../types/shared";
14+
import { ChangeEvent } from "react";
815

9-
function RemodelTable(): React.JSX.Element {
16+
type Props = {
17+
handlePageForward: () => void;
18+
handlePageBack: () => void;
19+
forwardDisabled: boolean;
20+
backDisabled: boolean;
21+
pageSize: number;
22+
setSearchParams: (arg: Record<string, string>) => void;
23+
};
24+
25+
function RemodelTable({
26+
handlePageForward,
27+
handlePageBack,
28+
forwardDisabled,
29+
backDisabled,
30+
pageSize,
31+
setSearchParams,
32+
}: Props): React.JSX.Element {
1033
const remodels = useAtomValue(remodelsListState);
1134

1235
const headers = [
1336
{
1437
content: "Target model",
1538
style: { width: "250px" },
39+
className: "u-truncate",
1640
},
1741
{
1842
content: "Original model",
1943
style: { width: "250px" },
44+
className: "u-truncate",
2045
},
21-
{ content: "Serial" },
46+
{ content: "Serial", className: "u-truncate" },
2247
{
2348
content: "Created date",
24-
className: "u-align--right",
49+
className: "u-align--right u-truncate",
2550
style: { width: "130px" },
2651
},
27-
{ content: "Note" },
52+
{ content: "Note", className: "u-truncate" },
2853
];
2954

3055
const rows = remodels.map((remodel: Remodel) => {
@@ -46,17 +71,49 @@ function RemodelTable(): React.JSX.Element {
4671
});
4772

4873
return (
49-
<TablePagination
50-
data={rows}
51-
pageLimits={[25, 50, 100, 200]}
52-
position="below"
53-
>
74+
<>
5475
<MainTable
5576
data-testid="remodel-table"
5677
emptyStateMsg="No remodels found"
5778
headers={headers}
79+
rows={rows}
5880
/>
59-
</TablePagination>
81+
<Row>
82+
<Col size={6} medium={3}>
83+
<Pagination
84+
onForward={() => {
85+
handlePageForward();
86+
}}
87+
onBack={() => {
88+
handlePageBack();
89+
}}
90+
showLabels
91+
forwardLabel="Next"
92+
forwardDisabled={forwardDisabled}
93+
backLabel="Prev"
94+
backDisabled={backDisabled}
95+
/>
96+
</Col>
97+
<Col size={6} medium={3} className="u-align--right">
98+
<Select
99+
label="Number of remodels per page"
100+
labelClassName="u-off-screen"
101+
defaultValue={pageSize}
102+
style={{ maxWidth: "150px" }}
103+
onChange={(e: ChangeEvent<HTMLSelectElement>) => {
104+
setSearchParams({ "page-size": e.target.value });
105+
}}
106+
options={[
107+
{ label: "10/page", value: 10 },
108+
{ label: "25/page", value: 25 },
109+
{ label: "50/page", value: 50 },
110+
{ label: "100/page", value: 100 },
111+
{ label: "200/page", value: 200 },
112+
]}
113+
/>
114+
</Col>
115+
</Row>
116+
</>
60117
);
61118
}
62119

0 commit comments

Comments
 (0)