diff --git a/public/discord-mark-blue.svg b/public/discord-mark-blue.svg new file mode 100644 index 000000000000..4cadbc7f7ed3 --- /dev/null +++ b/public/discord-mark-blue.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/CippComponents/CippFormCondition.jsx b/src/components/CippComponents/CippFormCondition.jsx index 1763091fbe27..7d3dfd029b68 100644 --- a/src/components/CippComponents/CippFormCondition.jsx +++ b/src/components/CippComponents/CippFormCondition.jsx @@ -51,7 +51,7 @@ export const CippFormCondition = (props) => { if (watcher.includes(compareValue)) { return children; } - } else if (typeof watcher === "object" && compareValue in watcher) { + } else if (typeof watcher === "object" && watcher !== null && compareValue in watcher) { // Check if object contains the key return children; } diff --git a/src/components/CippComponents/CippSpeedDial.jsx b/src/components/CippComponents/CippSpeedDial.jsx new file mode 100644 index 000000000000..4313ace7a7ef --- /dev/null +++ b/src/components/CippComponents/CippSpeedDial.jsx @@ -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 = , +}) => { + 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 ( + <> + } + open={speedDialOpen} + onClose={handleSpeedDialClose} + onOpen={() => setSpeedDialOpen(true)} + onMouseEnter={handleMouseEnter} + onMouseLeave={handleMouseLeave} + > + {actions.map((action) => ( + { + 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", + }, + }, + }} + /> + ))} + + + {actions + .filter((action) => action.form) + .map((action) => ( + handleDialogClose(action.id)} + maxWidth="md" + fullWidth + > + {action.form.title} + + + + + + + + + ))} + + setShowSnackbar(false)} + anchorOrigin={{ vertical: "bottom", horizontal: "center" }} + > + setShowSnackbar(false)} severity="success" sx={{ width: "100%" }}> + {snackbarMessage} + + + + ); +}; + +export default CippSpeedDial; diff --git a/src/components/CippComponents/CippUserActions.jsx b/src/components/CippComponents/CippUserActions.jsx index f88d909281e8..cf30780eb5e8 100644 --- a/src/components/CippComponents/CippUserActions.jsx +++ b/src/components/CippComponents/CippUserActions.jsx @@ -292,7 +292,7 @@ export const CippUserActions = () => { }, confirmText: "Are you sure you want to clear the Immutable ID for this user?", multiPost: false, - condition: (row) => row.onPremisesSyncEnabled, + condition: (row) => !row.onPremisesSyncEnabled && row?.onPremisesImmutableId, }, { label: "Revoke all user sessions", diff --git a/src/pages/_app.js b/src/pages/_app.js index 702eb328927c..e520d50c5822 100644 --- a/src/pages/_app.js +++ b/src/pages/_app.js @@ -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); @@ -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: , + 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: , + 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: ( + + + ), + name: "Join the Discord!", + href: "https://discord.gg/cyberdrain", + onClick: () => window.open("https://discord.gg/cyberdrain", "_blank") + }, + ]; + return ( @@ -69,6 +104,11 @@ const App = (props) => { {getLayout()} + } + position={{ bottom: 16, right: 16 }} + /> {settings?.showDevtools && ( diff --git a/src/pages/email/transport/list-connectors/add.jsx b/src/pages/email/transport/list-connectors/add.jsx index 5410d79c653d..8d173c49908b 100644 --- a/src/pages/email/transport/list-connectors/add.jsx +++ b/src/pages/email/transport/list-connectors/add.jsx @@ -27,9 +27,9 @@ const AddPolicy = () => { return ( @@ -55,7 +55,7 @@ const AddPolicy = () => { formControl={formControl} multiple={false} api={{ - queryKey: `TemplateListTransport`, + queryKey: `TemplateListConnectors`, labelField: "name", valueField: (option) => option, url: "/api/ListExconnectorTemplates", diff --git a/src/pages/email/transport/list-connectors/index.js b/src/pages/email/transport/list-connectors/index.js index 477b191fdff5..6f2f9fe11539 100644 --- a/src/pages/email/transport/list-connectors/index.js +++ b/src/pages/email/transport/list-connectors/index.js @@ -22,8 +22,9 @@ const Page = () => { type: "POST", url: "/api/EditExConnector", icon: , + condition: (row) => !row.Enabled, data: { - State: "Enable", + State: "!Enable", GUID: "Guid", Type: "cippconnectortype", }, @@ -35,8 +36,9 @@ const Page = () => { type: "POST", url: "/api/EditExConnector", icon: , + condition: (row) => row.Enabled, data: { - State: "Disable", + State: "!Disable", GUID: "Guid", Type: "cippconnectortype", }, @@ -82,11 +84,6 @@ const Page = () => { actions={actions} offCanvas={offCanvas} simpleColumns={simpleColumns} - titleButton={{ - label: "Deploy Connector", - href: "/email/connectors/deploy-connector", - startIcon: , // Added icon - }} cardButton={ <>