Skip to content

Commit 225a736

Browse files
Merge pull request #1114 from Arnei/reduce-duplicate-code-tableactions
Reduce duplicate code for table action dropdown
2 parents 86d0a06 + f8ea5b1 commit 225a736

3 files changed

Lines changed: 134 additions & 115 deletions

File tree

src/components/events/Events.tsx

Lines changed: 26 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import React, { useEffect, useRef, useState } from "react";
22
import { useTranslation } from "react-i18next";
3-
import cn from "classnames";
43
import { useLocation } from "react-router";
54
import TableFilters from "../shared/TableFilters";
65
import Stats from "../shared/Stats";
@@ -36,14 +35,11 @@ import {
3635
} from "../../slices/eventSlice";
3736
import EventDetailsModal from "./partials/modals/EventDetailsModal";
3837
import { showModal } from "../../selectors/eventDetailsSelectors";
39-
import ButtonLikeAnchor from "../shared/ButtonLikeAnchor";
4038
import { eventsLinks } from "./partials/EventsNavigation";
4139
import { Modal, ModalHandle } from "../shared/modals/Modal";
40+
import TableActionDropdown from "../shared/TableActionDropdown";
4241
import { resetTableProperties } from "../../slices/tableSlice";
4342

44-
// References for detecting a click outside of the container of the dropdown menu
45-
const containerAction = React.createRef<HTMLDivElement>();
46-
4743
/**
4844
* This component renders the table view of events
4945
*/
@@ -53,7 +49,6 @@ const Events = () => {
5349

5450
const displayEventDetailsModal = useAppSelector(state => showModal(state));
5551

56-
const [displayActionMenu, setActionMenu] = useState(false);
5752
const [displayNavigation, setNavigation] = useState(false);
5853
const newEventModalRef = useRef<ModalHandle>(null);
5954
const startTaskModalRef = useRef<ModalHandle>(null);
@@ -96,35 +91,16 @@ const Events = () => {
9691
// call the function
9792
loadEvents();
9893

99-
// Function for handling clicks outside of an open dropdown menu
100-
const handleClickOutside = (e: MouseEvent) => {
101-
if (
102-
containerAction.current &&
103-
!containerAction.current.contains(e.target as Node)
104-
) {
105-
setActionMenu(false);
106-
}
107-
};
108-
10994
// Fetch events every five seconds
11095
let fetchEventsInterval = setInterval(() => loadEvents(), 5000);
11196

112-
// Event listener for handle a click outside of dropdown menu
113-
window.addEventListener("mousedown", handleClickOutside);
114-
11597
return () => {
11698
allowLoadIntoTable = false;
117-
window.removeEventListener("mousedown", handleClickOutside);
11899
clearInterval(fetchEventsInterval);
119100
};
120101
// eslint-disable-next-line react-hooks/exhaustive-deps
121102
}, [location.hash]);
122103

123-
const handleActionMenu = (e: React.MouseEvent) => {
124-
e.preventDefault();
125-
setActionMenu(!displayActionMenu);
126-
};
127-
128104
const onNewEventModal = async () => {
129105
await dispatch(fetchEventMetadata());
130106
await dispatch(fetchAssetUploadOptions());
@@ -215,48 +191,31 @@ const Events = () => {
215191

216192
<div className="controls-container">
217193
<div className="filters-container">
218-
<div
219-
className={cn("drop-down-container", { disabled: !showActions })}
220-
onClick={(e) => handleActionMenu(e)}
221-
ref={containerAction}
222-
>
223-
<span>{t("BULK_ACTIONS.CAPTION")}</span>
224-
{/* show dropdown if actions is clicked*/}
225-
{displayActionMenu && (
226-
<ul className="dropdown-ul">
227-
{hasAccess("ROLE_UI_EVENTS_DELETE", user) && (
228-
<li>
229-
<ButtonLikeAnchor onClick={() => deleteModalRef.current?.open()}>
230-
{t("BULK_ACTIONS.DELETE.EVENTS.CAPTION")}
231-
</ButtonLikeAnchor>
232-
</li>
233-
)}
234-
{hasAccess("ROLE_UI_TASKS_CREATE", user) && (
235-
<li>
236-
<ButtonLikeAnchor onClick={() => startTaskModalRef.current?.open()}>
237-
{t("BULK_ACTIONS.SCHEDULE_TASK.CAPTION")}
238-
</ButtonLikeAnchor>
239-
</li>
240-
)}
241-
{hasAccess("ROLE_UI_EVENTS_DETAILS_SCHEDULING_EDIT", user) &&
242-
hasAccess("ROLE_UI_EVENTS_DETAILS_METADATA_EDIT", user) && (
243-
<li>
244-
<ButtonLikeAnchor onClick={() => editScheduledEventsModalRef.current?.open()}>
245-
{t("BULK_ACTIONS.EDIT_EVENTS.CAPTION")}
246-
</ButtonLikeAnchor>
247-
</li>
248-
)}
249-
{hasAccess("ROLE_UI_EVENTS_DETAILS_METADATA_EDIT", user) && (
250-
<li>
251-
<ButtonLikeAnchor onClick={() => editMetadataEventsModalRef.current?.open()}>
252-
{t("BULK_ACTIONS.EDIT_EVENTS_METADATA.CAPTION")}
253-
</ButtonLikeAnchor>
254-
</li>
255-
)}
256-
</ul>
257-
)}
258-
</div>
259-
194+
<TableActionDropdown
195+
actions={[
196+
{
197+
accessRole: ["ROLE_UI_EVENTS_DELETE"],
198+
handleOnClick: () => deleteModalRef.current?.open(),
199+
text: "BULK_ACTIONS.DELETE.EVENTS.CAPTION",
200+
},
201+
{
202+
accessRole: ["ROLE_UI_TASKS_CREATE"],
203+
handleOnClick: () => startTaskModalRef.current?.open(),
204+
text: "BULK_ACTIONS.SCHEDULE_TASK.CAPTION",
205+
},
206+
{
207+
accessRole: ["ROLE_UI_EVENTS_DETAILS_SCHEDULING_EDIT", "ROLE_UI_EVENTS_DETAILS_METADATA_EDIT"],
208+
handleOnClick: () => editScheduledEventsModalRef.current?.open(),
209+
text: "BULK_ACTIONS.EDIT_EVENTS.CAPTION",
210+
},
211+
{
212+
accessRole: ["ROLE_UI_EVENTS_DETAILS_METADATA_EDIT"],
213+
handleOnClick: () => editMetadataEventsModalRef.current?.open(),
214+
text: "BULK_ACTIONS.EDIT_EVENTS_METADATA.CAPTION",
215+
}
216+
]}
217+
disabled={!showActions}
218+
/>
260219
{/* Include filters component*/}
261220
<TableFilters
262221
loadResource={fetchEvents}

src/components/events/Series.tsx

Lines changed: 11 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import React, { useEffect, useRef, useState } from "react";
22
import { useTranslation } from "react-i18next";
3-
import cn from "classnames";
43
import { useLocation } from "react-router";
54
import TableFilters from "../shared/TableFilters";
65
import Table from "../shared/Table";
@@ -16,8 +15,6 @@ import Header from "../Header";
1615
import NavBar from "../NavBar";
1716
import MainView from "../MainView";
1817
import Footer from "../Footer";
19-
import { getUserInformation } from "../../selectors/userInfoSelectors";
20-
import { hasAccess } from "../../utils/utils";
2118
import { useAppDispatch, useAppSelector } from "../../store";
2219
import {
2320
fetchSeries,
@@ -26,28 +23,22 @@ import {
2623
showActionsSeries,
2724
} from "../../slices/seriesSlice";
2825
import { fetchSeriesDetailsTobiraNew } from "../../slices/seriesSlice";
29-
import ButtonLikeAnchor from "../shared/ButtonLikeAnchor";
3026
import { eventsLinks } from "./partials/EventsNavigation";
3127
import { Modal, ModalHandle } from "../shared/modals/Modal";
3228
import { availableHotkeys } from "../../configs/hotkeysConfig";
29+
import TableActionDropdown from "../shared/TableActionDropdown";
3330
import { resetTableProperties } from "../../slices/tableSlice";
3431

35-
// References for detecting a click outside of the container of the dropdown menu
36-
const containerAction = React.createRef<HTMLDivElement>();
37-
3832
/**
3933
* This component renders the table view of series
4034
*/
4135
const Series = () => {
4236
const { t } = useTranslation();
4337
const dispatch = useAppDispatch();
44-
const [displayActionMenu, setActionMenu] = useState(false);
4538
const [displayNavigation, setNavigation] = useState(false);
4639
const newSeriesModalRef = useRef<ModalHandle>(null);
4740
const deleteModalRef = useRef<ModalHandle>(null);
4841

49-
const user = useAppSelector(state => getUserInformation(state));
50-
5142
let location = useLocation();
5243

5344
const series = useAppSelector(state => getTotalSeries(state));
@@ -80,35 +71,16 @@ const Series = () => {
8071
};
8172
loadSeries();
8273

83-
// Function for handling clicks outside of an dropdown menu
84-
const handleClickOutside = (e: MouseEvent) => {
85-
if (
86-
containerAction.current &&
87-
!containerAction.current.contains(e.target as Node)
88-
) {
89-
setActionMenu(false);
90-
}
91-
};
92-
9374
// Fetch series every minute
9475
let fetchSeriesInterval = setInterval(() => loadSeries(), 5000);
9576

96-
// Event listener for handle a click outside of dropdown menu
97-
window.addEventListener("mousedown", handleClickOutside);
98-
9977
return () => {
10078
allowLoadIntoTable = false;
101-
window.removeEventListener("mousedown", handleClickOutside);
10279
clearInterval(fetchSeriesInterval);
10380
};
10481
// eslint-disable-next-line react-hooks/exhaustive-deps
10582
}, [location.hash]);
10683

107-
const handleActionMenu = (e: React.MouseEvent) => {
108-
e.preventDefault();
109-
setActionMenu(!displayActionMenu);
110-
};
111-
11284
const onNewSeriesModal = async () => {
11385
await dispatch(fetchSeriesMetadata());
11486
await dispatch(fetchSeriesThemes());
@@ -155,25 +127,16 @@ const Series = () => {
155127

156128
<div className="controls-container">
157129
<div className="filters-container">
158-
<div
159-
className={cn("drop-down-container", { disabled: !showActions })}
160-
onClick={(e) => handleActionMenu(e)}
161-
ref={containerAction}
162-
>
163-
<span>{t("BULK_ACTIONS.CAPTION")}</span>
164-
{/* show dropdown if actions is clicked*/}
165-
{displayActionMenu && (
166-
<ul className="dropdown-ul">
167-
{hasAccess("ROLE_UI_SERIES_DELETE", user) && (
168-
<li>
169-
<ButtonLikeAnchor onClick={() => deleteModalRef.current?.open()}>
170-
{t("BULK_ACTIONS.DELETE.SERIES.CAPTION")}
171-
</ButtonLikeAnchor>
172-
</li>
173-
)}
174-
</ul>
175-
)}
176-
</div>
130+
<TableActionDropdown
131+
actions={[
132+
{
133+
accessRole: ["ROLE_UI_SERIES_DELETE"],
134+
handleOnClick: () => deleteModalRef.current?.open(),
135+
text: "BULK_ACTIONS.DELETE.SERIES.CAPTION",
136+
},
137+
]}
138+
disabled={!showActions}
139+
/>
177140
{/* Include filters component */}
178141
<TableFilters
179142
loadResource={fetchSeries}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import React, { useEffect, useState } from "react";
2+
import cn from "classnames";
3+
import { hasAccess } from "../../utils/utils";
4+
import { useTranslation } from "react-i18next";
5+
import { useAppSelector } from "../../store";
6+
import { getUserInformation } from "../../selectors/userInfoSelectors";
7+
import { ParseKeys } from "i18next";
8+
import ButtonLikeAnchor from "./ButtonLikeAnchor";
9+
10+
// References for detecting a click outside of the container of the dropdown menu
11+
const containerAction = React.createRef<HTMLDivElement>();
12+
13+
/**
14+
* A dropdown menu displayed above a table. The menu contains actions that can
15+
* affect one or multiple selected entries in the table.
16+
*/
17+
const TableActionDropdown = ({
18+
actions,
19+
disabled=true,
20+
}: {
21+
actions: React.ComponentProps<typeof Action>[]
22+
disabled: boolean
23+
}) => {
24+
const { t } = useTranslation();
25+
26+
const [displayActionMenu, setActionMenu] = useState(false);
27+
28+
useEffect(() => {
29+
// Function for handling clicks outside of an open dropdown menu
30+
const handleClickOutside = (e: MouseEvent) => {
31+
if (
32+
containerAction.current &&
33+
!containerAction.current.contains(e.target as Node)
34+
) {
35+
setActionMenu(false);
36+
}
37+
};
38+
39+
// Event listener for handle a click outside of dropdown menu
40+
window.addEventListener("mousedown", handleClickOutside);
41+
42+
return () => {
43+
window.removeEventListener("mousedown", handleClickOutside);
44+
};
45+
}, []);
46+
47+
const handleActionMenu = (e: React.MouseEvent) => {
48+
e.preventDefault();
49+
setActionMenu(!displayActionMenu);
50+
};
51+
52+
return (
53+
<div
54+
className={cn("drop-down-container", { disabled: disabled })}
55+
onClick={(e) => handleActionMenu(e)}
56+
ref={containerAction}
57+
>
58+
<span>{t("BULK_ACTIONS.CAPTION")}</span>
59+
{/* show dropdown if actions is clicked*/}
60+
{displayActionMenu && (
61+
<ul className="dropdown-ul">
62+
{actions.map((action =>
63+
<Action
64+
{...action}
65+
/>
66+
))}
67+
</ul>
68+
)}
69+
</div>
70+
)
71+
};
72+
73+
const Action = ({
74+
accessRole,
75+
handleOnClick,
76+
text,
77+
}: {
78+
accessRole: string[]
79+
handleOnClick: (() => unknown) | void | undefined
80+
text: ParseKeys
81+
}) => {
82+
const { t } = useTranslation();
83+
const user = useAppSelector(state => getUserInformation(state));
84+
85+
return (
86+
!!handleOnClick &&
87+
accessRole.every((accessRole) => hasAccess(accessRole, user)) && (
88+
<li>
89+
<ButtonLikeAnchor onClick={handleOnClick}>
90+
{t(text)}
91+
</ButtonLikeAnchor>
92+
</li>
93+
)
94+
)
95+
}
96+
97+
export default TableActionDropdown;

0 commit comments

Comments
 (0)