Skip to content

Commit 59c44ba

Browse files
committed
fix: Only show remodels to users with permissions
1 parent c0711ed commit 59c44ba

8 files changed

Lines changed: 154 additions & 79 deletions

File tree

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

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,19 +34,18 @@ const remodelsResponse = {
3434
"to-model": "test-to-model",
3535
},
3636
],
37+
"next-cursor": null,
3738
};
3839

3940
const handlers = [
4041
http.get("/api/store/test-brand-id/models/remodel-allowlist", () => {
4142
return HttpResponse.json({
4243
data: remodelsResponse,
43-
message: "",
4444
success: true,
4545
});
4646
}),
4747
http.get("/api/store/test-brand-id-fail/models/remodel-allowlist", () => {
4848
return HttpResponse.json({
49-
data: [],
5049
message: "There was a problem fetching remodels",
5150
success: false,
5251
});
@@ -84,10 +83,13 @@ describe("useRemodels", () => {
8483
expect(result.current.isSuccess).toBe(true);
8584
});
8685

87-
expect(result.current.data).toEqual(remodelsResponse.allowlist);
86+
expect(result.current.data).toEqual({
87+
data: remodelsResponse,
88+
success: true,
89+
});
8890
});
8991

90-
test("returns error if request fails", async () => {
92+
test("returns message if request fails", async () => {
9193
const { result } = renderHook(
9294
() => useRemodels("test-brand-id-fail", "test-to-model"),
9395
{
@@ -96,10 +98,13 @@ describe("useRemodels", () => {
9698
);
9799

98100
await waitFor(() => {
99-
expect(result.current.isError).toBe(true);
101+
expect(result.current.isSuccess).toBe(true);
100102
});
101103

102-
expect(result.current.data).toBeUndefined();
104+
expect(result.current.data).toEqual({
105+
message: "There was a problem fetching remodels",
106+
success: false,
107+
});
103108
});
104109

105110
test("returns error if network error", async () => {

static/js/publisher/hooks/useRemodels.ts

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

44
const useRemodels = (
55
brandId: string | undefined,
66
modelId: string | undefined,
7-
): UseQueryResult<Remodel[], Error> => {
8-
return useQuery<Remodel[], Error>({
7+
): UseQueryResult<ApiResponse<RemodelResponse>, Error> => {
8+
return useQuery<ApiResponse<RemodelResponse>, Error>({
99
queryKey: ["remodels", brandId, modelId],
1010
queryFn: async () => {
1111
const response = await fetch(
1212
`/api/store/${brandId}/models/remodel-allowlist`,
1313
);
1414

15-
if (!response.ok) {
16-
throw new Error("There was a problem fetching remodels");
15+
const responseData = await response.json();
16+
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+
);
26+
27+
responseData.data.allowlist = remodelsForCurrentModel.sort(
28+
(a: Remodel, b: Remodel) => {
29+
if (a["created-at"] > b["created-at"]) {
30+
return -1;
31+
}
32+
33+
if (a["created-at"] < b["created-at"]) {
34+
return 1;
35+
}
36+
37+
return 0;
38+
},
39+
);
1740
}
1841

19-
const data = await response.json();
20-
21-
if (!data.success) {
22-
throw new Error(data.message);
23-
}
24-
25-
const remodelsForCurrentModel = data.data.allowlist.filter(
26-
(remodel: Remodel) => {
27-
return (
28-
remodel["from-model"] === modelId || remodel["to-model"] === modelId
29-
);
30-
},
31-
);
32-
33-
return remodelsForCurrentModel.sort((a: Remodel, b: Remodel) => {
34-
if (a["created-at"] > b["created-at"]) {
35-
return -1;
36-
}
37-
38-
if (a["created-at"] < b["created-at"]) {
39-
return 1;
40-
}
41-
42-
return 0;
43-
});
42+
return responseData;
4443
},
4544
enabled: !!brandId,
4645
});

static/js/publisher/layouts/ModelDetailsPageLayout/ModelNav.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import { Link, useParams } from "react-router-dom";
2+
import { useAtomValue } from "jotai";
3+
4+
import { remodelPermissionsState } from "../../state/remodelsState";
25

36
function ModelNav({ sectionName }: { sectionName: string }): React.JSX.Element {
47
const { id, modelId } = useParams();
8+
const hasRemodelPermissions = useAtomValue(remodelPermissionsState);
59

610
return (
711
<nav className="p-tabs">
@@ -26,6 +30,18 @@ function ModelNav({ sectionName }: { sectionName: string }): React.JSX.Element {
2630
Policies
2731
</Link>
2832
</li>
33+
{hasRemodelPermissions && (
34+
<li className="p-tabs__item">
35+
<Link
36+
to={`/admin/${id}/models/${modelId}/remodel`}
37+
className="p-tabs__link"
38+
aria-selected={sectionName === "remodel"}
39+
role="tab"
40+
>
41+
Remodel
42+
</Link>
43+
</li>
44+
)}
2945
</ul>
3046
</nav>
3147
);

static/js/publisher/pages/Model/Model.tsx

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,27 @@ import {
1313
} from "@canonical/react-components";
1414

1515
import { modelsListState } from "../../state/modelsState";
16+
import { remodelPermissionsState } from "../../state/remodelsState";
1617
import { brandIdState, brandStoreState } from "../../state/brandStoreState";
17-
import { useModels } from "../../hooks";
18+
import { useModels, useRemodels } from "../../hooks";
1819
import { setPageTitle } from "../../utils";
19-
import type { Model as ModelType } from "../../types/shared";
2020
import { PortalEntrance } from "../Portals/Portals";
2121

22+
import type { UseQueryResult } from "react-query";
23+
import type {
24+
Model as ModelType,
25+
RemodelResponse,
26+
ApiResponse,
27+
} from "../../types/shared";
28+
2229
function Model() {
2330
const { id, modelId } = useParams();
2431
const brandId = useAtomValue(brandIdState);
2532
const [apiKey, setApiKey] = useState("");
2633
const [showSuccessNotification, setShowSuccessNotificaton] = useState(false);
2734
const [showErrorNotification, setShowErrorNotificaton] = useState(false);
2835
const setModelsList = useSetAtom(modelsListState);
36+
const setRemodelPermissionState = useSetAtom(remodelPermissionsState);
2937
const brandStore = useAtomValue(brandStoreState(id));
3038

3139
const mutation = useMutation({
@@ -72,6 +80,25 @@ function Model() {
7280
refetch: () => void;
7381
} = useModels(brandId);
7482

83+
const {
84+
isLoading: remodelsIsLoading,
85+
isError: remodelsIsError,
86+
data: remodelsData,
87+
}: UseQueryResult<ApiResponse<RemodelResponse>, Error> = useRemodels(
88+
brandId,
89+
modelId,
90+
);
91+
92+
useEffect(() => {
93+
setRemodelPermissionState(false);
94+
95+
if (!remodelsIsLoading && !remodelsIsError && remodelsData) {
96+
if (remodelsData.success) {
97+
setRemodelPermissionState(true);
98+
}
99+
}
100+
}, [remodelsIsLoading, remodelsIsError, remodelsData, brandId, id]);
101+
75102
const handleError = () => {
76103
setShowErrorNotificaton(true);
77104
setTimeout(() => {

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

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useEffect, useState } from "react";
2-
import { useAtomValue, useSetAtom } from "jotai";
2+
import { useAtomValue, useSetAtom, useAtom } from "jotai";
33
import {
44
useParams,
55
useSearchParams,
@@ -10,6 +10,7 @@ import { Notification, Icon, Row, Col } from "@canonical/react-components";
1010

1111
import { useRemodels } from "../../hooks";
1212
import {
13+
remodelPermissionsState,
1314
remodelsListFilterState,
1415
remodelsListState,
1516
} from "../../state/remodelsState";
@@ -22,7 +23,7 @@ import RemodelTable from "./RemodelTable";
2223
import ConfigureRemodelForm from "./ConfigureRemodelForm";
2324

2425
import type { UseQueryResult } from "react-query";
25-
import type { Remodel } from "../../types/shared";
26+
import type { ApiResponse, Remodel, RemodelResponse } from "../../types/shared";
2627

2728
function Remodel(): React.JSX.Element {
2829
const { id, modelId } = useParams();
@@ -33,41 +34,58 @@ function Remodel(): React.JSX.Element {
3334
error,
3435
data,
3536
refetch,
36-
}: UseQueryResult<Remodel[], Error> = useRemodels(brandId, modelId);
37+
}: UseQueryResult<ApiResponse<RemodelResponse>, Error> = useRemodels(
38+
brandId,
39+
modelId,
40+
);
3741
const setRemodels = useSetAtom(remodelsListState);
3842
const setFilter = useSetAtom(remodelsListFilterState);
3943
const [showNotification, setShowNotification] = useState(false);
4044
const [showErrorNotification, setShowErrorNotification] = useState(false);
4145
const [errorMessage, setErrorMessage] = useState("");
4246
const brandStore = useAtomValue(brandStoreState(id));
4347
const [searchParams] = useSearchParams();
48+
const [hasRemodelPermissions, setHasRemodelPermissions] = useAtom(
49+
remodelPermissionsState,
50+
);
51+
const [permissionsMessage, setPermissionsMessage] = useState("");
4452
const navigate = useNavigate();
4553

4654
brandStore
4755
? setPageTitle(`Remodels in ${brandStore.name}`)
4856
: setPageTitle("Remodels");
4957

5058
useEffect(() => {
59+
setHasRemodelPermissions(false);
60+
5161
if (!isLoading && !isError && data) {
52-
setRemodels(data);
62+
if (data.success) {
63+
setHasRemodelPermissions(true);
64+
}
65+
66+
if (data.data?.allowlist) {
67+
setRemodels(data.data?.allowlist);
68+
}
69+
70+
if (!data.success) {
71+
setPermissionsMessage(
72+
data.message || "There was a problem fetching remodels",
73+
);
74+
}
75+
5376
setFilter(searchParams.get("filter") || "");
5477
}
5578
}, [isLoading, error, data, brandId, id]);
5679

5780
return (
5881
<>
5982
<div className="u-fixed-width u-flex-column u-flex-grow">
60-
{isError && error && (
61-
<Notification severity="negative">
62-
Error: {error.message}
63-
</Notification>
64-
)}
6583
{isLoading ? (
6684
<p>
6785
<Icon name="spinner" className="u-animation--spin" />
6886
&nbsp;Fetching remodels...
6987
</p>
70-
) : (
88+
) : hasRemodelPermissions ? (
7189
<>
7290
<Row>
7391
<Col size={6}>
@@ -90,6 +108,8 @@ function Remodel(): React.JSX.Element {
90108
<RemodelTable />
91109
</div>
92110
</>
111+
) : (
112+
<Notification severity="caution">{permissionsMessage}</Notification>
93113
)}
94114
</div>
95115

Lines changed: 25 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,48 @@
11
import { BrowserRouter } from "react-router-dom";
22
import { QueryClient, QueryClientProvider } from "react-query";
3+
import { JotaiTestProvider } from "../../../test-utils";
34
import { render, screen } from "@testing-library/react";
45

6+
import { remodelPermissionsState } from "../../../state/remodelsState";
7+
58
import "@testing-library/jest-dom";
69

710
import Remodel from "../Remodel";
811

9-
const mockFilterQuery = "test-model";
10-
11-
vi.mock("react-router-dom", async (importOriginal) => ({
12-
...(await importOriginal()),
13-
useSearchParams: () => [new URLSearchParams({ filter: mockFilterQuery })],
14-
}));
15-
16-
vi.mock("../../Portals/Portals", async (importOriginal) => ({
17-
...(await importOriginal()),
18-
PortalEntrance: ({ children }: { children: React.ReactNode }) => (
19-
<>{children}</>
20-
),
21-
}));
22-
23-
const queryClient = new QueryClient({
24-
defaultOptions: {
25-
queries: {
26-
refetchOnWindowFocus: false,
27-
refetchOnReconnect: false,
28-
},
29-
},
30-
});
12+
const queryClient = new QueryClient();
3113

32-
function renderComponent() {
14+
function renderComponent(hasPermissions: boolean) {
3315
return render(
3416
<BrowserRouter>
3517
<QueryClientProvider client={queryClient}>
36-
<Remodel />
18+
<JotaiTestProvider
19+
initialValues={[[remodelPermissionsState, hasPermissions]]}
20+
>
21+
<Remodel />
22+
</JotaiTestProvider>
3723
</QueryClientProvider>
3824
</BrowserRouter>,
3925
);
4026
}
4127

4228
describe("Remodel", () => {
43-
it("displays a filter input", () => {
44-
renderComponent();
29+
it("renders filter if user has permissions", () => {
30+
renderComponent(true);
4531
expect(screen.getByLabelText("Search remodels")).toBeInTheDocument();
4632
});
4733

48-
it("populates filter with the filter query parameter", () => {
49-
renderComponent();
50-
expect(screen.getByLabelText("Search remodels")).toHaveValue(
51-
mockFilterQuery,
52-
);
34+
it("doesn't render filter if user doesn't have permissions", () => {
35+
renderComponent(false);
36+
expect(screen.queryByLabelText("Search remodels")).not.toBeInTheDocument();
37+
});
38+
39+
it("renders table if user has permissions", () => {
40+
renderComponent(true);
41+
expect(screen.getByTestId("remodel-table")).toBeInTheDocument();
42+
});
43+
44+
it("doesn't render table if user doesn't have permissions", () => {
45+
renderComponent(false);
46+
expect(screen.queryByTestId("remodel-table")).not.toBeInTheDocument();
5347
});
5448
});

0 commit comments

Comments
 (0)