Skip to content

Commit be34d7b

Browse files
authored
Merge pull request KelvinTegelaar#3500 from Jr7468/dev
Add speed dial actions for bug reporting and feature requests
2 parents 7c115b1 + d6eaf09 commit be34d7b

File tree

3 files changed

+263
-0
lines changed

3 files changed

+263
-0
lines changed

public/discord-mark-blue.svg

Lines changed: 1 addition & 0 deletions
Loading
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
import React, { useState, useEffect } from "react";
2+
import {
3+
SpeedDial,
4+
SpeedDialAction,
5+
SpeedDialIcon,
6+
Dialog,
7+
DialogTitle,
8+
DialogContent,
9+
DialogActions,
10+
Button,
11+
Snackbar,
12+
Alert,
13+
CircularProgress,
14+
} from "@mui/material";
15+
import { Close as CloseIcon } from "@mui/icons-material";
16+
import { useForm } from "react-hook-form";
17+
import { CippFormComponent } from "../../components/CippComponents/CippFormComponent";
18+
19+
const CippSpeedDial = ({
20+
actions = [],
21+
position = { bottom: 16, right: 16 },
22+
icon,
23+
openIcon = <CloseIcon />,
24+
}) => {
25+
const [openDialogs, setOpenDialogs] = useState({});
26+
const [loading, setLoading] = useState(false);
27+
const [showSnackbar, setShowSnackbar] = useState(false);
28+
const [speedDialOpen, setSpeedDialOpen] = useState(false);
29+
const [isHovering, setIsHovering] = useState(false);
30+
const [snackbarMessage, setSnackbarMessage] = useState("");
31+
32+
const formControls = actions.reduce((acc, action) => {
33+
if (action.form) {
34+
acc[action.id] = useForm({
35+
mode: "onChange",
36+
defaultValues: action.form.defaultValues || {},
37+
});
38+
}
39+
return acc;
40+
}, {});
41+
42+
const handleSpeedDialClose = () => {
43+
if (!isHovering) {
44+
setTimeout(() => {
45+
setSpeedDialOpen(false);
46+
}, 200);
47+
}
48+
};
49+
50+
const handleMouseEnter = () => {
51+
setIsHovering(true);
52+
setSpeedDialOpen(true);
53+
};
54+
55+
const handleMouseLeave = () => {
56+
setIsHovering(false);
57+
handleSpeedDialClose();
58+
};
59+
60+
const handleDialogOpen = (actionId) => {
61+
setOpenDialogs((prev) => ({ ...prev, [actionId]: true }));
62+
};
63+
64+
const handleDialogClose = (actionId) => {
65+
setOpenDialogs((prev) => ({ ...prev, [actionId]: false }));
66+
};
67+
68+
const handleSubmit = async (actionId, data) => {
69+
if (!actions.find((a) => a.id === actionId)?.onSubmit) return;
70+
71+
setLoading(true);
72+
try {
73+
const action = actions.find((a) => a.id === actionId);
74+
const result = await action.onSubmit(data);
75+
76+
if (result.success) {
77+
formControls[actionId]?.reset();
78+
handleDialogClose(actionId);
79+
}
80+
setSnackbarMessage(result.message);
81+
setShowSnackbar(true);
82+
} catch (error) {
83+
console.error(`Error submitting ${actionId}:`, error);
84+
setSnackbarMessage("An error occurred while submitting");
85+
setShowSnackbar(true);
86+
} finally {
87+
setLoading(false);
88+
}
89+
};
90+
91+
useEffect(() => {
92+
const handleClickOutside = (event) => {
93+
if (speedDialOpen) {
94+
const speedDial = document.querySelector('[aria-label="Navigation SpeedDial"]');
95+
if (speedDial && !speedDial.contains(event.target)) {
96+
setSpeedDialOpen(false);
97+
}
98+
}
99+
};
100+
101+
document.addEventListener("click", handleClickOutside);
102+
return () => {
103+
document.removeEventListener("click", handleClickOutside);
104+
};
105+
}, [speedDialOpen]);
106+
107+
return (
108+
<>
109+
<SpeedDial
110+
ariaLabel="Navigation SpeedDial"
111+
sx={{
112+
position: "fixed",
113+
...position,
114+
"& .MuiFab-primary": {
115+
width: 46,
116+
height: 46,
117+
"&:hover": {
118+
backgroundColor: "primary.dark",
119+
},
120+
},
121+
}}
122+
icon={<SpeedDialIcon icon={icon} openIcon={openIcon} />}
123+
open={speedDialOpen}
124+
onClose={handleSpeedDialClose}
125+
onOpen={() => setSpeedDialOpen(true)}
126+
onMouseEnter={handleMouseEnter}
127+
onMouseLeave={handleMouseLeave}
128+
>
129+
{actions.map((action) => (
130+
<SpeedDialAction
131+
key={action.id}
132+
icon={action.icon}
133+
tooltipTitle={action.name}
134+
onClick={() => {
135+
if (action.form) {
136+
handleDialogOpen(action.id);
137+
} else if (action.onClick) {
138+
action.onClick();
139+
}
140+
setSpeedDialOpen(false);
141+
}}
142+
tooltipOpen
143+
sx={{
144+
"&.MuiSpeedDialAction-fab": {
145+
backgroundColor: "background.paper",
146+
"&:hover": {
147+
backgroundColor: "action.hover",
148+
},
149+
},
150+
"& .MuiSpeedDialAction-staticTooltipLabel": {
151+
cursor: "pointer",
152+
whiteSpace: "nowrap",
153+
marginRight: "10px",
154+
padding: "6px 10px",
155+
"&:hover": {
156+
backgroundColor: "action.hover",
157+
},
158+
},
159+
}}
160+
/>
161+
))}
162+
</SpeedDial>
163+
164+
{actions
165+
.filter((action) => action.form)
166+
.map((action) => (
167+
<Dialog
168+
key={action.id}
169+
open={openDialogs[action.id] || false}
170+
onClose={() => handleDialogClose(action.id)}
171+
maxWidth="md"
172+
fullWidth
173+
>
174+
<DialogTitle>{action.form.title}</DialogTitle>
175+
<DialogContent>
176+
<CippFormComponent
177+
type="richText"
178+
name={action.form.fieldName}
179+
required
180+
formControl={formControls[action.id]}
181+
style={{ minHeight: "150px" }}
182+
editorProps={{
183+
attributes: {
184+
style: "min-height: 150px; font-size: 1.1rem; padding: 1rem;",
185+
},
186+
}}
187+
/>
188+
</DialogContent>
189+
<DialogActions>
190+
<Button onClick={() => handleDialogClose(action.id)} disabled={loading}>
191+
Cancel
192+
</Button>
193+
<Button
194+
onClick={formControls[action.id]?.handleSubmit((data) =>
195+
handleSubmit(action.id, data)
196+
)}
197+
variant="contained"
198+
color="primary"
199+
disabled={loading}
200+
startIcon={loading ? <CircularProgress size={20} /> : null}
201+
>
202+
{loading ? "Submitting..." : action.form.submitText || "Submit"}
203+
</Button>
204+
</DialogActions>
205+
</Dialog>
206+
))}
207+
208+
<Snackbar
209+
open={showSnackbar}
210+
autoHideDuration={6000}
211+
onClose={() => setShowSnackbar(false)}
212+
anchorOrigin={{ vertical: "bottom", horizontal: "center" }}
213+
>
214+
<Alert onClose={() => setShowSnackbar(false)} severity="success" sx={{ width: "100%" }}>
215+
{snackbarMessage}
216+
</Alert>
217+
</Snackbar>
218+
</>
219+
);
220+
};
221+
222+
export default CippSpeedDial;

src/pages/_app.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,14 @@ import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
2020
import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns";
2121
import TimeAgo from "javascript-time-ago";
2222
import en from "javascript-time-ago/locale/en.json";
23+
import CippSpeedDial from "../components/CippComponents/CippSpeedDial";
24+
import {
25+
Help as HelpIcon,
26+
BugReport as BugReportIcon,
27+
Feedback as FeedbackIcon,
28+
} from "@mui/icons-material";
29+
import { SvgIcon } from "@mui/material";
30+
import discordIcon from "../../public/discord-mark-blue.svg";
2331
import React from "react";
2432
TimeAgo.addDefaultLocale(en);
2533

@@ -36,6 +44,33 @@ const App = (props) => {
3644
const getLayout = Component.getLayout ?? ((page) => page);
3745
const preferredTheme = useMediaPredicate("(prefers-color-scheme: dark)") ? "dark" : "light";
3846

47+
const speedDialActions = [
48+
{
49+
id: "bug-report",
50+
icon: <BugReportIcon />,
51+
name: "Report Bug",
52+
href: "https://github.com/KelvinTegelaar/CIPP/issues/new?template=bug.yml",
53+
onClick: () => window.open("https://github.com/KelvinTegelaar/CIPP/issues/new?template=bug.yml", "_blank")
54+
},
55+
{
56+
id: "feature-request",
57+
icon: <FeedbackIcon />,
58+
name: "Request Feature",
59+
href: "https://github.com/KelvinTegelaar/CIPP/issues/new?template=feature.yml",
60+
onClick: () => window.open("https://github.com/KelvinTegelaar/CIPP/issues/new?template=feature.yml", "_blank")
61+
},
62+
{
63+
id: "discord",
64+
icon: (
65+
<SvgIcon component={discordIcon} viewBox="0 0 127.14 96.36" sx={{ fontSize: '1.5rem' }}>
66+
</SvgIcon>
67+
),
68+
name: "Join the Discord!",
69+
href: "https://discord.gg/cyberdrain",
70+
onClick: () => window.open("https://discord.gg/cyberdrain", "_blank")
71+
},
72+
];
73+
3974
return (
4075
<CacheProvider value={emotionCache}>
4176
<Head>
@@ -69,6 +104,11 @@ const App = (props) => {
69104
<PrivateRoute>{getLayout(<Component {...pageProps} />)}</PrivateRoute>
70105
</ErrorBoundary>
71106
<Toaster position="top-center" />
107+
<CippSpeedDial
108+
actions={speedDialActions}
109+
icon={<HelpIcon />}
110+
position={{ bottom: 16, right: 16 }}
111+
/>
72112
</RTL>
73113
</ThemeProvider>
74114
{settings?.showDevtools && (

0 commit comments

Comments
 (0)