Skip to content

Commit 63132cf

Browse files
authored
Merge pull request dmm-com#1646 from syucream/fix/reduce-non-core-modules
chore: update dependencies and remove deprecated packages in package.json
2 parents 05f69e4 + 5b6ea83 commit 63132cf

19 files changed

+971
-844
lines changed

frontend/src/ErrorHandler.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ import {
99
import { styled } from "@mui/material/styles";
1010
import { FC, ReactNode, useCallback, useEffect, useState } from "react";
1111
import { ErrorBoundary, FallbackProps } from "react-error-boundary";
12-
import { useError } from "react-use";
1312

13+
import { useError } from "./hooks/useError";
1414
import { ForbiddenErrorPage } from "./pages/ForbiddenErrorPage";
1515
import { NonTermsServiceAgreementPage } from "./pages/NonTermsServiceAgreement";
1616
import { NotFoundErrorPage } from "./pages/NotFoundErrorPage";

frontend/src/components/common/Header.test.tsx

Lines changed: 126 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,24 @@ import { Header } from "./Header";
88

99
import { TestWrapper } from "TestWrapper";
1010

11-
// Mock ServerContext
11+
// Mock ServerContext - configurable per test
12+
const defaultServerContext = {
13+
title: "Airone",
14+
version: "1.0.0",
15+
headerColor: "#1976d2",
16+
legacyUiDisabled: true,
17+
extendedHeaderMenus: [] as Array<{
18+
name: string;
19+
children: Array<{ name: string; url: string }>;
20+
}>,
21+
user: { id: 1, username: "testuser" },
22+
};
23+
24+
let mockServerContext = { ...defaultServerContext };
25+
1226
jest.mock("../../services/ServerContext", () => ({
1327
ServerContext: {
14-
getInstance: () => ({
15-
title: "Airone",
16-
version: "1.0.0",
17-
headerColor: "#1976d2",
18-
legacyUiDisabled: true,
19-
extendedHeaderMenus: [],
20-
user: { id: 1, username: "testuser" },
21-
}),
28+
getInstance: () => mockServerContext,
2229
},
2330
}));
2431

@@ -61,12 +68,16 @@ jest.mock("../../repository/AironeApiClient", () => ({
6168
},
6269
}));
6370

64-
// Mock react-use useInterval
65-
jest.mock("react-use", () => ({
71+
// Mock useInterval
72+
jest.mock("../../hooks/useInterval", () => ({
6673
useInterval: jest.fn(),
6774
}));
6875

6976
describe("Header", () => {
77+
beforeEach(() => {
78+
mockServerContext = { ...defaultServerContext, extendedHeaderMenus: [] };
79+
});
80+
7081
describe("rendering", () => {
7182
test("should render header with title", () => {
7283
render(<Header />, { wrapper: TestWrapper });
@@ -149,6 +160,110 @@ describe("Header", () => {
149160
expect(screen.getByText("Triggers")).toBeInTheDocument();
150161
});
151162
});
163+
164+
test("should close management submenu on mouse leave", async () => {
165+
render(<Header />, { wrapper: TestWrapper });
166+
167+
const managementButton = screen.getByText("Management");
168+
fireEvent.mouseEnter(managementButton);
169+
170+
await waitFor(() => {
171+
expect(screen.getByText("Users")).toBeInTheDocument();
172+
});
173+
174+
// Find the menu list and trigger mouseLeave
175+
const menuItems = screen.getByText("Users").closest("ul");
176+
if (menuItems) {
177+
fireEvent.mouseLeave(menuItems);
178+
}
179+
180+
await waitFor(() => {
181+
expect(screen.queryByText("Users")).not.toBeVisible();
182+
});
183+
});
184+
});
185+
186+
describe("extended header menus", () => {
187+
test("should show extended menu items on hover", async () => {
188+
mockServerContext = {
189+
...defaultServerContext,
190+
extendedHeaderMenus: [
191+
{
192+
name: "External Tools",
193+
children: [
194+
{ name: "Tool A", url: "https://tool-a.example.com" },
195+
{ name: "Tool B", url: "https://tool-b.example.com" },
196+
],
197+
},
198+
],
199+
};
200+
201+
render(<Header />, { wrapper: TestWrapper });
202+
203+
const extendedMenuButton = screen.getByText("External Tools");
204+
expect(extendedMenuButton).toBeInTheDocument();
205+
206+
fireEvent.mouseEnter(extendedMenuButton);
207+
208+
await waitFor(() => {
209+
expect(screen.getByText("Tool A")).toBeInTheDocument();
210+
expect(screen.getByText("Tool B")).toBeInTheDocument();
211+
});
212+
});
213+
214+
test("should close extended menu on mouse leave", async () => {
215+
mockServerContext = {
216+
...defaultServerContext,
217+
extendedHeaderMenus: [
218+
{
219+
name: "External Tools",
220+
children: [{ name: "Tool A", url: "https://tool-a.example.com" }],
221+
},
222+
],
223+
};
224+
225+
render(<Header />, { wrapper: TestWrapper });
226+
227+
const extendedMenuButton = screen.getByText("External Tools");
228+
fireEvent.mouseEnter(extendedMenuButton);
229+
230+
await waitFor(() => {
231+
expect(screen.getByText("Tool A")).toBeInTheDocument();
232+
});
233+
234+
const menuItems = screen.getByText("Tool A").closest("ul");
235+
if (menuItems) {
236+
fireEvent.mouseLeave(menuItems);
237+
}
238+
239+
await waitFor(() => {
240+
expect(screen.queryByText("Tool A")).not.toBeVisible();
241+
});
242+
});
243+
244+
test("should render links with correct href in extended menu", async () => {
245+
mockServerContext = {
246+
...defaultServerContext,
247+
extendedHeaderMenus: [
248+
{
249+
name: "External Tools",
250+
children: [{ name: "Tool A", url: "https://tool-a.example.com" }],
251+
},
252+
],
253+
};
254+
255+
render(<Header />, { wrapper: TestWrapper });
256+
257+
fireEvent.mouseEnter(screen.getByText("External Tools"));
258+
259+
await waitFor(() => {
260+
const toolLink = screen.getByText("Tool A");
261+
expect(toolLink.closest("a")).toHaveAttribute(
262+
"href",
263+
"https://tool-a.example.com",
264+
);
265+
});
266+
});
152267
});
153268

154269
describe("search functionality", () => {

frontend/src/components/common/Header.tsx

Lines changed: 69 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,13 @@ import {
1717
} from "@mui/material";
1818
import { OverridableComponent } from "@mui/material/OverridableComponent";
1919
import { styled } from "@mui/material/styles";
20-
import PopupState, { bindHover, bindMenu } from "material-ui-popup-state";
21-
import HoverMenu from "material-ui-popup-state/HoverMenu";
2220
import { FC, Fragment, MouseEvent, useMemo, useState } from "react";
2321
import { Link } from "react-router";
24-
import { useInterval } from "react-use";
2522

2623
import { useTranslation } from "../../hooks/useTranslation";
2724

2825
import { SearchBox } from "components/common/SearchBox";
26+
import { useInterval } from "hooks/useInterval";
2927
import { useSimpleSearch } from "hooks/useSimpleSearch";
3028
import { aironeApiClient } from "repository/AironeApiClient";
3129
import {
@@ -123,6 +121,11 @@ export const Header: FC = () => {
123121

124122
const [userAnchorEl, setUserAnchorEl] = useState<HTMLButtonElement | null>();
125123
const [jobAnchorEl, setJobAnchorEl] = useState<HTMLButtonElement | null>();
124+
const [managementAnchorEl, setManagementAnchorEl] =
125+
useState<HTMLElement | null>(null);
126+
const [extendedMenuAnchors, setExtendedMenuAnchors] = useState<
127+
Record<number, HTMLElement | null>
128+
>({});
126129
const [latestCheckDate, setLatestCheckDate] = useState<Date | null>(
127130
getLatestCheckDate(),
128131
);
@@ -188,51 +191,73 @@ export const Header: FC = () => {
188191
<Button component={Link} to={advancedSearchPath()}>
189192
{t("advancedSearch")}
190193
</Button>
191-
<PopupState variant="popover" popupId="basic">
192-
{(popupState) => (
193-
<Fragment>
194-
<Button {...bindHover(popupState)}>
195-
{t("management")}
196-
<KeyboardArrowDownIcon fontSize="small" />
197-
</Button>
198-
<HoverMenu {...bindMenu(popupState)}>
199-
<MenuItem component={Link} to={usersPath()}>
200-
{t("manageUsers")}
201-
</MenuItem>
202-
<MenuItem component={Link} to={groupsPath()}>
203-
{t("manageGroups")}
204-
</MenuItem>
205-
<MenuItem component={Link} to={rolesPath()}>
206-
{t("manageRoles")}
207-
</MenuItem>
208-
<MenuItem component={Link} to={triggersPath()}>
209-
{t("manageTriggers")}
210-
</MenuItem>
211-
</HoverMenu>
212-
</Fragment>
213-
)}
214-
</PopupState>
194+
<Button
195+
onMouseEnter={(e) => setManagementAnchorEl(e.currentTarget)}
196+
>
197+
{t("management")}
198+
<KeyboardArrowDownIcon fontSize="small" />
199+
</Button>
200+
<Menu
201+
anchorEl={managementAnchorEl}
202+
open={Boolean(managementAnchorEl)}
203+
onClose={() => setManagementAnchorEl(null)}
204+
MenuListProps={{
205+
onMouseLeave: () => setManagementAnchorEl(null),
206+
}}
207+
>
208+
<MenuItem component={Link} to={usersPath()}>
209+
{t("manageUsers")}
210+
</MenuItem>
211+
<MenuItem component={Link} to={groupsPath()}>
212+
{t("manageGroups")}
213+
</MenuItem>
214+
<MenuItem component={Link} to={rolesPath()}>
215+
{t("manageRoles")}
216+
</MenuItem>
217+
<MenuItem component={Link} to={triggersPath()}>
218+
{t("manageTriggers")}
219+
</MenuItem>
220+
</Menu>
215221

216222
{/* If there is another menu settings are passed from Server,
217223
this represent another menu*/}
218224
{serverContext?.extendedHeaderMenus.map((menu, index) => (
219-
<PopupState variant="popover" popupId={menu.name} key={index}>
220-
{(popupState) => (
221-
<Fragment>
222-
<Button {...bindHover(popupState)}>
223-
{menu.name}
224-
<KeyboardArrowDownIcon fontSize="small" />
225-
</Button>
226-
<HoverMenu {...bindMenu(popupState)}>
227-
{menu.children.map((child, index) => (
228-
<MenuItem key={index} component="a" href={child.url}>
229-
{child.name}
230-
</MenuItem>
231-
))}
232-
</HoverMenu>
233-
</Fragment>
234-
)}
235-
</PopupState>
225+
<Fragment key={index}>
226+
<Button
227+
onMouseEnter={(e) =>
228+
setExtendedMenuAnchors((prev) => ({
229+
...prev,
230+
[index]: e.currentTarget,
231+
}))
232+
}
233+
>
234+
{menu.name}
235+
<KeyboardArrowDownIcon fontSize="small" />
236+
</Button>
237+
<Menu
238+
anchorEl={extendedMenuAnchors[index]}
239+
open={Boolean(extendedMenuAnchors[index])}
240+
onClose={() =>
241+
setExtendedMenuAnchors((prev) => ({
242+
...prev,
243+
[index]: null,
244+
}))
245+
}
246+
MenuListProps={{
247+
onMouseLeave: () =>
248+
setExtendedMenuAnchors((prev) => ({
249+
...prev,
250+
[index]: null,
251+
})),
252+
}}
253+
>
254+
{menu.children.map((child, childIndex) => (
255+
<MenuItem key={childIndex} component="a" href={child.url}>
256+
{child.name}
257+
</MenuItem>
258+
))}
259+
</Menu>
260+
</Fragment>
236261
))}
237262
</MenuBox>
238263

frontend/src/components/entry/SearchResultControlMenuForReferral.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@ import {
1616
} from "@mui/material";
1717
import { styled } from "@mui/material/styles";
1818
import { ChangeEvent, Dispatch, FC, KeyboardEvent, useState } from "react";
19-
import { useAsync } from "react-use";
2019

2120
import { handleSelectFilterConditionsParams } from "./SearchResultsTableHead";
2221

22+
import { useAsync } from "hooks/useAsync";
2323
import { aironeApiClient } from "repository";
2424

2525
const StyledTextField = styled(TextField)({

frontend/src/hooks/useAsync.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { useEffect, useState } from "react";
2+
3+
interface AsyncState<T> {
4+
value?: T;
5+
loading: boolean;
6+
error?: Error;
7+
}
8+
9+
export function useAsync<T>(
10+
fn: () => Promise<T>,
11+
deps?: unknown[],
12+
): AsyncState<T> {
13+
const [state, setState] = useState<AsyncState<T>>({ loading: true });
14+
useEffect(() => {
15+
setState((s) => ({ ...s, loading: true }));
16+
fn()
17+
.then((value) => setState({ value, loading: false }))
18+
.catch((error) => setState({ error, loading: false }));
19+
}, deps ?? []);
20+
return state;
21+
}

frontend/src/hooks/useError.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { useCallback, useState } from "react";
2+
3+
export function useError(): (error: Error) => void {
4+
const [, setState] = useState();
5+
return useCallback((error: Error) => {
6+
setState(() => {
7+
throw error;
8+
});
9+
}, []);
10+
}

frontend/src/hooks/useInterval.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { useEffect, useRef } from "react";
2+
3+
export function useInterval(callback: () => void, delay: number | null): void {
4+
const savedCallback = useRef(callback);
5+
useEffect(() => {
6+
savedCallback.current = callback;
7+
});
8+
useEffect(() => {
9+
if (delay === null) return;
10+
const id = setInterval(() => savedCallback.current(), delay);
11+
return () => clearInterval(id);
12+
}, [delay]);
13+
}

frontend/src/pages/AliasEntryListPage.test.tsx

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,6 @@ import { createMemoryRouter, RouterProvider } from "react-router";
1010
import { TestWrapperWithoutRoutes } from "TestWrapper";
1111
import { AliasEntryListPage } from "pages/AliasEntryListPage";
1212

13-
// Setup mock location
14-
const mockLocation = {
15-
pathname: "/ui/entities/1/alias",
16-
search: "",
17-
hash: "",
18-
state: null,
19-
};
20-
21-
// Mock useLocation
22-
jest.mock("react-use", () => ({
23-
...jest.requireActual("react-use"),
24-
useLocation: () => mockLocation,
25-
}));
26-
2713
const server = setupServer(
2814
// GET /entity/api/v2/:entityId/
2915
http.get("http://localhost/entity/api/v2/1/", () => {

0 commit comments

Comments
 (0)