Skip to content

Commit e8b803d

Browse files
committed
Merge branch 'master' of github.com:dmm-com/airone into feature/frontend-core-module
2 parents f747a93 + 184bbc6 commit e8b803d

18 files changed

+499
-320
lines changed

CHANGELOG.md

+8
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@
88

99
### Fixed
1010

11+
## v3.109.0
12+
13+
### Changed
14+
* Update duplicate entity name error handling.
15+
Contributed by @tsunoda-takahiro
16+
* Update react-router from v5 to v6.
17+
Contributed by @syucream
18+
1119
## v3.108.0
1220

1321
### Changed

entity/tests/test_view.py

+23
Original file line numberDiff line numberDiff line change
@@ -1602,3 +1602,26 @@ def side_effect(handler_name, spec_name, entity):
16021602

16031603
self.assertEqual(resp.status_code, 200)
16041604
self.assertTrue(self._test_data["is_call_custom_called"])
1605+
1606+
@mock.patch("entity.tasks.create_entity.delay", mock.Mock(side_effect=tasks.create_entity))
1607+
def test_try_to_create_duplicate_name_of_entity(self):
1608+
user = self.admin_login()
1609+
Entity.objects.create(name="hoge", created_user=user)
1610+
1611+
# Set parameters with a duplicate entity name "hoge"
1612+
params = {
1613+
"name": "hoge",
1614+
"note": "",
1615+
"is_toplevel": False,
1616+
"attrs": [],
1617+
}
1618+
1619+
resp = self.client.post(
1620+
reverse("entity:do_create"),
1621+
json.dumps(params),
1622+
"application/json",
1623+
)
1624+
1625+
# Check the response is 400 and the content is "Duplicate name entity is existed"
1626+
self.assertEqual(resp.status_code, 400)
1627+
self.assertEqual(resp.content.decode("utf-8"), "Duplicate name entity is existed")

entity/views.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -215,9 +215,7 @@ def do_edit(request, entity_id, recv_data):
215215
"name": "name",
216216
"type": str,
217217
"checker": lambda x: (
218-
x["name"]
219-
and not Entity.objects.filter(name=x["name"]).exists()
220-
and len(x["name"]) <= Entity._meta.get_field("name").max_length
218+
x["name"] and len(x["name"]) <= Entity._meta.get_field("name").max_length
221219
),
222220
},
223221
{"name": "note", "type": str},
@@ -277,6 +275,10 @@ def do_create(request, recv_data):
277275
if len([v for v, count in counter.items() if count > 1]):
278276
return HttpResponse("Duplicated attribute names are not allowed", status=400)
279277

278+
# checks that a same name entity corresponding to the entity is existed, or not.
279+
if Entity.objects.filter(name=recv_data["name"], is_active=True).exists():
280+
return HttpResponse("Duplicate name entity is existed", status=400)
281+
280282
if custom_view.is_custom("create_entity"):
281283
resp = custom_view.call_custom("create_entity", None, recv_data["name"], recv_data["attrs"])
282284
if resp:

frontend/src/ErrorHandler.tsx

+9-5
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
import { styled } from "@mui/material/styles";
1010
import React, { FC, useCallback, useEffect, useState } from "react";
1111
import { ErrorBoundary, FallbackProps } from "react-error-boundary";
12-
import { useLocation, useNavigate } from "react-router-dom";
12+
import { useNavigate } from "react-router-dom";
1313
import { useError } from "react-use";
1414

1515
import { ForbiddenErrorPage } from "./pages/ForbiddenErrorPage";
@@ -96,11 +96,15 @@ const GenericError: FC<GenericErrorProps> = ({ children }) => {
9696
};
9797

9898
const ErrorFallback: FC<FallbackProps> = ({ error, resetErrorBoundary }) => {
99-
const location = useLocation();
100-
10199
useEffect(() => {
102-
resetErrorBoundary();
103-
}, [location, resetErrorBoundary]);
100+
const handlePopState = () => {
101+
resetErrorBoundary();
102+
};
103+
window.addEventListener("popstate", handlePopState);
104+
return () => {
105+
window.removeEventListener("popstate", handlePopState);
106+
};
107+
}, [resetErrorBoundary]);
104108

105109
switch (error.name) {
106110
case ForbiddenError.errorName:

frontend/src/pages/EntityListPage.test.tsx

+48-46
Original file line numberDiff line numberDiff line change
@@ -2,59 +2,61 @@
22
* @jest-environment jsdom
33
*/
44

5-
import {
6-
render,
7-
waitForElementToBeRemoved,
8-
screen,
9-
} from "@testing-library/react";
5+
import { render, screen, waitFor } from "@testing-library/react";
6+
import { http, HttpResponse } from "msw";
7+
import { setupServer } from "msw/node";
108
import React from "react";
119

1210
import { TestWrapper } from "TestWrapper";
1311
import { EntityListPage } from "pages/EntityListPage";
1412

15-
afterEach(() => {
16-
jest.clearAllMocks();
17-
});
13+
const server = setupServer(
14+
// getEntities
15+
http.get("http://localhost/entity/api/v2/", () => {
16+
return HttpResponse.json({
17+
count: 3,
18+
next: null,
19+
previous: null,
20+
results: [
21+
{
22+
id: 1,
23+
name: "aaa",
24+
note: "",
25+
is_toplevel: false,
26+
attrs: [],
27+
},
28+
{
29+
id: 2,
30+
name: "aaaaa",
31+
note: "",
32+
is_toplevel: false,
33+
attrs: [],
34+
},
35+
{
36+
id: 3,
37+
name: "bbbbb",
38+
note: "",
39+
is_toplevel: false,
40+
attrs: [],
41+
},
42+
],
43+
});
44+
})
45+
);
1846

19-
test("should match snapshot", async () => {
20-
const entities = [
21-
{
22-
id: 1,
23-
name: "aaa",
24-
note: "",
25-
isToplevel: false,
26-
attrs: [],
27-
},
28-
{
29-
id: 2,
30-
name: "aaaaa",
31-
note: "",
32-
isToplevel: false,
33-
attrs: [],
34-
},
35-
{
36-
id: 3,
37-
name: "bbbbb",
38-
note: "",
39-
isToplevel: false,
40-
attrs: [],
41-
},
42-
];
47+
beforeAll(() => server.listen());
48+
afterEach(() => server.resetHandlers());
49+
afterAll(() => server.close());
4350

44-
/* eslint-disable */
45-
jest
46-
.spyOn(
47-
require("../repository/AironeApiClient").aironeApiClient,
48-
"getEntities"
49-
)
50-
.mockResolvedValue(Promise.resolve({ results: entities }));
51-
/* eslint-enable */
51+
describe("EntityListPage", () => {
52+
test("should match snapshot", async () => {
53+
const result = render(<EntityListPage />, {
54+
wrapper: TestWrapper,
55+
});
56+
await waitFor(() => {
57+
expect(screen.queryByTestId("loading")).not.toBeInTheDocument();
58+
});
5259

53-
// wait async calls and get rendered fragment
54-
const result = render(<EntityListPage />, {
55-
wrapper: TestWrapper,
60+
expect(result).toMatchSnapshot();
5661
});
57-
await waitForElementToBeRemoved(screen.getByTestId("loading"));
58-
59-
expect(result).toMatchSnapshot();
6062
});

frontend/src/pages/EntryListPage.test.tsx

+71-56
Original file line numberDiff line numberDiff line change
@@ -2,70 +2,85 @@
22
* @jest-environment jsdom
33
*/
44

5-
import {
6-
render,
7-
waitForElementToBeRemoved,
8-
screen,
9-
} from "@testing-library/react";
5+
import { act, render, screen, waitFor } from "@testing-library/react";
6+
import { http, HttpResponse } from "msw";
7+
import { setupServer } from "msw/node";
108
import React from "react";
9+
import { createMemoryRouter, RouterProvider } from "react-router-dom";
1110

1211
import { EntryListPage } from "./EntryListPage";
1312

14-
import { TestWrapper } from "TestWrapper";
13+
import { entityEntriesPath } from "Routes";
14+
import { TestWrapperWithoutRoutes } from "TestWrapper";
1515

16-
afterEach(() => {
17-
jest.clearAllMocks();
18-
});
19-
20-
test("should match snapshot", async () => {
21-
const entity = {
22-
id: 1,
23-
name: "aaa",
24-
note: "",
25-
isToplevel: false,
26-
attrs: [],
27-
};
28-
const entries = [
29-
{
16+
const server = setupServer(
17+
// getEntity
18+
http.get("http://localhost/entity/api/v2/1", () => {
19+
return HttpResponse.json({
3020
id: 1,
3121
name: "aaa",
32-
schema: null,
33-
isActive: true,
34-
},
35-
{
36-
id: 2,
37-
name: "aaaaa",
38-
schema: null,
39-
isActive: true,
40-
},
41-
{
42-
id: 3,
43-
name: "bbbbb",
44-
schema: null,
45-
isActive: true,
46-
},
47-
];
22+
note: "",
23+
is_toplevel: false,
24+
attrs: [],
25+
webhooks: [],
26+
});
27+
}),
28+
// getEntries
29+
http.get("http://localhost/entity/api/v2/1/entries/", () => {
30+
return HttpResponse.json({
31+
count: 2,
32+
next: null,
33+
previous: null,
34+
results: [
35+
{
36+
id: 1,
37+
name: "aaa",
38+
schema: null,
39+
is_active: true,
40+
},
41+
{
42+
id: 2,
43+
name: "aaaaa",
44+
schema: null,
45+
is_active: true,
46+
},
47+
{
48+
id: 3,
49+
name: "bbbbb",
50+
schema: null,
51+
is_active: true,
52+
},
53+
],
54+
});
55+
})
56+
);
4857

49-
/* eslint-disable */
50-
jest
51-
.spyOn(
52-
require("../repository/AironeApiClient").aironeApiClient,
53-
"getEntity"
54-
)
55-
.mockResolvedValue(Promise.resolve(entity));
56-
jest
57-
.spyOn(
58-
require("../repository/AironeApiClient").aironeApiClient,
59-
"getEntries"
60-
)
61-
.mockResolvedValue(Promise.resolve({ results: entries }));
62-
/* eslint-enable */
58+
beforeAll(() => server.listen());
59+
afterEach(() => server.resetHandlers());
60+
afterAll(() => server.close());
6361

64-
// wait async calls and get rendered fragment
65-
const result = render(<EntryListPage />, {
66-
wrapper: TestWrapper,
67-
});
68-
await waitForElementToBeRemoved(screen.getByTestId("loading"));
62+
describe("EntryListPage", () => {
63+
test("should match snapshot", async () => {
64+
const router = createMemoryRouter(
65+
[
66+
{
67+
path: entityEntriesPath(":entityId"),
68+
element: <EntryListPage />,
69+
},
70+
],
71+
{
72+
initialEntries: ["/ui/entities/1/entries"],
73+
}
74+
);
75+
const result = await act(async () => {
76+
return render(<RouterProvider router={router} />, {
77+
wrapper: TestWrapperWithoutRoutes,
78+
});
79+
});
80+
await waitFor(() => {
81+
expect(screen.queryByTestId("loading")).not.toBeInTheDocument();
82+
});
6983

70-
expect(result).toMatchSnapshot();
84+
expect(result).toMatchSnapshot();
85+
});
7186
});

0 commit comments

Comments
 (0)