Skip to content

Commit

Permalink
Merge pull request KelvinTegelaar#3500 from Jr7468/dev
Browse files Browse the repository at this point in the history
Add speed dial actions for bug reporting and feature requests
  • Loading branch information
JohnDuprey authored Feb 5, 2025
2 parents 7c115b1 + d6eaf09 commit be34d7b
Show file tree
Hide file tree
Showing 3 changed files with 263 additions and 0 deletions.
1 change: 1 addition & 0 deletions public/discord-mark-blue.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
222 changes: 222 additions & 0 deletions src/components/CippComponents/CippSpeedDial.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
import React, { useState, useEffect } from "react";
import {
SpeedDial,
SpeedDialAction,
SpeedDialIcon,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Button,
Snackbar,
Alert,
CircularProgress,
} from "@mui/material";
import { Close as CloseIcon } from "@mui/icons-material";
import { useForm } from "react-hook-form";
import { CippFormComponent } from "../../components/CippComponents/CippFormComponent";

const CippSpeedDial = ({
actions = [],
position = { bottom: 16, right: 16 },
icon,
openIcon = <CloseIcon />,
}) => {
const [openDialogs, setOpenDialogs] = useState({});
const [loading, setLoading] = useState(false);
const [showSnackbar, setShowSnackbar] = useState(false);
const [speedDialOpen, setSpeedDialOpen] = useState(false);
const [isHovering, setIsHovering] = useState(false);
const [snackbarMessage, setSnackbarMessage] = useState("");

const formControls = actions.reduce((acc, action) => {
if (action.form) {
acc[action.id] = useForm({
mode: "onChange",
defaultValues: action.form.defaultValues || {},
});
}
return acc;
}, {});

const handleSpeedDialClose = () => {
if (!isHovering) {
setTimeout(() => {
setSpeedDialOpen(false);
}, 200);
}
};

const handleMouseEnter = () => {
setIsHovering(true);
setSpeedDialOpen(true);
};

const handleMouseLeave = () => {
setIsHovering(false);
handleSpeedDialClose();
};

const handleDialogOpen = (actionId) => {
setOpenDialogs((prev) => ({ ...prev, [actionId]: true }));
};

const handleDialogClose = (actionId) => {
setOpenDialogs((prev) => ({ ...prev, [actionId]: false }));
};

const handleSubmit = async (actionId, data) => {
if (!actions.find((a) => a.id === actionId)?.onSubmit) return;

setLoading(true);
try {
const action = actions.find((a) => a.id === actionId);
const result = await action.onSubmit(data);

if (result.success) {
formControls[actionId]?.reset();
handleDialogClose(actionId);
}
setSnackbarMessage(result.message);
setShowSnackbar(true);
} catch (error) {
console.error(`Error submitting ${actionId}:`, error);
setSnackbarMessage("An error occurred while submitting");
setShowSnackbar(true);
} finally {
setLoading(false);
}
};

useEffect(() => {
const handleClickOutside = (event) => {
if (speedDialOpen) {
const speedDial = document.querySelector('[aria-label="Navigation SpeedDial"]');
if (speedDial && !speedDial.contains(event.target)) {
setSpeedDialOpen(false);
}
}
};

document.addEventListener("click", handleClickOutside);
return () => {
document.removeEventListener("click", handleClickOutside);
};
}, [speedDialOpen]);

return (
<>
<SpeedDial
ariaLabel="Navigation SpeedDial"
sx={{
position: "fixed",
...position,
"& .MuiFab-primary": {
width: 46,
height: 46,
"&:hover": {
backgroundColor: "primary.dark",
},
},
}}
icon={<SpeedDialIcon icon={icon} openIcon={openIcon} />}
open={speedDialOpen}
onClose={handleSpeedDialClose}
onOpen={() => setSpeedDialOpen(true)}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
{actions.map((action) => (
<SpeedDialAction
key={action.id}
icon={action.icon}
tooltipTitle={action.name}
onClick={() => {
if (action.form) {
handleDialogOpen(action.id);
} else if (action.onClick) {
action.onClick();
}
setSpeedDialOpen(false);
}}
tooltipOpen
sx={{
"&.MuiSpeedDialAction-fab": {
backgroundColor: "background.paper",
"&:hover": {
backgroundColor: "action.hover",
},
},
"& .MuiSpeedDialAction-staticTooltipLabel": {
cursor: "pointer",
whiteSpace: "nowrap",
marginRight: "10px",
padding: "6px 10px",
"&:hover": {
backgroundColor: "action.hover",
},
},
}}
/>
))}
</SpeedDial>

{actions
.filter((action) => action.form)
.map((action) => (
<Dialog
key={action.id}
open={openDialogs[action.id] || false}
onClose={() => handleDialogClose(action.id)}
maxWidth="md"
fullWidth
>
<DialogTitle>{action.form.title}</DialogTitle>
<DialogContent>
<CippFormComponent
type="richText"
name={action.form.fieldName}
required
formControl={formControls[action.id]}
style={{ minHeight: "150px" }}
editorProps={{
attributes: {
style: "min-height: 150px; font-size: 1.1rem; padding: 1rem;",
},
}}
/>
</DialogContent>
<DialogActions>
<Button onClick={() => handleDialogClose(action.id)} disabled={loading}>
Cancel
</Button>
<Button
onClick={formControls[action.id]?.handleSubmit((data) =>
handleSubmit(action.id, data)
)}
variant="contained"
color="primary"
disabled={loading}
startIcon={loading ? <CircularProgress size={20} /> : null}
>
{loading ? "Submitting..." : action.form.submitText || "Submit"}
</Button>
</DialogActions>
</Dialog>
))}

<Snackbar
open={showSnackbar}
autoHideDuration={6000}
onClose={() => setShowSnackbar(false)}
anchorOrigin={{ vertical: "bottom", horizontal: "center" }}
>
<Alert onClose={() => setShowSnackbar(false)} severity="success" sx={{ width: "100%" }}>
{snackbarMessage}
</Alert>
</Snackbar>
</>
);
};

export default CippSpeedDial;
40 changes: 40 additions & 0 deletions src/pages/_app.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns";
import TimeAgo from "javascript-time-ago";
import en from "javascript-time-ago/locale/en.json";
import CippSpeedDial from "../components/CippComponents/CippSpeedDial";
import {
Help as HelpIcon,
BugReport as BugReportIcon,
Feedback as FeedbackIcon,
} from "@mui/icons-material";
import { SvgIcon } from "@mui/material";
import discordIcon from "../../public/discord-mark-blue.svg";
import React from "react";
TimeAgo.addDefaultLocale(en);

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

const speedDialActions = [
{
id: "bug-report",
icon: <BugReportIcon />,
name: "Report Bug",
href: "https://github.com/KelvinTegelaar/CIPP/issues/new?template=bug.yml",
onClick: () => window.open("https://github.com/KelvinTegelaar/CIPP/issues/new?template=bug.yml", "_blank")
},
{
id: "feature-request",
icon: <FeedbackIcon />,
name: "Request Feature",
href: "https://github.com/KelvinTegelaar/CIPP/issues/new?template=feature.yml",
onClick: () => window.open("https://github.com/KelvinTegelaar/CIPP/issues/new?template=feature.yml", "_blank")
},
{
id: "discord",
icon: (
<SvgIcon component={discordIcon} viewBox="0 0 127.14 96.36" sx={{ fontSize: '1.5rem' }}>
</SvgIcon>
),
name: "Join the Discord!",
href: "https://discord.gg/cyberdrain",
onClick: () => window.open("https://discord.gg/cyberdrain", "_blank")
},
];

return (
<CacheProvider value={emotionCache}>
<Head>
Expand Down Expand Up @@ -69,6 +104,11 @@ const App = (props) => {
<PrivateRoute>{getLayout(<Component {...pageProps} />)}</PrivateRoute>
</ErrorBoundary>
<Toaster position="top-center" />
<CippSpeedDial
actions={speedDialActions}
icon={<HelpIcon />}
position={{ bottom: 16, right: 16 }}
/>
</RTL>
</ThemeProvider>
{settings?.showDevtools && (
Expand Down

0 comments on commit be34d7b

Please sign in to comment.