From 17a8def250dd997452fe791a37a709f8dd8a68b7 Mon Sep 17 00:00:00 2001 From: Jr7468 Date: Wed, 28 May 2025 20:10:11 +0100 Subject: [PATCH 01/42] Add Mailbox Recipient Limits standard to standards.json This update introduces a new standard for configuring the maximum number of recipients in the To, Cc, and Bcc fields for all mailboxes in the tenant. The new standard includes a default recipient limit of 500 and provides relevant documentation and PowerShell equivalent commands. --- src/data/standards.json | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/data/standards.json b/src/data/standards.json index 4dba29061348..6e0d1efb3391 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -4109,5 +4109,26 @@ } } ] + }, + { + "name": "standards.MailboxRecipientLimits", + "cat": "Exchange Standards", + "tag": [], + "helpText": "Sets the maximum number of recipients that can be specified in the To, Cc, and Bcc fields of a message for all mailboxes in the tenant.", + "docsDescription": "This standard configures the recipient limits for all mailboxes in the tenant. The recipient limit determines the maximum number of recipients that can be specified in the To, Cc, and Bcc fields of a message. This helps prevent spam and manage email flow.", + "addedComponent": [ + { + "type": "number", + "name": "standards.MailboxRecipientLimits.RecipientLimit", + "label": "Recipient Limit", + "defaultValue": 500 + } + ], + "label": "Set Mailbox Recipient Limits", + "impact": "Low Impact", + "impactColour": "info", + "addedDate": "2025-05-28", + "powershellEquivalent": "Set-Mailbox -RecipientLimits", + "recommendedBy": ["CIPP"] } ] From afa06ac8d5081e8eee113903a51d8000ebe233c5 Mon Sep 17 00:00:00 2001 From: Jr7468 Date: Wed, 28 May 2025 22:51:24 +0100 Subject: [PATCH 02/42] Add Deploy Mail Contact standard to standards.json This update introduces a new standard for creating mail contacts in Exchange Online. The standard includes fields for external email address, display name, first name, and last name, along with relevant documentation and a PowerShell equivalent command. --- src/data/standards.json | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/data/standards.json b/src/data/standards.json index 6e0d1efb3391..b8d1248225ca 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -38,6 +38,45 @@ "powershellEquivalent": "Set-MsolCompanyContactInformation", "recommendedBy": [] }, + { + "name": "standards.DeployMailContact", + "cat": "Exchange Standards", + "tag": [], + "helpText": "Creates a new mail contact in Exchange Online across all selected tenants. The contact will be visible in the Global Address List.", + "docsDescription": "This standard creates a new mail contact in Exchange Online. Mail contacts are useful for adding external email addresses to your organization's address book. They can be used for distribution lists, shared mailboxes, and other collaboration scenarios.", + "addedComponent": [ + { + "type": "textField", + "name": "standards.DeployMailContact.ExternalEmailAddress", + "label": "External Email Address", + "required": true + }, + { + "type": "textField", + "name": "standards.DeployMailContact.DisplayName", + "label": "Display Name", + "required": true + }, + { + "type": "textField", + "name": "standards.DeployMailContact.FirstName", + "label": "First Name", + "required": false + }, + { + "type": "textField", + "name": "standards.DeployMailContact.LastName", + "label": "Last Name", + "required": false + } + ], + "label": "Deploy Mail Contact", + "impact": "Low Impact", + "impactColour": "info", + "addedDate": "2024-03-19", + "powershellEquivalent": "New-MailContact", + "recommendedBy": ["CIPP"] + }, { "name": "standards.AuditLog", "cat": "Global Standards", From e046e9c927c332bbf8813e9c6e60fa1c98e5fcfb Mon Sep 17 00:00:00 2001 From: Jr7468 Date: Thu, 29 May 2025 01:31:29 +0100 Subject: [PATCH 03/42] Added a section to remove proxy addresses and set primary addresses --- .../administration/users/user/exchange.jsx | 107 +++++++++++++++++- 1 file changed, 104 insertions(+), 3 deletions(-) diff --git a/src/pages/identity/administration/users/user/exchange.jsx b/src/pages/identity/administration/users/user/exchange.jsx index 2ce3578fa876..82ecccce694d 100644 --- a/src/pages/identity/administration/users/user/exchange.jsx +++ b/src/pages/identity/administration/users/user/exchange.jsx @@ -4,7 +4,7 @@ import { useRouter } from "next/router"; import { ApiGetCall } from "/src/api/ApiCall"; import CippFormSkeleton from "/src/components/CippFormPages/CippFormSkeleton"; import CalendarIcon from "@heroicons/react/24/outline/CalendarIcon"; -import { Check, Error, Mail, Fingerprint, Launch } from "@mui/icons-material"; +import { Check, Error, Mail, Fingerprint, Launch, Delete, Star } from "@mui/icons-material"; import { HeaderedTabbedLayout } from "../../../../../layouts/HeaderedTabbedLayout"; import tabOptions from "./tabOptions"; import { CippTimeAgo } from "../../../../../components/CippComponents/CippTimeAgo"; @@ -18,16 +18,20 @@ import CippExchangeSettingsForm from "../../../../../components/CippFormPages/Ci import { useForm } from "react-hook-form"; import { Alert, Button, Collapse, CircularProgress, Typography } from "@mui/material"; import { CippApiResults } from "../../../../../components/CippComponents/CippApiResults"; -import { Block, PlayArrow, DeleteForever } from "@mui/icons-material"; +import { Block, PlayArrow } from "@mui/icons-material"; import { CippPropertyListCard } from "../../../../../components/CippCards/CippPropertyListCard"; import { getCippTranslation } from "../../../../../utils/get-cipp-translation"; import { getCippFormatting } from "../../../../../utils/get-cipp-formatting"; import CippExchangeActions from "../../../../../components/CippComponents/CippExchangeActions"; +import { CippApiDialog } from "../../../../../components/CippComponents/CippApiDialog"; +import { useDialog } from "../../../../../hooks/use-dialog"; const Page = () => { const userSettingsDefaults = useSettings(); const [waiting, setWaiting] = useState(false); const [showDetails, setShowDetails] = useState(false); + const [actionData, setActionData] = useState({ ready: false }); + const createDialog = useDialog(); const router = useRouter(); const { userId } = router.query; @@ -221,7 +225,7 @@ const Page = () => { { label: "Remove Mailbox Rule", type: "POST", - icon: , + icon: , url: "/api/ExecRemoveMailboxRule", data: { ruleId: "Identity", @@ -287,6 +291,90 @@ const Page = () => { }, ]; + const proxyAddressActions = [ + { + label: "Make Primary", + type: "POST", + icon: , + url: "/api/SetUserAliases", + data: { + id: userId, + tenantFilter: userSettingsDefaults.currentTenant, + MakePrimary: "Address", + }, + confirmText: "Are you sure you want to make this the primary proxy address?", + multiPost: false, + relatedQueryKeys: `ListUsers-${userId}`, + }, + { + label: "Remove Proxy Address", + type: "POST", + icon: , + url: "/api/SetUserAliases", + data: { + id: userId, + tenantFilter: userSettingsDefaults.currentTenant, + RemovedAliases: "Address", + }, + confirmText: "Are you sure you want to remove this proxy address?", + multiPost: false, + relatedQueryKeys: `ListUsers-${userId}`, + }, + ]; + + const proxyAddressesCard = [ + { + id: 1, + cardLabelBox: { + cardLabelBoxHeader: graphUserRequest.isFetching ? ( + + ) : graphUserRequest.data?.[0]?.proxyAddresses?.length !== 0 ? ( + + ) : ( + + ), + }, + text: "Current Proxy Addresses", + subtext: graphUserRequest.data?.[0]?.proxyAddresses?.length > 1 + ? "Proxy addresses are configured for this user" + : "No proxy addresses configured for this user", + statusColor: "green.main", + table: { + title: "Proxy Addresses", + hideTitle: true, + data: graphUserRequest.data?.[0]?.proxyAddresses?.map(address => ({ + Address: address, + Type: address.startsWith('SMTP:') ? 'Primary' : 'Alias', + })) || [], + refreshFunction: () => graphUserRequest.refetch(), + isFetching: graphUserRequest.isFetching, + simpleColumns: ["Address", "Type"], + actions: proxyAddressActions, + offCanvas: { + children: (data) => { + return ( + + ); + }, + }, + }, + }, + ]; + return ( { + { )} + {actionData.ready && ( + + )} ); }; From 777570c1aaa3afd403dea93a32e9244265dea534 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Thu, 29 May 2025 15:01:21 +0200 Subject: [PATCH 04/42] add "TERRL" to custom words in cspell configuration --- cspell.json | 1 + 1 file changed, 1 insertion(+) diff --git a/cspell.json b/cspell.json index 1ff07bf80063..8d5d275d003e 100644 --- a/cspell.json +++ b/cspell.json @@ -26,6 +26,7 @@ "Rewst", "Sherweb", "Syncro", + "TERRL", "Yubikey" ], "ignoreWords": [ From 7980de164ba10a8a45e00e3be6945cefef8ba592 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Thu, 29 May 2025 15:02:27 +0200 Subject: [PATCH 05/42] Feat: add TERRL alert for monitoring Tenant External Recipient Rate Limit --- src/data/alerts.json | 10 ++++++++++ .../administration/alert-configuration/alert.jsx | 4 ++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/data/alerts.json b/src/data/alerts.json index e71be4e1547f..57d05c380a2c 100644 --- a/src/data/alerts.json +++ b/src/data/alerts.json @@ -154,5 +154,15 @@ "label": "Alert on Huntress Rogue Apps detected", "recommendedRunInterval": "4h", "description": "Huntress has provided a repository of known rogue apps that are commonly used in BEC, data exfiltration and other Microsoft 365 attacks. This alert will notify you if any of these apps are detected in the selected tenant(s). For more information, see https://huntresslabs.github.io/rogueapps/." + }, + { + "name": "TERRL", + "label": "Alert when Tenant External Recipient Rate Limit exceeds X %", + "requiresInput": true, + "inputType": "number", + "inputLabel": "Alert % (default: 80)", + "inputName": "TERRLThreshold", + "recommendedRunInterval": "1h", + "description": "Monitors tenant outbound email volume against Microsoft's TERRL limits. Data is updated every hour. The alert triggers when email volume exceeds the specified percentage of the tenant's limit." } ] diff --git a/src/pages/tenant/administration/alert-configuration/alert.jsx b/src/pages/tenant/administration/alert-configuration/alert.jsx index 050a30ef4612..97222c113e3d 100644 --- a/src/pages/tenant/administration/alert-configuration/alert.jsx +++ b/src/pages/tenant/administration/alert-configuration/alert.jsx @@ -63,7 +63,7 @@ const AlertWizard = () => { { label: "Email", value: "Email" }, { label: "PSA", value: "PSA" }, ]; - const actionstoTake = [ + const actionsToTake = [ //{ value: 'cippcommand', label: 'Execute a CIPP Command' }, { value: "becremediate", label: "Execute a BEC Remediate" }, { value: "disableuser", label: "Disable the user in the log entry" }, @@ -523,7 +523,7 @@ const AlertWizard = () => { formControl={formControl} multiple={true} creatable={false} - options={actionstoTake} + options={actionsToTake} /> From ec9a56e93c008c74bdb61fd74d4f073acc05c941 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Thu, 29 May 2025 15:48:17 +0200 Subject: [PATCH 06/42] anti foot shooting measures --- src/data/alerts.json | 2 +- src/pages/tenant/administration/alert-configuration/alert.jsx | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/data/alerts.json b/src/data/alerts.json index 57d05c380a2c..ef7c74ba8045 100644 --- a/src/data/alerts.json +++ b/src/data/alerts.json @@ -163,6 +163,6 @@ "inputLabel": "Alert % (default: 80)", "inputName": "TERRLThreshold", "recommendedRunInterval": "1h", - "description": "Monitors tenant outbound email volume against Microsoft's TERRL limits. Data is updated every hour. The alert triggers when email volume exceeds the specified percentage of the tenant's limit." + "description": "Monitors tenant outbound email volume against Microsoft's TERRL limits. Tenant data is updated every hour." } ] diff --git a/src/pages/tenant/administration/alert-configuration/alert.jsx b/src/pages/tenant/administration/alert-configuration/alert.jsx index 97222c113e3d..248feba734eb 100644 --- a/src/pages/tenant/administration/alert-configuration/alert.jsx +++ b/src/pages/tenant/administration/alert-configuration/alert.jsx @@ -556,6 +556,9 @@ const AlertWizard = () => { multiple={false} formControl={formControl} label="Included Tenants for alert" + validators={{ + required: { value: true, message: "This field is required" }, + }} /> Date: Fri, 30 May 2025 22:24:50 +0100 Subject: [PATCH 07/42] Add CippDocsLookup component to display documentation links for error severity results in CippApiResults --- .../CippComponents/CippApiResults.jsx | 4 ++ .../CippComponents/CippDocsLookup.jsx | 71 +++++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 src/components/CippComponents/CippDocsLookup.jsx diff --git a/src/components/CippComponents/CippApiResults.jsx b/src/components/CippComponents/CippApiResults.jsx index edf5826eccba..934c1a82fe1d 100644 --- a/src/components/CippComponents/CippApiResults.jsx +++ b/src/components/CippComponents/CippApiResults.jsx @@ -13,6 +13,7 @@ import { import { useEffect, useState, useMemo, useCallback } from "react"; import { getCippError } from "../../utils/get-cipp-error"; import { CippCopyToClipBoard } from "./CippCopyToClipboard"; +import { CippDocsLookup } from "./CippDocsLookup"; import React from "react"; import { CippTableDialog } from "./CippTableDialog"; import { EyeIcon } from "@heroicons/react/24/outline"; @@ -276,6 +277,9 @@ export const CippApiResults = (props) => { action={ <> + {resultObj.severity === "error" && ( + + )} { + const { text, type = "button", visible = true, ...other } = props; + const [showPassword, setShowPassword] = useState(false); + + const handleTogglePassword = () => { + setShowPassword((prev) => !prev); + }; + + const handleDocsLookup = () => { + const searchUrl = `https://docs.cipp.app/?q=Help+with:+${encodeURIComponent(text)}&ask=true`; + window.open(searchUrl, '_blank'); + }; + + if (!visible) return null; + + if (type === "button") { + return ( + + + + + + + + ); + } + + if (type === "chip") { + return ( + + + + ); + } + + if (type === "password") { + return ( + <> + + + {showPassword ? : } + + + + + + + ); + } + + return null; +}; \ No newline at end of file From 10a1189b66f954d0fb5ec7bbe8b9e69133f9234d Mon Sep 17 00:00:00 2001 From: Jr7468 Date: Fri, 30 May 2025 22:24:50 +0100 Subject: [PATCH 08/42] Add CippDocsLookup component to display documentation links for error severity results in CippApiResults --- .../CippComponents/CippApiResults.jsx | 33 ++++++++- .../CippComponents/CippDocsLookup.jsx | 71 +++++++++++++++++++ 2 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 src/components/CippComponents/CippDocsLookup.jsx diff --git a/src/components/CippComponents/CippApiResults.jsx b/src/components/CippComponents/CippApiResults.jsx index edf5826eccba..5131a3331ad7 100644 --- a/src/components/CippComponents/CippApiResults.jsx +++ b/src/components/CippComponents/CippApiResults.jsx @@ -1,4 +1,4 @@ -import { Close, Download } from "@mui/icons-material"; +import { Close, Download, Help } from "@mui/icons-material"; import { Alert, CircularProgress, @@ -9,10 +9,13 @@ import { Box, SvgIcon, Tooltip, + Button, + keyframes, } from "@mui/material"; import { useEffect, useState, useMemo, useCallback } from "react"; import { getCippError } from "../../utils/get-cipp-error"; import { CippCopyToClipBoard } from "./CippCopyToClipboard"; +import { CippDocsLookup } from "./CippDocsLookup"; import React from "react"; import { CippTableDialog } from "./CippTableDialog"; import { EyeIcon } from "@heroicons/react/24/outline"; @@ -275,6 +278,34 @@ export const CippApiResults = (props) => { severity={resultObj.severity || "success"} action={ <> + {resultObj.severity === "error" && ( + + )} { + const { text, type = "button", visible = true, ...other } = props; + const [showPassword, setShowPassword] = useState(false); + + const handleTogglePassword = () => { + setShowPassword((prev) => !prev); + }; + + const handleDocsLookup = () => { + const searchUrl = `https://docs.cipp.app/?q=Help+with:+${encodeURIComponent(text)}&ask=true`; + window.open(searchUrl, '_blank'); + }; + + if (!visible) return null; + + if (type === "button") { + return ( + + + + + + + + ); + } + + if (type === "chip") { + return ( + + + + ); + } + + if (type === "password") { + return ( + <> + + + {showPassword ? : } + + + + + + + ); + } + + return null; +}; \ No newline at end of file From 6e874fdc441e46ffbb5c72b4b25f5aa4ba2a7168 Mon Sep 17 00:00:00 2001 From: Jr7468 Date: Fri, 30 May 2025 23:32:04 +0100 Subject: [PATCH 09/42] Tweaks --- src/pages/_app.js | 69 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 68 insertions(+), 1 deletion(-) diff --git a/src/pages/_app.js b/src/pages/_app.js index 2f0a405111d9..59dd154b372a 100644 --- a/src/pages/_app.js +++ b/src/pages/_app.js @@ -26,10 +26,11 @@ import { Feedback as FeedbackIcon, AutoStories, Gavel, + Celebration, } from "@mui/icons-material"; import { SvgIcon } from "@mui/material"; import discordIcon from "../../public/discord-mark-blue.svg"; -import React, { useEffect } from "react"; +import React, { useEffect, useState, useRef } from "react"; import { usePathname } from "next/navigation"; import { useRouter } from "next/router"; import { persistQueryClient } from "@tanstack/react-query-persist-client"; @@ -50,9 +51,16 @@ const App = (props) => { const preferredTheme = useMediaPredicate("(prefers-color-scheme: dark)") ? "dark" : "light"; const pathname = usePathname(); const route = useRouter(); + const [_0x8h9i, _0x2j3k] = useState(false); // toRemove const excludeQueryKeys = ["authmeswa"]; + const _0x4f2d = [1772236800, 1772391599]; // toRemove + const _0x2e1f = () => { // toRemove + const _0x1a2b = Date.now() / 1000; // toRemove + return _0x1a2b >= _0x4f2d[0] && _0x1a2b <= _0x4f2d[1]; // toRemove + }; + // 👇 Persist TanStack Query cache to localStorage useEffect(() => { if (typeof window !== "undefined") { @@ -88,7 +96,66 @@ const App = (props) => { } }, []); + useEffect(() => { // toRemove + if (_0x8h9i) { // toRemove + const _0x3c4d = Array.from(document.querySelectorAll('*')).filter(_0x5e6f => { // toRemove + const _0x7g8h = document.querySelector('[aria-label="Navigation SpeedDial"]'); // toRemove + return !_0x7g8h?.contains(_0x5e6f); // toRemove + }); + + _0x3c4d.forEach((_0x9i0j, _0x1k2l) => { // toRemove + const _0x3m4n = Math.random() * 10 - 5; // toRemove + const _0x5o6p = Math.random() * 10 - 5; // toRemove + const _0x7q8r = Math.random() * 10 - 5; // toRemove + const _0x9s0t = Math.random() * 0.5; // toRemove + const _0x1u2v = 0.3 + Math.random() * 0.4; // toRemove + + const _0x3w4x = `_${_0x1k2l}`; // toRemove + const _0x5y6z = document.styleSheets[0]; // toRemove + _0x5y6z.insertRule(` // toRemove + @keyframes ${_0x3w4x} { // toRemove + 0% { transform: translate(0, 0) rotate(0deg); } // toRemove + 25% { transform: translate(${_0x3m4n}px, ${_0x5o6p}px) rotate(${_0x7q8r}deg); } // toRemove + 50% { transform: translate(0, 0) rotate(0deg); } // toRemove + 75% { transform: translate(${-_0x3m4n}px, ${_0x5o6p}px) rotate(${-_0x7q8r}deg); } // toRemove + 100% { transform: translate(0, 0) rotate(0deg); } // toRemove + } + `, _0x5y6z.cssRules.length); // toRemove + + _0x9i0j.style.animation = `${_0x3w4x} ${_0x1u2v}s infinite ${_0x9s0t}s`; // toRemove + }); + + const _0x1a2b = setTimeout(() => { // toRemove + _0x2j3k(false); // toRemove + _0x3c4d.forEach(_0x5e6f => { // toRemove + _0x5e6f.style.animation = ''; // toRemove + }); + const _0x7g8h = document.styleSheets[0]; // toRemove + while (_0x7g8h.cssRules.length > 0) { // toRemove + _0x7g8h.deleteRule(0); // toRemove + } + }, 5000); // toRemove + + return () => { // toRemove + clearTimeout(_0x1a2b); // toRemove + _0x3c4d.forEach(_0x5e6f => { // toRemove + _0x5e6f.style.animation = ''; // toRemove + }); + const _0x7g8h = document.styleSheets[0]; // toRemove + while (_0x7g8h.cssRules.length > 0) { // toRemove + _0x7g8h.deleteRule(0); // toRemove + } + }; + } + }, [_0x8h9i]); // toRemove + const speedDialActions = [ + ...(_0x2e1f() ? [{ // toRemove + id: "_", // toRemove + icon: , // toRemove + name: String.fromCharCode(68, 111, 32, 116, 104, 101, 32, 72, 97, 114, 108, 101, 109, 32, 83, 104, 97, 107, 101, 33), // toRemove + onClick: () => _0x2j3k(true), // toRemove + }] : []), // toRemove { id: "license", icon: , From d1d62c92b8639a5f3f5122e19afaa949e1111528 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Sat, 31 May 2025 19:11:27 -0400 Subject: [PATCH 10/42] fix auth check for dev --- src/components/PrivateRoute.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/PrivateRoute.js b/src/components/PrivateRoute.js index d08c03a7fbf6..47d8b7b563ed 100644 --- a/src/components/PrivateRoute.js +++ b/src/components/PrivateRoute.js @@ -27,7 +27,7 @@ export const PrivateRoute = ({ children, routeType }) => { if ( apiRoles?.error?.response?.status === 404 || // API endpoint not found apiRoles?.error?.response?.status === 502 || // Service unavailable - (apiRoles?.isSuccess && !apiRoles?.data?.clientPrincipal) // No client principal data, indicating API might be offline + (apiRoles?.isSuccess && !apiRoles?.data) // No client principal data, indicating API might be offline ) { return ; } From 3d98f97ac8ab555cdee4b9f4255375e71795d8d1 Mon Sep 17 00:00:00 2001 From: Jr7468 Date: Sun, 1 Jun 2025 02:05:29 +0100 Subject: [PATCH 11/42] Enhance CippExchangeSettingsForm and user exchange page with mailbox permissions management - Added cardLabelBoxActions to CippBannerListCard for displaying action buttons. - Introduced mailbox permissions management in CippExchangeSettingsForm, including form controls for adding Full Access, Send-as, and Send On Behalf permissions. - Implemented a dialog for adding mailbox permissions with validation and submission handling. - Refactored user exchange page to integrate new permissions functionality and manage state for permissions submission. --- .../CippCards/CippBannerListCard.jsx | 2 + .../CippExchangeSettingsForm.jsx | 182 +--------- .../administration/users/user/exchange.jsx | 329 ++++++++++++++++-- 3 files changed, 300 insertions(+), 213 deletions(-) diff --git a/src/components/CippCards/CippBannerListCard.jsx b/src/components/CippCards/CippBannerListCard.jsx index 32e8aee05e8d..1e8976e30b90 100644 --- a/src/components/CippCards/CippBannerListCard.jsx +++ b/src/components/CippCards/CippBannerListCard.jsx @@ -127,6 +127,7 @@ export const CippBannerListCard = (props) => { {item.statusText} )} + {item?.cardLabelBoxActions && item.cardLabelBoxActions} {isCollapsible && ( handleExpand(item.id)}> { const userSettingsDefaults = useSettings(); @@ -50,9 +50,7 @@ const CippExchangeSettingsForm = (props) => { }); const handleSubmit = (type) => { - if (type === "permissions") { - setRelatedQueryKeys([`Mailbox-${userId}`]); - } else if (type === "calendar") { + if (type === "calendar") { setRelatedQueryKeys([`CalendarPermissions-${userId}`]); } else if (type === "forwarding") { setRelatedQueryKeys([`Mailbox-${userId}`]); @@ -83,7 +81,6 @@ const CippExchangeSettingsForm = (props) => { } }); const url = { - permissions: "/api/ExecEditMailboxPermissions", calendar: "/api/ExecEditCalendarPermissions", forwarding: "/api/ExecEmailForward", ooo: "/api/ExecSetOoO", @@ -101,181 +98,6 @@ const CippExchangeSettingsForm = (props) => { // Data for each section const sections = [ - { - id: "mailboxPermissions", - cardLabelBox: "-", - text: "Mailbox Permissions", - subtext: "Manage mailbox permissions for users", - formContent: ( - - {/* Full Access Section */} - - Full Access - - Manage who has full access to this mailbox - - - - currentSettings?.Permissions?.some( - (perm) => - perm.AccessRights === "FullAccess" && perm.User === user.userPrincipalName - ) - ).map((user) => ({ - value: user.userPrincipalName, - label: `${user.displayName} (${user.userPrincipalName})`, - })) || [] - } - formControl={formControl} - /> - ({ - value: user.userPrincipalName, - label: `${user.displayName} (${user.userPrincipalName})`, - })) || [] - } - formControl={formControl} - /> - ({ - value: user.userPrincipalName, - label: `${user.displayName} (${user.userPrincipalName})`, - })) || [] - } - formControl={formControl} - /> - - - - {/* Send As Section */} - - Send As - - Manage who can send emails as this user - - - - currentSettings?.Permissions?.some( - (perm) => perm.AccessRights === "SendAs" && perm.User === user.userPrincipalName - ) - ).map((user) => ({ - value: user.userPrincipalName, - label: `${user.displayName} (${user.userPrincipalName})`, - })) || [] - } - formControl={formControl} - /> - ({ - value: user.userPrincipalName, - label: `${user.displayName} (${user.userPrincipalName})`, - })) || [] - } - formControl={formControl} - /> - - - - {/* Send On Behalf Section */} - - Send On Behalf - - Manage who can send emails on behalf of this user - - - - currentSettings?.Permissions?.some( - (perm) => - perm.AccessRights === "SendOnBehalf" && perm.User === user.userPrincipalName - ) - ).map((user) => ({ - value: user.userPrincipalName, - label: `${user.displayName} (${user.userPrincipalName})`, - })) || [] - } - formControl={formControl} - /> - ({ - value: user.userPrincipalName, - label: `${user.displayName} (${user.userPrincipalName})`, - })) || [] - } - formControl={formControl} - /> - - - - - - - - - - - ), - }, { id: "calendarPermissions", cardLabelBox: "-", diff --git a/src/pages/identity/administration/users/user/exchange.jsx b/src/pages/identity/administration/users/user/exchange.jsx index 637bb142a816..63f0dba85f80 100644 --- a/src/pages/identity/administration/users/user/exchange.jsx +++ b/src/pages/identity/administration/users/user/exchange.jsx @@ -16,7 +16,7 @@ import { CippExchangeInfoCard } from "../../../../../components/CippCards/CippEx import { useEffect, useState } from "react"; import CippExchangeSettingsForm from "../../../../../components/CippFormPages/CippExchangeSettingsForm"; import { useForm } from "react-hook-form"; -import { Alert, Button, Collapse, CircularProgress, Typography, TextField, Dialog, DialogTitle, DialogContent, DialogActions, IconButton } from "@mui/material"; +import { Alert, Button, Collapse, CircularProgress, Typography, TextField, Dialog, DialogTitle, DialogContent, DialogActions, IconButton, FormControlLabel, Switch } from "@mui/material"; import { CippApiResults } from "../../../../../components/CippComponents/CippApiResults"; import { Block, PlayArrow } from "@mui/icons-material"; import { CippPropertyListCard } from "../../../../../components/CippCards/CippPropertyListCard"; @@ -25,6 +25,7 @@ import { getCippFormatting } from "../../../../../utils/get-cipp-formatting"; import CippExchangeActions from "../../../../../components/CippComponents/CippExchangeActions"; import { CippApiDialog } from "../../../../../components/CippComponents/CippApiDialog"; import { useDialog } from "../../../../../hooks/use-dialog"; +import { CippFormComponent } from "../../../../../components/CippComponents/CippFormComponent"; const Page = () => { const userSettingsDefaults = useSettings(); @@ -35,6 +36,10 @@ const Page = () => { const [newAliases, setNewAliases] = useState(''); const [isSubmitting, setIsSubmitting] = useState(false); const [submitResult, setSubmitResult] = useState(null); + const [showAddPermissionsDialog, setShowAddPermissionsDialog] = useState(false); + const [isSubmittingPermissions, setIsSubmittingPermissions] = useState(false); + const [submitPermissionsResult, setSubmitPermissionsResult] = useState(null); + const [autoMap, setAutoMap] = useState(true); const createDialog = useDialog(); const router = useRouter(); const { userId } = router.query; @@ -56,6 +61,18 @@ const Page = () => { waiting: waiting, }); + const usersList = ApiGetCall({ + url: "/api/ListGraphRequest", + data: { + Endpoint: `users`, + tenantFilter: userSettingsDefaults.currentTenant, + $select: "id,displayName,userPrincipalName,mail", + noPagination: true, + $top: 999, + }, + queryKey: `UserNames-${userSettingsDefaults.currentTenant}`, + }); + const oooRequest = ApiGetCall({ url: `/api/ListOoO?UserId=${userId}&tenantFilter=${userSettingsDefaults.currentTenant}`, queryKey: `ooo-${userId}`, @@ -74,6 +91,36 @@ const Page = () => { waiting: waiting, }); + const permissionsFormControl = useForm({ + mode: "onChange", + defaultValues: { + fullAccess: "", + sendAs: "", + sendOnBehalf: "", + autoMap: true, + }, + }); + + const fullAccessValue = permissionsFormControl.watch("fullAccess"); + + useEffect(() => { + const subscription = permissionsFormControl.watch((value, { name, type }) => { + }); + return () => subscription.unsubscribe(); + }, [permissionsFormControl]); + + useEffect(() => { + if (showAddPermissionsDialog) { + permissionsFormControl.reset({ + fullAccess: "", + sendAs: "", + sendOnBehalf: "", + autoMap: true, + }); + usersList.refetch(); + } + }, [showAddPermissionsDialog]); + useEffect(() => { if (oooRequest.isSuccess) { formControl.setValue("ooo.ExternalMessage", oooRequest.data?.ExternalMessage); @@ -142,6 +189,74 @@ const Page = () => { const data = userRequest.data?.[0]; + const mailboxPermissionActions = [ + { + label: "Remove Permission", + type: "POST", + icon: , + url: "/api/ExecEditMailboxPermissions", + data: { + tenantFilter: userSettingsDefaults.currentTenant, + userID: graphUserRequest.data?.[0]?.userPrincipalName, + RemoveFullAccess: { + value: "User" + }, + RemoveSendAs: { + value: "User" + } + }, + confirmText: "Are you sure you want to remove this permission?", + multiPost: false, + relatedQueryKeys: `Mailbox-${userId}`, + }, + ]; + + const handleAddPermissions = () => { + const values = formControl.getValues(); + const data = { + tenantFilter: userSettingsDefaults.currentTenant, + userid: graphUserRequest.data?.[0]?.userPrincipalName, + ...values.permissions + }; + + //remove all nulls and undefined values + Object.keys(data).forEach((key) => { + if (data[key] === "" || data[key] === null) { + delete data[key]; + } + }); + + setIsSubmittingPermissions(true); + setSubmitPermissionsResult(null); + fetch('/api/ExecEditMailboxPermissions', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), + }) + .then(response => response.json()) + .then(data => { + setSubmitPermissionsResult({ success: true, message: 'Permissions added successfully' }); + userRequest.refetch(); + setTimeout(() => { + setShowAddPermissionsDialog(false); + formControl.reset(); + setSubmitPermissionsResult(null); + }, 1500); + }) + .catch(error => { + setSubmitPermissionsResult({ success: false, message: 'Failed to add permissions' }); + }) + .finally(() => { + setIsSubmittingPermissions(false); + }); + }; + + const handleOpenPermissionsDialog = () => { + setShowAddPermissionsDialog(true); + }; + const permissions = [ { id: 1, @@ -154,18 +269,55 @@ const Page = () => { ), }, - text: "Current mailbox permissions", - subtext: - userRequest.data?.[0]?.Permissions?.length !== 0 - ? "Other users have access to this mailbox" - : "No other users have access to this mailbox", + text: "Mailbox permissions", + subtext: userRequest.data?.[0]?.Permissions?.length !== 0 + ? "Other users have access to this mailbox" + : "No other users have access to this mailbox", statusColor: "green.main", - //map each of the permissions to a label/value pair, where the label is the user's name and the value is the permission level - propertyItems: - userRequest.data?.[0]?.Permissions?.map((permission) => ({ - label: permission.User, - value: permission.AccessRights, + cardLabelBoxActions: ( + + ), + table: { + title: "Mailbox Permissions", + hideTitle: true, + data: userRequest.data?.[0]?.Permissions?.map(permission => ({ + User: permission.User, + AccessRights: permission.AccessRights, })) || [], + refreshFunction: () => userRequest.refetch(), + isFetching: userRequest.isFetching, + simpleColumns: ["User", "AccessRights"], + actions: mailboxPermissionActions, + offCanvas: { + children: (data) => { + return ( + + ); + }, + }, + }, }, ]; @@ -377,11 +529,22 @@ const Page = () => { ), }, - text: "Current Proxy Addresses", + text: "Proxy Addresses", subtext: graphUserRequest.data?.[0]?.proxyAddresses?.length > 1 ? "Proxy addresses are configured for this user" : "No proxy addresses configured for this user", statusColor: "green.main", + cardLabelBoxActions: ( + + ), table: { title: "Proxy Addresses", hideTitle: true, @@ -415,22 +578,23 @@ const Page = () => { }, }, }, - children: ( - - - - ), }, ]; + const aliasApiRequest = { + isSuccess: submitResult?.success, + isError: submitResult?.success === false, + error: submitResult?.success === false ? submitResult?.message : null, + data: submitResult?.success ? { message: submitResult?.message } : null, + }; + + const permissionsApiRequest = { + isSuccess: submitPermissionsResult?.success, + isError: submitPermissionsResult?.success === false, + error: submitPermissionsResult?.success === false ? submitPermissionsResult?.message : null, + data: submitPermissionsResult?.success ? { message: submitPermissionsResult?.message } : null, + }; + return ( { variant="outlined" disabled={isSubmitting} /> - {submitResult && ( - - {submitResult.message} - - )} + @@ -586,6 +743,112 @@ const Page = () => { + setShowAddPermissionsDialog(false)} + maxWidth="sm" + fullWidth + > + + + Add Mailbox Permissions + setShowAddPermissionsDialog(false)} size="small"> + + + + + + + + ({ + value: user.userPrincipalName, + label: `${user.displayName} (${user.userPrincipalName})`, + })) || [] + } + formControl={formControl} + /> + {(() => { + const fullAccessValue = formControl.watch("permissions.AutoMap"); + console.log("FullAccess value:", fullAccessValue); + console.log("FullAccess value type:", typeof fullAccessValue); + console.log("FullAccess value structure:", JSON.stringify(fullAccessValue, null, 2)); + return fullAccessValue && ( + { + formControl.setValue("permissions.AutoMap", e.target.checked); + }} + /> + } + label="Enable Automapping" + sx={{ mt: 0.5, ml: 0.5 }} + /> + ); + })()} + + + ({ + value: user.userPrincipalName, + label: `${user.displayName} (${user.userPrincipalName})`, + })) || [] + } + formControl={formControl} + /> + + + ({ + value: user.userPrincipalName, + label: `${user.displayName} (${user.userPrincipalName})`, + })) || [] + } + formControl={formControl} + /> + + + + + + + + + ); }; From ccd7a00e812187f8f2db6f01f1176bf479a855dd Mon Sep 17 00:00:00 2001 From: Jr7468 Date: Sun, 1 Jun 2025 17:33:51 +0100 Subject: [PATCH 12/42] Corrected issues raised. --- .../administration/users/user/exchange.jsx | 162 ++++++++++-------- 1 file changed, 87 insertions(+), 75 deletions(-) diff --git a/src/pages/identity/administration/users/user/exchange.jsx b/src/pages/identity/administration/users/user/exchange.jsx index 63f0dba85f80..9c027e5eb97e 100644 --- a/src/pages/identity/administration/users/user/exchange.jsx +++ b/src/pages/identity/administration/users/user/exchange.jsx @@ -1,7 +1,7 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { useSettings } from "/src/hooks/use-settings"; import { useRouter } from "next/router"; -import { ApiGetCall } from "/src/api/ApiCall"; +import { ApiGetCall, ApiPostCall } from "/src/api/ApiCall"; import CippFormSkeleton from "/src/components/CippFormPages/CippFormSkeleton"; import CalendarIcon from "@heroicons/react/24/outline/CalendarIcon"; import { Check, Error, Mail, Fingerprint, Launch, Delete, Star, Close } from "@mui/icons-material"; @@ -194,16 +194,15 @@ const Page = () => { label: "Remove Permission", type: "POST", icon: , - url: "/api/ExecEditMailboxPermissions", + url: "/api/ExecModifyMBPerms", data: { - tenantFilter: userSettingsDefaults.currentTenant, userID: graphUserRequest.data?.[0]?.userPrincipalName, - RemoveFullAccess: { - value: "User" - }, - RemoveSendAs: { - value: "User" - } + tenantFilter: userSettingsDefaults.currentTenant, + permissions: [{ + UserID: "User", + PermissionLevel: "AccessRights", + Modification: "Remove" + }] }, confirmText: "Are you sure you want to remove this permission?", multiPost: false, @@ -211,46 +210,67 @@ const Page = () => { }, ]; + const addPermissionsMutation = ApiPostCall({ + relatedQueryKeys: `Mailbox-${userId}` + }); + const handleAddPermissions = () => { const values = formControl.getValues(); - const data = { - tenantFilter: userSettingsDefaults.currentTenant, - userid: graphUserRequest.data?.[0]?.userPrincipalName, - ...values.permissions - }; - - //remove all nulls and undefined values - Object.keys(data).forEach((key) => { - if (data[key] === "" || data[key] === null) { - delete data[key]; - } - }); + const permissions = []; + + // Build permissions array based on form values + if (values.permissions?.AddFullAccess) { + permissions.push({ + UserID: values.permissions.AddFullAccess, + PermissionLevel: "FullAccess", + Modification: "Add", + AutoMap: autoMap, + }); + } + if (values.permissions?.AddSendAs) { + permissions.push({ + UserID: values.permissions.AddSendAs, + PermissionLevel: "SendAs", + Modification: "Add" + }); + } + if (values.permissions?.AddSendOnBehalf) { + permissions.push({ + UserID: values.permissions.AddSendOnBehalf, + PermissionLevel: "SendOnBehalf", + Modification: "Add" + }); + } + + if (permissions.length === 0) return; setIsSubmittingPermissions(true); setSubmitPermissionsResult(null); - fetch('/api/ExecEditMailboxPermissions', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(data), - }) - .then(response => response.json()) - .then(data => { - setSubmitPermissionsResult({ success: true, message: 'Permissions added successfully' }); + + addPermissionsMutation.mutate({ + url: '/api/ExecModifyMBPerms', + data: { + userID: graphUserRequest.data?.[0]?.userPrincipalName, + tenantFilter: userSettingsDefaults.currentTenant, + permissions: permissions + } + }, { + onSuccess: (response) => { + setSubmitPermissionsResult({ success: true, message: response.data?.Results?.join('\n') || 'Permissions added successfully' }); userRequest.refetch(); setTimeout(() => { setShowAddPermissionsDialog(false); formControl.reset(); setSubmitPermissionsResult(null); }, 1500); - }) - .catch(error => { - setSubmitPermissionsResult({ success: false, message: 'Failed to add permissions' }); - }) - .finally(() => { + }, + onError: (error) => { + setSubmitPermissionsResult({ success: false, message: error.message || 'Failed to add permissions' }); + }, + onSettled: () => { setIsSubmittingPermissions(false); - }); + } + }); }; const handleOpenPermissionsDialog = () => { @@ -291,6 +311,7 @@ const Page = () => { data: userRequest.data?.[0]?.Permissions?.map(permission => ({ User: permission.User, AccessRights: permission.AccessRights, + _raw: permission })) || [], refreshFunction: () => userRequest.refetch(), isFetching: userRequest.isFetching, @@ -486,34 +507,31 @@ const Page = () => { if (aliases.length > 0) { setIsSubmitting(true); setSubmitResult(null); - fetch('/api/SetUserAliases', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ + + ApiPostCall({ + url: '/api/SetUserAliases', + data: { id: userId, tenantFilter: userSettingsDefaults.currentTenant, AddedAliases: aliases.join(','), userPrincipalName: graphUserRequest.data?.[0]?.userPrincipalName, - }), - }) - .then(response => response.json()) - .then(data => { - setSubmitResult({ success: true, message: 'Aliases added successfully' }); + }, + onSuccess: (response) => { + setSubmitResult({ success: true, message: response.message || 'Aliases added successfully' }); graphUserRequest.refetch(); setTimeout(() => { setShowAddAliasDialog(false); setNewAliases(''); setSubmitResult(null); }, 1500); - }) - .catch(error => { - setSubmitResult({ success: false, message: 'Failed to add aliases' }); - }) - .finally(() => { + }, + onError: (error) => { + setSubmitResult({ success: false, message: error.message || 'Failed to add aliases' }); + }, + onFinally: () => { setIsSubmitting(false); - }); + } + }); } }; @@ -773,27 +791,21 @@ const Page = () => { } formControl={formControl} /> - {(() => { - const fullAccessValue = formControl.watch("permissions.AutoMap"); - console.log("FullAccess value:", fullAccessValue); - console.log("FullAccess value type:", typeof fullAccessValue); - console.log("FullAccess value structure:", JSON.stringify(fullAccessValue, null, 2)); - return fullAccessValue && ( - { - formControl.setValue("permissions.AutoMap", e.target.checked); - }} - /> - } - label="Enable Automapping" - sx={{ mt: 0.5, ml: 0.5 }} - /> - ); - })()} + {formControl.watch("permissions.AddFullAccess") && ( + { + setAutoMap(e.target.checked); + }} + /> + } + label="Enable Automapping" + sx={{ mt: 0.5, ml: 0.5 }} + /> + )} Date: Sun, 1 Jun 2025 12:53:28 -0400 Subject: [PATCH 13/42] make entire banner clickable --- .../CippCards/CippBannerListCard.jsx | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/components/CippCards/CippBannerListCard.jsx b/src/components/CippCards/CippBannerListCard.jsx index 1e8976e30b90..8a123e5064b2 100644 --- a/src/components/CippCards/CippBannerListCard.jsx +++ b/src/components/CippCards/CippBannerListCard.jsx @@ -39,8 +39,8 @@ export const CippBannerListCard = (props) => { - + @@ -74,7 +74,16 @@ export const CippBannerListCard = (props) => { direction="row" flexWrap="wrap" justifyContent="space-between" - sx={{ p: 3 }} + sx={{ + p: 3, + ...(isCollapsible && { + cursor: "pointer", + "&:hover": { + bgcolor: "action.hover", + }, + }), + }} + onClick={isCollapsible ? () => handleExpand(item.id) : undefined} > {/* Left Side: cardLabelBox */} @@ -127,9 +136,16 @@ export const CippBannerListCard = (props) => { {item.statusText} )} - {item?.cardLabelBoxActions && item.cardLabelBoxActions} + {item?.cardLabelBoxActions && ( + e.stopPropagation()}>{item.cardLabelBoxActions} + )} {isCollapsible && ( - handleExpand(item.id)}> + { + e.stopPropagation(); + handleExpand(item.id); + }} + > Date: Sun, 1 Jun 2025 13:04:58 -0400 Subject: [PATCH 14/42] make the rest of the banners clickable minor tweaks --- .../CippExchangeSettingsForm.jsx | 34 +-- .../administration/users/user/exchange.jsx | 219 ++++++++++-------- 2 files changed, 144 insertions(+), 109 deletions(-) diff --git a/src/components/CippFormPages/CippExchangeSettingsForm.jsx b/src/components/CippFormPages/CippExchangeSettingsForm.jsx index b90494fd2066..4fdc368a301d 100644 --- a/src/components/CippFormPages/CippExchangeSettingsForm.jsx +++ b/src/components/CippFormPages/CippExchangeSettingsForm.jsx @@ -172,19 +172,19 @@ const CippExchangeSettingsForm = (props) => { {(() => { const permissionLevel = useWatch({ control: formControl.control, - name: "calendar.Permissions" + name: "calendar.Permissions", }); const isEditor = permissionLevel?.value === "Editor"; - + // Use useEffect to handle the switch value reset useEffect(() => { if (!isEditor) { formControl.setValue("calendar.CanViewPrivateItems", false); } }, [isEditor, formControl]); - + return ( - { display: "flex", justifyContent: "space-between", p: 2, + cursor: "pointer", + "&:hover": { + bgcolor: "action.hover", + }, }} + onClick={() => handleExpand(section.id)} > {/* Left Side: cardLabelBox, text, subtext */} @@ -453,18 +458,15 @@ const CippExchangeSettingsForm = (props) => { - {/* Expand Icon */} - handleExpand(section.id)}> - - - - + + + diff --git a/src/pages/identity/administration/users/user/exchange.jsx b/src/pages/identity/administration/users/user/exchange.jsx index 9c027e5eb97e..52eed0034384 100644 --- a/src/pages/identity/administration/users/user/exchange.jsx +++ b/src/pages/identity/administration/users/user/exchange.jsx @@ -4,7 +4,7 @@ import { useRouter } from "next/router"; import { ApiGetCall, ApiPostCall } from "/src/api/ApiCall"; import CippFormSkeleton from "/src/components/CippFormPages/CippFormSkeleton"; import CalendarIcon from "@heroicons/react/24/outline/CalendarIcon"; -import { Check, Error, Mail, Fingerprint, Launch, Delete, Star, Close } from "@mui/icons-material"; +import { Check, Error, Mail, Fingerprint, Launch, Delete, Star, Close, AlternateEmail, PersonAdd } from "@mui/icons-material"; import { HeaderedTabbedLayout } from "../../../../../layouts/HeaderedTabbedLayout"; import tabOptions from "./tabOptions"; import { CippTimeAgo } from "../../../../../components/CippComponents/CippTimeAgo"; @@ -16,7 +16,21 @@ import { CippExchangeInfoCard } from "../../../../../components/CippCards/CippEx import { useEffect, useState } from "react"; import CippExchangeSettingsForm from "../../../../../components/CippFormPages/CippExchangeSettingsForm"; import { useForm } from "react-hook-form"; -import { Alert, Button, Collapse, CircularProgress, Typography, TextField, Dialog, DialogTitle, DialogContent, DialogActions, IconButton, FormControlLabel, Switch } from "@mui/material"; +import { + Alert, + Button, + Collapse, + CircularProgress, + Typography, + TextField, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + IconButton, + FormControlLabel, + Switch, +} from "@mui/material"; import { CippApiResults } from "../../../../../components/CippComponents/CippApiResults"; import { Block, PlayArrow } from "@mui/icons-material"; import { CippPropertyListCard } from "../../../../../components/CippCards/CippPropertyListCard"; @@ -33,7 +47,7 @@ const Page = () => { const [showDetails, setShowDetails] = useState(false); const [actionData, setActionData] = useState({ ready: false }); const [showAddAliasDialog, setShowAddAliasDialog] = useState(false); - const [newAliases, setNewAliases] = useState(''); + const [newAliases, setNewAliases] = useState(""); const [isSubmitting, setIsSubmitting] = useState(false); const [submitResult, setSubmitResult] = useState(null); const [showAddPermissionsDialog, setShowAddPermissionsDialog] = useState(false); @@ -102,10 +116,9 @@ const Page = () => { }); const fullAccessValue = permissionsFormControl.watch("fullAccess"); - + useEffect(() => { - const subscription = permissionsFormControl.watch((value, { name, type }) => { - }); + const subscription = permissionsFormControl.watch((value, { name, type }) => {}); return () => subscription.unsubscribe(); }, [permissionsFormControl]); @@ -198,11 +211,13 @@ const Page = () => { data: { userID: graphUserRequest.data?.[0]?.userPrincipalName, tenantFilter: userSettingsDefaults.currentTenant, - permissions: [{ - UserID: "User", - PermissionLevel: "AccessRights", - Modification: "Remove" - }] + permissions: [ + { + UserID: "User", + PermissionLevel: "AccessRights", + Modification: "Remove", + }, + ], }, confirmText: "Are you sure you want to remove this permission?", multiPost: false, @@ -211,7 +226,7 @@ const Page = () => { ]; const addPermissionsMutation = ApiPostCall({ - relatedQueryKeys: `Mailbox-${userId}` + relatedQueryKeys: `Mailbox-${userId}`, }); const handleAddPermissions = () => { @@ -222,7 +237,7 @@ const Page = () => { if (values.permissions?.AddFullAccess) { permissions.push({ UserID: values.permissions.AddFullAccess, - PermissionLevel: "FullAccess", + PermissionLevel: "FullAccess", Modification: "Add", AutoMap: autoMap, }); @@ -231,14 +246,14 @@ const Page = () => { permissions.push({ UserID: values.permissions.AddSendAs, PermissionLevel: "SendAs", - Modification: "Add" + Modification: "Add", }); } if (values.permissions?.AddSendOnBehalf) { permissions.push({ UserID: values.permissions.AddSendOnBehalf, PermissionLevel: "SendOnBehalf", - Modification: "Add" + Modification: "Add", }); } @@ -246,31 +261,40 @@ const Page = () => { setIsSubmittingPermissions(true); setSubmitPermissionsResult(null); - - addPermissionsMutation.mutate({ - url: '/api/ExecModifyMBPerms', - data: { - userID: graphUserRequest.data?.[0]?.userPrincipalName, - tenantFilter: userSettingsDefaults.currentTenant, - permissions: permissions - } - }, { - onSuccess: (response) => { - setSubmitPermissionsResult({ success: true, message: response.data?.Results?.join('\n') || 'Permissions added successfully' }); - userRequest.refetch(); - setTimeout(() => { - setShowAddPermissionsDialog(false); - formControl.reset(); - setSubmitPermissionsResult(null); - }, 1500); - }, - onError: (error) => { - setSubmitPermissionsResult({ success: false, message: error.message || 'Failed to add permissions' }); + + addPermissionsMutation.mutate( + { + url: "/api/ExecModifyMBPerms", + data: { + userID: graphUserRequest.data?.[0]?.userPrincipalName, + tenantFilter: userSettingsDefaults.currentTenant, + permissions: permissions, + }, }, - onSettled: () => { - setIsSubmittingPermissions(false); + { + onSuccess: (response) => { + setSubmitPermissionsResult({ + success: true, + message: response.data?.Results?.join("\n") || "Permissions added successfully", + }); + userRequest.refetch(); + setTimeout(() => { + setShowAddPermissionsDialog(false); + formControl.reset(); + setSubmitPermissionsResult(null); + }, 1500); + }, + onError: (error) => { + setSubmitPermissionsResult({ + success: false, + message: error.message || "Failed to add permissions", + }); + }, + onSettled: () => { + setIsSubmittingPermissions(false); + }, } - }); + ); }; const handleOpenPermissionsDialog = () => { @@ -289,14 +313,15 @@ const Page = () => { ), }, - text: "Mailbox permissions", - subtext: userRequest.data?.[0]?.Permissions?.length !== 0 - ? "Other users have access to this mailbox" - : "No other users have access to this mailbox", + text: "Mailbox Permissions", + subtext: + userRequest.data?.[0]?.Permissions?.length !== 0 + ? "Other users have access to this mailbox" + : "No other users have access to this mailbox", statusColor: "green.main", cardLabelBoxActions: ( - - setShowAddPermissionsDialog(false)} maxWidth="sm" fullWidth > - + Add Mailbox Permissions setShowAddPermissionsDialog(false)} size="small"> @@ -778,7 +807,7 @@ const Page = () => { - { { - - From 56e32110e612b5f14dda4f02136f5974751f4922 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Sun, 1 Jun 2025 13:18:20 -0400 Subject: [PATCH 15/42] fix invalid hook call issue with setting user aliases --- .../administration/users/user/exchange.jsx | 60 ++++++++++++------- 1 file changed, 38 insertions(+), 22 deletions(-) diff --git a/src/pages/identity/administration/users/user/exchange.jsx b/src/pages/identity/administration/users/user/exchange.jsx index 52eed0034384..6e0539869c33 100644 --- a/src/pages/identity/administration/users/user/exchange.jsx +++ b/src/pages/identity/administration/users/user/exchange.jsx @@ -4,7 +4,18 @@ import { useRouter } from "next/router"; import { ApiGetCall, ApiPostCall } from "/src/api/ApiCall"; import CippFormSkeleton from "/src/components/CippFormPages/CippFormSkeleton"; import CalendarIcon from "@heroicons/react/24/outline/CalendarIcon"; -import { Check, Error, Mail, Fingerprint, Launch, Delete, Star, Close, AlternateEmail, PersonAdd } from "@mui/icons-material"; +import { + Check, + Error, + Mail, + Fingerprint, + Launch, + Delete, + Star, + Close, + AlternateEmail, + PersonAdd, +} from "@mui/icons-material"; import { HeaderedTabbedLayout } from "../../../../../layouts/HeaderedTabbedLayout"; import tabOptions from "./tabOptions"; import { CippTimeAgo } from "../../../../../components/CippComponents/CippTimeAgo"; @@ -48,7 +59,6 @@ const Page = () => { const [actionData, setActionData] = useState({ ready: false }); const [showAddAliasDialog, setShowAddAliasDialog] = useState(false); const [newAliases, setNewAliases] = useState(""); - const [isSubmitting, setIsSubmitting] = useState(false); const [submitResult, setSubmitResult] = useState(null); const [showAddPermissionsDialog, setShowAddPermissionsDialog] = useState(false); const [isSubmittingPermissions, setIsSubmittingPermissions] = useState(false); @@ -117,6 +127,11 @@ const Page = () => { const fullAccessValue = permissionsFormControl.watch("fullAccess"); + const setUserAliases = ApiPostCall({ + relatedQueryKeys: `ListUsers-${userId}`, + datafromUrl: true, + }); + useEffect(() => { const subscription = permissionsFormControl.watch((value, { name, type }) => {}); return () => subscription.unsubscribe(); @@ -290,9 +305,6 @@ const Page = () => { message: error.message || "Failed to add permissions", }); }, - onSettled: () => { - setIsSubmittingPermissions(false); - }, } ); }; @@ -531,10 +543,9 @@ const Page = () => { .map((alias) => alias.trim()) .filter((alias) => alias); if (aliases.length > 0) { - setIsSubmitting(true); setSubmitResult(null); - ApiPostCall({ + setUserAliases.mutate({ url: "/api/SetUserAliases", data: { id: userId, @@ -557,9 +568,6 @@ const Page = () => { onError: (error) => { setSubmitResult({ success: false, message: error.message || "Failed to add aliases" }); }, - onFinally: () => { - setIsSubmitting(false); - }, }); } }; @@ -752,15 +760,21 @@ const Page = () => { fullWidth > - - Add Proxy Addresses - setShowAddAliasDialog(false)} size="small"> - - - + + + Add Proxy Addresses + setShowAddAliasDialog(false)} size="small"> + + + + + Add one or more proxy addresses (aliases) for this user. Each alias should be on a new + line. This should be in the format of smtp:user@domain.com. + + - + { onChange={(e) => setNewAliases(e.target.value)} placeholder="One alias per line" variant="outlined" - disabled={isSubmitting} + disabled={setUserAliases.isPending} /> - From e6cdf7673ffd0c9909073c386ef817c3f137722b Mon Sep 17 00:00:00 2001 From: John Duprey Date: Sun, 1 Jun 2025 13:26:16 -0400 Subject: [PATCH 16/42] Update exchange.jsx --- src/pages/identity/administration/users/user/exchange.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/identity/administration/users/user/exchange.jsx b/src/pages/identity/administration/users/user/exchange.jsx index 6e0539869c33..d0483f80af65 100644 --- a/src/pages/identity/administration/users/user/exchange.jsx +++ b/src/pages/identity/administration/users/user/exchange.jsx @@ -769,7 +769,7 @@ const Page = () => { Add one or more proxy addresses (aliases) for this user. Each alias should be on a new - line. This should be in the format of smtp:user@domain.com. + line. From 8ec9bba775bce0928c33425fad52fe5fc429c4e1 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Sun, 1 Jun 2025 13:36:31 -0400 Subject: [PATCH 17/42] Update exchange.jsx --- .../identity/administration/users/user/exchange.jsx | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/src/pages/identity/administration/users/user/exchange.jsx b/src/pages/identity/administration/users/user/exchange.jsx index d0483f80af65..0590cab136db 100644 --- a/src/pages/identity/administration/users/user/exchange.jsx +++ b/src/pages/identity/administration/users/user/exchange.jsx @@ -638,13 +638,6 @@ const Page = () => { }, ]; - const aliasApiRequest = { - isSuccess: submitResult?.success, - isError: submitResult?.success === false, - error: submitResult?.success === false ? submitResult?.message : null, - data: submitResult?.success ? { message: submitResult?.message } : null, - }; - const permissionsApiRequest = { isSuccess: submitPermissionsResult?.success, isError: submitPermissionsResult?.success === false, @@ -774,7 +767,7 @@ const Page = () => { - + { variant="outlined" disabled={setUserAliases.isPending} /> - - + + @@ -350,7 +497,7 @@ const CippStandardAccordion = ({ const isExpanded = expanded === standardName; const hasAddedComponents = standard.addedComponent && standard.addedComponent.length > 0; - const isConfigured = configuredState[standardName]; + const isConfigured = _.get(configuredState, standardName); const disabledFeatures = standard.disabledFeatures || {}; let selectedActions = _.get(watchedValues, `${standardName}.action`); @@ -361,9 +508,106 @@ const CippStandardAccordion = ({ const selectedTemplateName = standard.multiple ? _.get(watchedValues, `${standardName}.${standard.addedComponent?.[0]?.name}`) : ""; - const accordionTitle = selectedTemplateName - ? `${standard.label} - ${selectedTemplateName.label}` - : standard.label; + const accordionTitle = + selectedTemplateName && _.get(selectedTemplateName, "label") + ? `${standard.label} - ${_.get(selectedTemplateName, "label")}` + : standard.label; + + // Get current values and check if they differ from saved values + const current = _.get(watchedValues, standardName); + const saved = _.get(savedValues, standardName) || {}; + const hasUnsaved = !_.isEqual(current, saved); + + // Check if all required fields are filled + const requiredFieldsFilled = current + ? standard.addedComponent?.every((component) => { + // Always skip switches regardless of their required property + if (component.type === "switch") return true; + + // Skip optional fields (not required) + const isRequired = component.required !== false; + if (!isRequired) return true; + + // Handle conditional fields + if (component.condition) { + const conditionField = component.condition.field; + const conditionValue = _.get(current, conditionField); + const compareType = component.condition.compareType || "is"; + const compareValue = component.condition.compareValue; + const propertyName = component.condition.propertyName || "value"; + + let conditionMet = false; + if (propertyName === "value") { + switch (compareType) { + case "is": + conditionMet = _.isEqual(conditionValue, compareValue); + break; + case "isNot": + conditionMet = !_.isEqual(conditionValue, compareValue); + break; + default: + conditionMet = false; + } + } else if (Array.isArray(conditionValue)) { + switch (compareType) { + case "valueEq": + conditionMet = conditionValue.some( + (item) => item?.[propertyName] === compareValue + ); + break; + default: + conditionMet = false; + } + } + + // If condition is not met, skip validation + if (!conditionMet) return true; + } + + // Get field value for validation using lodash's get to properly handle nested properties + const fieldValue = _.get(current, component.name); + console.log(`Checking field: ${component.name}, value:`, fieldValue); + console.log(current); + // Check if required field has a value based on its type and multiple property + if (component.type === "autoComplete" || component.type === "select") { + if (component.multiple) { + // For multiple selection, check if array exists and has items + return Array.isArray(fieldValue) && fieldValue.length > 0; + } else { + // For single selection, check if value exists + return !!fieldValue; + } + } + + // For other field types + return !!fieldValue; + }) ?? true + : false; + + // ALWAYS require an action for all standards + const actionRequired = true; + + // Check if there are required non-switch components for UI display purposes + const hasRequiredComponents = + standard.addedComponent && + standard.addedComponent.some( + (comp) => comp.type !== "switch" && comp.required !== false + ); + + // Action is always required and must be an array with at least one element + const actionValue = _.get(current, "action"); + const hasAction = + actionValue && (!Array.isArray(actionValue) || actionValue.length > 0); + + // Allow saving if: + // 1. Action is selected if required + // 2. All required fields are filled + // 3. There are unsaved changes + const canSave = hasAction && requiredFieldsFilled && hasUnsaved; + + console.log( + `Standard: ${standardName}, Action Required: ${actionRequired}, Has Action: ${hasAction}, Required Fields Filled: ${requiredFieldsFilled}, Can Save: ${canSave}` + ); return ( @@ -456,6 +700,7 @@ const CippStandardAccordion = ({ + {/* Always show action field as it's required */} @@ -500,6 +746,25 @@ const CippStandardAccordion = ({ )} + + + + + diff --git a/src/components/CippStandards/CippStandardsSideBar.jsx b/src/components/CippStandards/CippStandardsSideBar.jsx index f069bc396ddc..f14945745a04 100644 --- a/src/components/CippStandards/CippStandardsSideBar.jsx +++ b/src/components/CippStandards/CippStandardsSideBar.jsx @@ -75,15 +75,25 @@ const CippStandardsSideBar = ({ useEffect(() => { const stepsStatus = { - step1: !!watchForm.templateName, - step2: watchForm.tenantFilter && watchForm.tenantFilter.length > 0, + step1: !!_.get(watchForm, "templateName"), + step2: _.get(watchForm, "tenantFilter", []).length > 0, step3: Object.keys(selectedStandards).length > 0, step4: - watchForm.standards && + _.get(watchForm, "standards") && Object.keys(selectedStandards).length > 0 && Object.keys(selectedStandards).every((standardName) => { - const standardValues = _.get(watchForm, `${standardName}`, {}) ?? {}; - return standardValues?.action; + const standardValues = _.get(watchForm, `${standardName}`, {}); + const standard = selectedStandards[standardName]; + // Check if this standard requires an action + const hasRequiredComponents = + standard?.addedComponent && + standard.addedComponent.some( + (comp) => comp.type !== "switch" && comp.required !== false + ); + const actionRequired = standard?.disabledFeatures !== undefined || hasRequiredComponents; + // Always require an action value which should be an array with at least one element + const actionValue = _.get(standardValues, "action"); + return actionValue && (!Array.isArray(actionValue) || actionValue.length > 0); }), }; @@ -91,16 +101,20 @@ const CippStandardsSideBar = ({ setCurrentStep(completedSteps); }, [selectedStandards, watchForm]); + // Create a local reference to the stepsStatus from the latest effect run const stepsStatus = { - step1: !!watchForm.templateName, - step2: watchForm.tenantFilter && watchForm.tenantFilter.length > 0, + step1: !!_.get(watchForm, "templateName"), + step2: _.get(watchForm, "tenantFilter", []).length > 0, step3: Object.keys(selectedStandards).length > 0, step4: - watchForm.standards && + _.get(watchForm, "standards") && Object.keys(selectedStandards).length > 0 && Object.keys(selectedStandards).every((standardName) => { const standardValues = _.get(watchForm, `${standardName}`, {}); - return standardValues?.action; + const standard = selectedStandards[standardName]; + // Always require an action for all standards (must be an array with at least one element) + const actionValue = _.get(standardValues, "action"); + return actionValue && (!Array.isArray(actionValue) || actionValue.length > 0); }), }; return ( @@ -134,7 +148,9 @@ const CippStandardsSideBar = ({ required={true} includeGroups={true} /> - {watchForm.tenantFilter?.some((tenant) => tenant.value === "AllTenants" || tenant.type === "Group" ) && ( + {watchForm.tenantFilter?.some( + (tenant) => tenant.value === "AllTenants" || tenant.type === "Group" + ) && ( <> Date: Tue, 3 Jun 2025 13:24:45 -0400 Subject: [PATCH 21/42] always show chips and differentiate between action/info --- .../CippStandards/CippStandardAccordion.jsx | 66 +++++++++++-------- 1 file changed, 39 insertions(+), 27 deletions(-) diff --git a/src/components/CippStandards/CippStandardAccordion.jsx b/src/components/CippStandards/CippStandardAccordion.jsx index 16c23213df0a..bbc6f0353b4e 100644 --- a/src/components/CippStandards/CippStandardAccordion.jsx +++ b/src/components/CippStandards/CippStandardAccordion.jsx @@ -24,6 +24,9 @@ import { Search, Close, FilterAlt, + NotificationImportant, + Assignment, + Construction, } from "@mui/icons-material"; import { Grid } from "@mui/system"; import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; @@ -258,19 +261,19 @@ const CippStandardAccordion = ({ // Set the entire standard's value at once to ensure proper handling of nested objects and arrays formControl.setValue(standardName, _.cloneDeep(savedValue)); - + // Find the original standard definition to get the base standard const baseStandardName = standardName.split("[")[0]; const standard = providedStandards.find((s) => s.name === baseStandardName); - + // Determine if the standard was configured with saved values if (standard) { const isConfigured = isStandardConfigured(standardName, standard, savedValue); - + // Restore the previous configuration state setConfiguredState((prev) => ({ ...prev, - [standardName]: isConfigured + [standardName]: isConfigured, })); } @@ -324,7 +327,7 @@ const CippStandardAccordion = ({ if (standardName === expanded) { return true; } - + const matchesSearch = !searchQuery || categoryMatchesSearch || @@ -635,28 +638,37 @@ const CippStandardAccordion = ({ {accordionTitle} - {selectedActions && selectedActions?.length > 0 && ( - - {selectedActions?.map((action, index) => ( - - - - ))} - - - )} + + {selectedActions && selectedActions?.length > 0 && ( + <> + {selectedActions?.map((action, index) => ( + + + {action.value === "Report" && } + {action.value === "warn" && } + {action.value === "Remediate" && } + + } + /> + + ))} + + )} + + {standard.helpText} From 2808a60b805ee48ab75d452547414bbb9864f7b5 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 3 Jun 2025 13:40:18 -0400 Subject: [PATCH 22/42] fix save button layout --- src/components/CippStandards/CippStandardAccordion.jsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/CippStandards/CippStandardAccordion.jsx b/src/components/CippStandards/CippStandardAccordion.jsx index bbc6f0353b4e..c07bc67c27d2 100644 --- a/src/components/CippStandards/CippStandardAccordion.jsx +++ b/src/components/CippStandards/CippStandardAccordion.jsx @@ -758,8 +758,10 @@ const CippStandardAccordion = ({ )} - - + + + + diff --git a/src/pages/tenant/standards/template.jsx b/src/pages/tenant/standards/template.jsx index 04c8b9b9c906..35fd5663b090 100644 --- a/src/pages/tenant/standards/template.jsx +++ b/src/pages/tenant/standards/template.jsx @@ -1,34 +1,116 @@ import { Box, Button, Container, Stack, Typography, SvgIcon, Skeleton } from "@mui/material"; import { Grid } from "@mui/system"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; -import { useForm } from "react-hook-form"; +import { useForm, useWatch } from "react-hook-form"; import { useRouter } from "next/router"; -import { Add } from "@mui/icons-material"; -import { useEffect, useState } from "react"; +import { Add, SaveRounded } from "@mui/icons-material"; +import { useEffect, useState, useCallback, useMemo, useRef, lazy, Suspense } from "react"; import standards from "/src/data/standards"; import CippStandardAccordion from "../../../components/CippStandards/CippStandardAccordion"; -import CippStandardDialog from "../../../components/CippStandards/CippStandardDialog"; +// Lazy load the dialog to improve initial page load performance +const CippStandardDialog = lazy(() => + import("../../../components/CippStandards/CippStandardDialog") +); import CippStandardsSideBar from "../../../components/CippStandards/CippStandardsSideBar"; import { ArrowLeftIcon } from "@mui/x-date-pickers"; -import CheckCircleIcon from "@mui/icons-material/CheckCircle"; import { useDialog } from "../../../hooks/use-dialog"; import { ApiGetCall } from "../../../api/ApiCall"; +import _ from "lodash"; const Page = () => { const router = useRouter(); const [editMode, setEditMode] = useState(false); const formControl = useForm({ mode: "onBlur" }); + const { formState } = formControl; const [dialogOpen, setDialogOpen] = useState(false); const [expanded, setExpanded] = useState(null); const [searchQuery, setSearchQuery] = useState(""); const [selectedStandards, setSelectedStandards] = useState({}); const [updatedAt, setUpdatedAt] = useState(false); + const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); + const [currentStep, setCurrentStep] = useState(0); + const initialStandardsRef = useRef({}); + + // Watch form values to check valid configuration + const watchForm = useWatch({ control: formControl.control }); + const existingTemplate = ApiGetCall({ url: `/api/listStandardTemplates`, data: { id: router.query.id }, queryKey: `listStandardTemplates-${router.query.id}`, waiting: editMode, }); + + // Check if the template configuration is valid and update currentStep + useEffect(() => { + const stepsStatus = { + step1: !!_.get(watchForm, "templateName"), + step2: _.get(watchForm, "tenantFilter", []).length > 0, + step3: Object.keys(selectedStandards).length > 0, + step4: + _.get(watchForm, "standards") && + Object.keys(selectedStandards).length > 0 && + Object.keys(selectedStandards).every((standardName) => { + const standardValues = _.get(watchForm, standardName, {}); + // Always require an action value which should be an array with at least one element + const actionValue = _.get(standardValues, "action"); + return actionValue && (!Array.isArray(actionValue) || actionValue.length > 0); + }), + }; + + const completedSteps = Object.values(stepsStatus).filter(Boolean).length; + setCurrentStep(completedSteps); + }, [selectedStandards, watchForm]); + + // Handle route change events + const handleRouteChange = useCallback( + (url) => { + if (hasUnsavedChanges) { + const confirmLeave = window.confirm( + "You have unsaved changes. Are you sure you want to leave this page?" + ); + if (!confirmLeave) { + router.events.emit("routeChangeError"); + throw "Route change was aborted"; + } + } + }, + [hasUnsavedChanges, router] + ); + + // Handle browser back/forward navigation or tab close + useEffect(() => { + const handleBeforeUnload = (e) => { + if (hasUnsavedChanges) { + e.preventDefault(); + e.returnValue = "You have unsaved changes. Are you sure you want to leave this page?"; + return e.returnValue; + } + }; + + // Add event listeners + window.addEventListener("beforeunload", handleBeforeUnload); + router.events.on("routeChangeStart", handleRouteChange); + + // Remove event listeners on cleanup + return () => { + window.removeEventListener("beforeunload", handleBeforeUnload); + router.events.off("routeChangeStart", handleRouteChange); + }; + }, [hasUnsavedChanges, handleRouteChange, router.events]); + + // Track form changes + useEffect(() => { + if ( + formState.isDirty || + JSON.stringify(selectedStandards) !== JSON.stringify(initialStandardsRef.current) + ) { + setHasUnsavedChanges(true); + } else { + setHasUnsavedChanges(false); + } + }, [formState.isDirty, selectedStandards]); + useEffect(() => { if (router.query.id) { setEditMode(true); @@ -71,30 +153,40 @@ const Page = () => { }); setSelectedStandards(transformedStandards); + // Store initial state for change detection + initialStandardsRef.current = { ...transformedStandards }; + setHasUnsavedChanges(false); } }, [existingTemplate.isSuccess, router]); - const categories = standards.reduce((acc, standard) => { - const { cat } = standard; - if (!acc[cat]) { - acc[cat] = []; - } - acc[cat].push(standard); - return acc; - }, {}); + // Memoize categories to avoid unnecessary recalculations + const categories = useMemo(() => { + return standards.reduce((acc, standard) => { + const { cat } = standard; + if (!acc[cat]) { + acc[cat] = []; + } + acc[cat].push(standard); + return acc; + }, {}); + }, []); + + const handleOpenDialog = useCallback(() => { + setDialogOpen(true); + }, []); - const handleOpenDialog = () => setDialogOpen(true); - const handleCloseDialog = () => { + const handleCloseDialog = useCallback(() => { setDialogOpen(false); setSearchQuery(""); - }; + }, []); const filterStandards = (standardsList) => standardsList.filter( (standard) => - standard.label.toLowerCase().includes(searchQuery) || - standard.helpText.toLowerCase().includes(searchQuery) || - (standard.tag && standard.tag.some((tag) => tag.toLowerCase().includes(searchQuery))) + standard.label.toLowerCase().includes(searchQuery.toLowerCase()) || + standard.helpText.toLowerCase().includes(searchQuery.toLowerCase()) || + (standard.tag && + standard.tag.some((tag) => tag.toLowerCase().includes(searchQuery.toLowerCase()))) ); const handleToggleStandard = (standardName) => { @@ -146,15 +238,22 @@ const Page = () => { setExpanded((prev) => (prev === standardName ? null : standardName)); }; - const actions = [ - { - label: "Save Template", - handler: () => createDialog.handleOpen(), - icon: , - }, - ]; const createDialog = useDialog(); + // Save action that will open the create dialog + const handleSave = () => { + createDialog.handleOpen(); + // Will be set to false after successful save in the dialog component + }; + + // Determine if save button should be disabled based on configuration + const isSaveDisabled = + !_.get(watchForm, "tenantFilter") || + !_.get(watchForm, "tenantFilter").length || + currentStep < 3; + + const actions = []; + const steps = [ "Set a name for the Template", "Assigned Template to Tenants", @@ -162,6 +261,19 @@ const Page = () => { "Configured all Standards", ]; + const handleSafeNavigation = (url) => { + if (hasUnsavedChanges) { + const confirmLeave = window.confirm( + "You have unsaved changes. Are you sure you want to leave this page?" + ); + if (confirmLeave) { + router.push(url); + } + } else { + router.push(url); + } + }; + return ( @@ -169,7 +281,13 @@ const Page = () => { + + + + @@ -212,6 +341,7 @@ const Page = () => { selectedStandards={selectedStandards} edit={editMode} updatedAt={updatedAt} + onSaveSuccess={() => setHasUnsavedChanges(false)} /> @@ -235,16 +365,21 @@ const Page = () => { - + {/* Only render the dialog when it's needed */} + {dialogOpen && ( + }> + + + )} ); diff --git a/yarn.lock b/yarn.lock index d809e0a29713..3c47f75cfe6d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6096,6 +6096,11 @@ react-transition-group@^4.4.5: loose-envify "^1.4.0" prop-types "^15.6.2" +react-virtuoso@^4.12.8: + version "4.12.8" + resolved "https://registry.yarnpkg.com/react-virtuoso/-/react-virtuoso-4.12.8.tgz#db1dbba617f91c1dcd760aa90e09ef991e65a356" + integrity sha512-NMMKfDBr/+xZZqCQF3tN1SZsh6FwOJkYgThlfnsPLkaEhdyQo0EuWUzu3ix6qjnI7rYwJhMwRGoJBi+aiDfGsA== + react-window@^1.8.10: version "1.8.11" resolved "https://registry.yarnpkg.com/react-window/-/react-window-1.8.11.tgz#a857b48fa85bd77042d59cc460964ff2e0648525" From d8eb517055d0a13bae47f82b0ab1ae61dc1713e8 Mon Sep 17 00:00:00 2001 From: Jr7468 Date: Wed, 4 Jun 2025 15:41:42 +0100 Subject: [PATCH 29/42] feat(exchange): Enhance Exchange settings management - Add mailbox forwarding configuration with internal/external options - Implement calendar permissions management interface - Add Out of Office (OOO) settings configuration - Add recipient limits management functionality - Improve form handling and validation for Exchange settings --- .../CippExchangeSettingsForm.jsx | 127 -------- .../administration/users/user/exchange.jsx | 285 +++++++++++++++++- 2 files changed, 276 insertions(+), 136 deletions(-) diff --git a/src/components/CippFormPages/CippExchangeSettingsForm.jsx b/src/components/CippFormPages/CippExchangeSettingsForm.jsx index 4fdc368a301d..0e6ab44d81bd 100644 --- a/src/components/CippFormPages/CippExchangeSettingsForm.jsx +++ b/src/components/CippFormPages/CippExchangeSettingsForm.jsx @@ -98,133 +98,6 @@ const CippExchangeSettingsForm = (props) => { // Data for each section const sections = [ - { - id: "calendarPermissions", - cardLabelBox: "-", - text: "Calendar Permissions", - subtext: "Adjust calendar sharing settings", - formContent: ( - - - calPermissions?.some((perm) => perm.User === user.displayName) - ).map((user) => ({ - value: user.userPrincipalName, - label: `${user.displayName} (${user.userPrincipalName})`, - })) || [] - } - formControl={formControl} - /> - ({ - value: user.userPrincipalName, - label: `${user.displayName} (${user.userPrincipalName})`, - })) || []), - ]} - multiple={false} - formControl={formControl} - /> - - - - value ? true : "Select the permission level for the calendar", - }} - isFetching={isFetching || usersList.isFetching} - options={[ - { value: "Author", label: "Author" }, - { value: "Contributor", label: "Contributor" }, - { value: "Editor", label: "Editor" }, - { value: "Owner", label: "Owner" }, - { value: "NonEditingAuthor", label: "Non Editing Author" }, - { value: "PublishingAuthor", label: "Publishing Author" }, - { value: "PublishingEditor", label: "Publishing Editor" }, - { value: "Reviewer", label: "Reviewer" }, - { value: "LimitedDetails", label: "Limited Details" }, - { value: "AvailabilityOnly", label: "Availability Only" }, - ]} - multiple={false} - formControl={formControl} - /> - - {(() => { - const permissionLevel = useWatch({ - control: formControl.control, - name: "calendar.Permissions", - }); - const isEditor = permissionLevel?.value === "Editor"; - - // Use useEffect to handle the switch value reset - useEffect(() => { - if (!isEditor) { - formControl.setValue("calendar.CanViewPrivateItems", false); - } - }, [isEditor, formControl]); - - return ( - - - - - - ); - })()} - - - - - - - - - - - - ), - }, { id: "mailboxForwarding", cardLabelBox: currentSettings?.ForwardAndDeliver ? : "-", diff --git a/src/pages/identity/administration/users/user/exchange.jsx b/src/pages/identity/administration/users/user/exchange.jsx index 0590cab136db..4d961193215e 100644 --- a/src/pages/identity/administration/users/user/exchange.jsx +++ b/src/pages/identity/administration/users/user/exchange.jsx @@ -13,6 +13,7 @@ import { Delete, Star, Close, + CalendarToday, AlternateEmail, PersonAdd, } from "@mui/icons-material"; @@ -41,6 +42,7 @@ import { IconButton, FormControlLabel, Switch, + Tooltip, } from "@mui/material"; import { CippApiResults } from "../../../../../components/CippComponents/CippApiResults"; import { Block, PlayArrow } from "@mui/icons-material"; @@ -67,6 +69,9 @@ const Page = () => { const createDialog = useDialog(); const router = useRouter(); const { userId } = router.query; + const [showAddCalendarPermissionsDialog, setShowAddCalendarPermissionsDialog] = useState(false); + const [isSubmittingCalendarPermissions, setIsSubmittingCalendarPermissions] = useState(false); + const [submitCalendarPermissionsResult, setSubmitCalendarPermissionsResult] = useState(null); const formControl = useForm({ mode: "onChange", @@ -392,17 +397,102 @@ const Page = () => { ), }, - text: "Current Calendar permissions", - subtext: calPermissions.data?.length - ? "Other users have access to this users calendar" - : "No other users have access to this users calendar", + text: "Calendar permissions", + subtext: calPermissions.data?.length !== 0 + ? "Other users have access to this calendar" + : "No other users have access to this calendar", statusColor: "green.main", - //map each of the permissions to a label/value pair, where the label is the user's name and the value is the permission level - propertyItems: - calPermissions.data?.map((permission) => ({ - label: `${permission.User} - ${permission.FolderName}`, - value: permission.AccessRights.join(", "), + cardLabelBoxActions: ( + + ), + table: { + title: "Calendar Permissions", + hideTitle: true, + data: calPermissions.data?.map(permission => ({ + User: permission.User, + AccessRights: permission.AccessRights.join(", "), + FolderName: permission.FolderName, + _raw: permission })) || [], + refreshFunction: () => calPermissions.refetch(), + isFetching: calPermissions.isFetching, + simpleColumns: ["User", "AccessRights", "FolderName"], + actions: [ + { + label: "Remove Permission", + type: "POST", + icon: , + url: "/api/ExecModifyCalPerms", + data: { + userID: graphUserRequest.data?.[0]?.userPrincipalName, + tenantFilter: userSettingsDefaults.currentTenant, + permissions: [{ + UserID: "User", + PermissionLevel: "AccessRights", + FolderName: "FolderName", + Modification: "Remove" + }] + }, + confirmText: "Are you sure you want to remove this calendar permission?", + multiPost: false, + relatedQueryKeys: `CalendarPermissions-${userId}`, + hideBulk: true + } + ], + offCanvas: { + children: (data) => { + return ( + , + url: "/api/ExecModifyCalPerms", + data: { + userID: graphUserRequest.data?.[0]?.userPrincipalName, + tenantFilter: userSettingsDefaults.currentTenant, + permissions: [{ + UserID: data.User, + PermissionLevel: data.AccessRights, + FolderName: data.FolderName, + Modification: "Remove" + }] + }, + confirmText: "Are you sure you want to remove this calendar permission?", + multiPost: false, + relatedQueryKeys: `CalendarPermissions-${userId}`, + } + ]} + /> + ); + }, + }, + }, }, ]; @@ -645,6 +735,71 @@ const Page = () => { data: submitPermissionsResult?.success ? { message: submitPermissionsResult?.message } : null, }; + const calendarPermissionsFormControl = useForm({ + mode: "onChange", + defaultValues: { + UserToGetPermissions: null, + Permissions: null, + CanViewPrivateItems: false, + FolderName: "Calendar" + }, + }); + + const handleAddCalendarPermissions = () => { + const values = calendarPermissionsFormControl.getValues(); + if (!values.UserToGetPermissions || !values.Permissions) return; + + setIsSubmittingCalendarPermissions(true); + setSubmitCalendarPermissionsResult(null); + + // Build permission object dynamically + const permission = { + UserID: values.UserToGetPermissions, + PermissionLevel: values.Permissions, + Modification: "Add" + }; + if (values.CanViewPrivateItems) { + permission.CanViewPrivateItems = true; + } + + addPermissionsMutation.mutate({ + url: '/api/ExecModifyCalPerms', + data: { + userID: graphUserRequest.data?.[0]?.userPrincipalName, + tenantFilter: userSettingsDefaults.currentTenant, + permissions: [permission] + } + }, { + onSuccess: (response) => { + setSubmitCalendarPermissionsResult({ success: true, message: response.data?.Results?.join('\n') || 'Calendar permissions added successfully' }); + calPermissions.refetch(); + setTimeout(() => { + setShowAddCalendarPermissionsDialog(false); + calendarPermissionsFormControl.reset(); + setSubmitCalendarPermissionsResult(null); + }, 1500); + }, + onError: (error) => { + // Try to extract a detailed message from the API response + const apiMessage = + error?.response?.data?.Results?.join('\n') || + error?.message || + 'Failed to add calendar permissions'; + setSubmitCalendarPermissionsResult({ success: false, message: apiMessage }); + }, + onSettled: () => { + setIsSubmittingCalendarPermissions(false); + } + }); + }; + + const calendarPermissionsApiRequest = { + isSuccess: submitCalendarPermissionsResult?.success, + isError: submitCalendarPermissionsResult?.success === false, + error: submitCalendarPermissionsResult?.success === false ? submitCalendarPermissionsResult?.message : null, + data: submitCalendarPermissionsResult?.success ? { message: submitCalendarPermissionsResult?.message } : null, + }; + return ( { + setShowAddCalendarPermissionsDialog(false)} + maxWidth="sm" + fullWidth + > + + + Add Calendar Permissions + setShowAddCalendarPermissionsDialog(false)} size="small"> + + + + + + + + ({ + value: user.userPrincipalName, + label: `${user.displayName} (${user.userPrincipalName})`, + })) || [] + } + multiple={false} + formControl={calendarPermissionsFormControl} + /> + + + + value ? true : "Select the permission level for the calendar", + }} + isFetching={userRequest.isFetching || usersList.isFetching} + options={[ + { value: "Author", label: "Author" }, + { value: "Contributor", label: "Contributor" }, + { value: "Editor", label: "Editor" }, + { value: "Owner", label: "Owner" }, + { value: "NonEditingAuthor", label: "Non Editing Author" }, + { value: "PublishingAuthor", label: "Publishing Author" }, + { value: "PublishingEditor", label: "Publishing Editor" }, + { value: "Reviewer", label: "Reviewer" }, + { value: "LimitedDetails", label: "Limited Details" }, + { value: "AvailabilityOnly", label: "Availability Only" }, + ]} + multiple={false} + formControl={calendarPermissionsFormControl} + /> + + + {(() => { + const permissionLevel = calendarPermissionsFormControl.watch("Permissions"); + const isEditor = permissionLevel?.value === "Editor"; + + useEffect(() => { + if (!isEditor) { + calendarPermissionsFormControl.setValue("CanViewPrivateItems", false); + } + }, [isEditor, calendarPermissionsFormControl]); + + return ( + + + + + + ); + })()} + + + + + + + + + ); }; From 734990dd5710ac8f52072da1ae0d50e2bffad2b3 Mon Sep 17 00:00:00 2001 From: Jr7468 Date: Wed, 4 Jun 2025 16:41:10 +0100 Subject: [PATCH 30/42] Modified the alias management dialog to allow for multiple aliases to be added at once, in a cleaner format. --- .../administration/users/user/exchange.jsx | 147 +++++++++++++----- 1 file changed, 112 insertions(+), 35 deletions(-) diff --git a/src/pages/identity/administration/users/user/exchange.jsx b/src/pages/identity/administration/users/user/exchange.jsx index 4d961193215e..9cc43ee9860b 100644 --- a/src/pages/identity/administration/users/user/exchange.jsx +++ b/src/pages/identity/administration/users/user/exchange.jsx @@ -43,9 +43,10 @@ import { FormControlLabel, Switch, Tooltip, + Chip, } from "@mui/material"; import { CippApiResults } from "../../../../../components/CippComponents/CippApiResults"; -import { Block, PlayArrow } from "@mui/icons-material"; +import { Block, PlayArrow, Add } from "@mui/icons-material"; import { CippPropertyListCard } from "../../../../../components/CippCards/CippPropertyListCard"; import { getCippTranslation } from "../../../../../utils/get-cipp-translation"; import { getCippFormatting } from "../../../../../utils/get-cipp-formatting"; @@ -60,7 +61,8 @@ const Page = () => { const [showDetails, setShowDetails] = useState(false); const [actionData, setActionData] = useState({ ready: false }); const [showAddAliasDialog, setShowAddAliasDialog] = useState(false); - const [newAliases, setNewAliases] = useState(""); + const [newAlias, setNewAlias] = useState(""); + const [aliasList, setAliasList] = useState([]); const [submitResult, setSubmitResult] = useState(null); const [showAddPermissionsDialog, setShowAddPermissionsDialog] = useState(false); const [isSubmittingPermissions, setIsSubmittingPermissions] = useState(false); @@ -627,12 +629,26 @@ const Page = () => { }, ]; + const handleAddAlias = () => { + if (newAlias.trim()) { + setAliasList([...aliasList, newAlias.trim()]); + setNewAlias(""); + } + }; + + const handleDeleteAlias = (aliasToDelete) => { + setAliasList(aliasList.filter((alias) => alias !== aliasToDelete)); + }; + + const handleKeyPress = (event) => { + if (event.key === 'Enter') { + event.preventDefault(); + handleAddAlias(); + } + }; + const handleAddAliases = () => { - const aliases = newAliases - .split("\n") - .map((alias) => alias.trim()) - .filter((alias) => alias); - if (aliases.length > 0) { + if (aliasList.length > 0) { setSubmitResult(null); setUserAliases.mutate({ @@ -640,7 +656,7 @@ const Page = () => { data: { id: userId, tenantFilter: userSettingsDefaults.currentTenant, - AddedAliases: aliases.join(","), + AddedAliases: aliasList.join(","), userPrincipalName: graphUserRequest.data?.[0]?.userPrincipalName, }, onSuccess: (response) => { @@ -651,7 +667,8 @@ const Page = () => { graphUserRequest.refetch(); setTimeout(() => { setShowAddAliasDialog(false); - setNewAliases(""); + setAliasList([]); + setNewAlias(""); setSubmitResult(null); }, 1500); }, @@ -908,44 +925,104 @@ const Page = () => { fullWidth > - - - Add Proxy Addresses - setShowAddAliasDialog(false)} size="small"> - - - - - Add one or more proxy addresses (aliases) for this user. Each alias should be on a new - line. - - + + Add Proxy Addresses + setShowAddAliasDialog(false)} size="small"> + + + - - setNewAliases(e.target.value)} - placeholder="One alias per line" - variant="outlined" - disabled={setUserAliases.isPending} - /> + + + Add proxy addresses (aliases) for this user. Enter each alias and click Add or press Enter. + + + setNewAlias(e.target.value)} + onKeyPress={handleKeyPress} + placeholder="Enter an alias" + variant="outlined" + disabled={setUserAliases.isPending} + size="small" + sx={{ + '& .MuiOutlinedInput-root': { + fontFamily: 'monospace', + '& .MuiOutlinedInput-input': { + px: 2 + } + } + }} + /> + + + + {aliasList.length === 0 ? ( + + No aliases added yet + + ) : ( + aliasList.map((alias) => ( + handleDeleteAlias(alias)} + color="primary" + variant="outlined" + /> + )) + )} + - )} - {resultObj.severity === "error" && ( - - )} + Date: Wed, 4 Jun 2025 22:08:58 -0400 Subject: [PATCH 34/42] extend CippApiDialog support custom data handling using customDataformatter logic from CippFormPage --- .../CippComponents/CippApiDialog.jsx | 99 ++++++++++--------- 1 file changed, 54 insertions(+), 45 deletions(-) diff --git a/src/components/CippComponents/CippApiDialog.jsx b/src/components/CippComponents/CippApiDialog.jsx index 987a8f937f7c..779a0cd320fb 100644 --- a/src/components/CippComponents/CippApiDialog.jsx +++ b/src/components/CippComponents/CippApiDialog.jsx @@ -8,11 +8,11 @@ import { DialogTitle, useMediaQuery, } from "@mui/material"; -import { Stack, Grid } from "@mui/system"; +import { Stack } from "@mui/system"; import { CippApiResults } from "./CippApiResults"; import { ApiGetCall, ApiPostCall } from "../../api/ApiCall"; import React, { useEffect, useState } from "react"; -import { useForm } from "react-hook-form"; +import { useForm, useWatch } from "react-hook-form"; import { useSettings } from "../../hooks/use-settings"; import CippFormComponent from "./CippFormComponent"; @@ -27,6 +27,7 @@ export const CippApiDialog = (props) => { dialogAfterEffect, allowResubmit = false, children, + customDataformatter, ...other } = props; const router = useRouter(); @@ -38,7 +39,6 @@ export const CippApiDialog = (props) => { if (mdDown) { other.fullScreen = true; } - useEffect(() => { if (createDialog.open) { setIsFormSubmitted(false); @@ -76,13 +76,17 @@ export const CippApiDialog = (props) => { }); const processActionData = (dataObject, row, replacementBehaviour) => { - if (typeof api?.dataFunction === "function") return api.dataFunction(row); + if (typeof api?.dataFunction === "function") return api.dataFunction(row, dataObject); let newData = {}; if (api?.postEntireRow) { return row; } + if (!dataObject) { + return dataObject; + } + Object.keys(dataObject).forEach((key) => { const value = dataObject[key]; @@ -108,57 +112,62 @@ export const CippApiDialog = (props) => { const tenantFilter = useSettings().currentTenant; const handleActionClick = (row, action, formData) => { setIsFormSubmitted(true); - if (action.multiPost === undefined) action.multiPost = false; + let finalData = {}; + if (typeof customDataformatter === "function") { + finalData = customDataformatter(row, action, formData); + } else { + if (action.multiPost === undefined) action.multiPost = false; - if (api.customFunction) { - action.customFunction(row, action, formData); - createDialog.handleClose(); - return; - } + if (api.customFunction) { + action.customFunction(row, action, formData); + createDialog.handleClose(); + return; + } - const commonData = { - tenantFilter, - ...formData, - ...addedFieldData, - }; - const processedActionData = processActionData(action.data, row, action.replacementBehaviour); + const commonData = { + tenantFilter, + ...formData, + ...addedFieldData, + }; + const processedActionData = processActionData(action.data, row, action.replacementBehaviour); - // MULTI ROW CASES - if (Array.isArray(row)) { - const arrayData = row.map((singleRow) => { - const itemData = { ...commonData }; - Object.keys(processedActionData).forEach((key) => { - const rowValue = singleRow[processedActionData[key]]; - itemData[key] = rowValue !== undefined ? rowValue : processedActionData[key]; + // MULTI ROW CASES + if (Array.isArray(row)) { + const arrayData = row.map((singleRow) => { + const itemData = { ...commonData }; + Object.keys(processedActionData).forEach((key) => { + const rowValue = singleRow[processedActionData[key]]; + itemData[key] = rowValue !== undefined ? rowValue : processedActionData[key]; + }); + return itemData; }); - return itemData; - }); - const payload = { - url: action.url, - bulkRequest: !action.multiPost, - data: arrayData, - }; + const payload = { + url: action.url, + bulkRequest: !action.multiPost, + data: arrayData, + }; - if (action.type === "POST") { - actionPostRequest.mutate(payload); - } else if (action.type === "GET") { - setGetRequestInfo({ - ...payload, - waiting: true, - queryKey: Date.now(), - }); + if (action.type === "POST") { + actionPostRequest.mutate(payload); + } else if (action.type === "GET") { + setGetRequestInfo({ + ...payload, + waiting: true, + queryKey: Date.now(), + }); + } + + return; } - return; + // ✅ FIXED: DIRECT MERGE INSTEAD OF CORRUPT TRANSFORMATION + finalData = { + ...commonData, + ...processedActionData, + }; } - // ✅ FIXED: DIRECT MERGE INSTEAD OF CORRUPT TRANSFORMATION - const finalData = { - ...commonData, - ...processedActionData, - }; - if (action.type === "POST") { actionPostRequest.mutate({ url: action.url, From c6cf28caca2c2db8d72e485bd1f6cabb6784dbe6 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 4 Jun 2025 22:09:45 -0400 Subject: [PATCH 35/42] user exchange settings page split dialogs out into separate components refactor dialogs to use CippApiDialog with new children functionality --- .../CippComponents/CippAliasDialog.jsx | 128 +++ .../CippCalendarPermissionsDialog.jsx | 105 +++ .../CippMailboxPermissionsDialog.jsx | 87 +++ .../administration/users/user/exchange.jsx | 738 ++++-------------- 4 files changed, 485 insertions(+), 573 deletions(-) create mode 100644 src/components/CippComponents/CippAliasDialog.jsx create mode 100644 src/components/CippComponents/CippCalendarPermissionsDialog.jsx create mode 100644 src/components/CippComponents/CippMailboxPermissionsDialog.jsx diff --git a/src/components/CippComponents/CippAliasDialog.jsx b/src/components/CippComponents/CippAliasDialog.jsx new file mode 100644 index 000000000000..1046a5bb1cac --- /dev/null +++ b/src/components/CippComponents/CippAliasDialog.jsx @@ -0,0 +1,128 @@ +import { useState, useEffect } from "react"; +import { Typography, Box, Button, TextField, Chip, Stack } from "@mui/material"; +import { Add } from "@mui/icons-material"; +import { useWatch } from "react-hook-form"; + +const CippAliasDialog = ({ formHook }) => { + const [newAlias, setNewAlias] = useState(""); + + // Initialize the form field if it doesn't exist + useEffect(() => { + // Set default empty array if AddedAliases doesn't exist in the form + if (!formHook.getValues("AddedAliases")) { + formHook.setValue("AddedAliases", []); + } + }, [formHook]); + + // Use useWatch to subscribe to form field changes + const aliasList = useWatch({ + control: formHook.control, + name: "AddedAliases", + defaultValue: [], + }); + + const isPending = formHook.formState.isSubmitting; + + const handleAddAlias = () => { + if (newAlias.trim()) { + const currentAliases = formHook.getValues("AddedAliases") || []; + const newList = [...currentAliases, newAlias.trim()]; + formHook.setValue("AddedAliases", newList, { shouldValidate: true }); + setNewAlias(""); + } + }; + + const handleDeleteAlias = (aliasToDelete) => { + const currentAliases = formHook.getValues("AddedAliases") || []; + const updatedList = currentAliases.filter((alias) => alias !== aliasToDelete); + formHook.setValue("AddedAliases", updatedList, { shouldValidate: true }); + }; + + const handleKeyPress = (event) => { + if (event.key === "Enter") { + event.preventDefault(); + handleAddAlias(); + } + }; + + return ( + <> + + + Add proxy addresses (aliases) for this user. Enter each alias and click Add or press + Enter. + + + setNewAlias(e.target.value)} + onKeyPress={handleKeyPress} + placeholder="Enter an alias" + variant="outlined" + disabled={isPending} + size="small" + sx={{ + "& .MuiOutlinedInput-root": { + fontFamily: "monospace", + "& .MuiOutlinedInput-input": { + px: 2, + }, + }, + }} + /> + + + + {aliasList.length === 0 ? ( + + No aliases added yet + + ) : ( + aliasList.map((alias) => ( + handleDeleteAlias(alias)} + color="primary" + variant="outlined" + /> + )) + )} + + + + ); +}; + +export default CippAliasDialog; diff --git a/src/components/CippComponents/CippCalendarPermissionsDialog.jsx b/src/components/CippComponents/CippCalendarPermissionsDialog.jsx new file mode 100644 index 000000000000..6f44cb6fd064 --- /dev/null +++ b/src/components/CippComponents/CippCalendarPermissionsDialog.jsx @@ -0,0 +1,105 @@ +import { useEffect } from "react"; +import { Box, Stack, Tooltip } from "@mui/material"; +import CippFormComponent from "./CippFormComponent"; +import { useWatch } from "react-hook-form"; +import { ApiGetCall } from "../../api/ApiCall"; +import { useSettings } from "../../hooks/use-settings"; + +const CippCalendarPermissionsDialog = ({ formHook }) => { + const permissionLevel = useWatch({ + control: formHook.control, + name: "Permissions", + }); + + const userSettingsDefaults = useSettings(); + + const usersList = ApiGetCall({ + url: "/api/ListGraphRequest", + data: { + Endpoint: `users`, + tenantFilter: userSettingsDefaults.currentTenant, + $select: "id,displayName,userPrincipalName,mail", + noPagination: true, + $top: 999, + }, + queryKey: `UserNames-${userSettingsDefaults.currentTenant}`, + }); + + const isEditor = permissionLevel?.value === "Editor"; + + useEffect(() => { + if (!isEditor) { + formHook.setValue("CanViewPrivateItems", false); + } + }, [isEditor, formHook]); + + return ( + + + ({ + value: user.userPrincipalName, + label: `${user.displayName} (${user.userPrincipalName})`, + })) || [] + } + required={true} + validators={{ + validate: (value) => (value ? true : "Select a user to assign permissions to"), + }} + placeholder="Select a user to assign permissions to" + /> + + + (value ? true : "Select the permission level for the calendar"), + }} + options={[ + { value: "Author", label: "Author" }, + { value: "Contributor", label: "Contributor" }, + { value: "Editor", label: "Editor" }, + { value: "Owner", label: "Owner" }, + { value: "NonEditingAuthor", label: "Non Editing Author" }, + { value: "PublishingAuthor", label: "Publishing Author" }, + { value: "PublishingEditor", label: "Publishing Editor" }, + { value: "Reviewer", label: "Reviewer" }, + { value: "LimitedDetails", label: "Limited Details" }, + { value: "AvailabilityOnly", label: "Availability Only" }, + ]} + multiple={false} + formControl={formHook} + /> + + + + + + + + + + ); +}; + +export default CippCalendarPermissionsDialog; diff --git a/src/components/CippComponents/CippMailboxPermissionsDialog.jsx b/src/components/CippComponents/CippMailboxPermissionsDialog.jsx new file mode 100644 index 000000000000..52b2f3cb7372 --- /dev/null +++ b/src/components/CippComponents/CippMailboxPermissionsDialog.jsx @@ -0,0 +1,87 @@ +import { Box, Stack } from "@mui/material"; +import CippFormComponent from "./CippFormComponent"; +import { useWatch } from "react-hook-form"; +import { ApiGetCall } from "../../api/ApiCall"; +import { useSettings } from "../../hooks/use-settings"; + +const CippMailboxPermissionsDialog = ({ formHook }) => { + const fullAccess = useWatch({ + control: formHook.control, + name: "permissions.AddFullAccess", + }); + + const userSettingsDefaults = useSettings(); + + const usersList = ApiGetCall({ + url: "/api/ListGraphRequest", + data: { + Endpoint: `users`, + tenantFilter: userSettingsDefaults.currentTenant, + $select: "id,displayName,userPrincipalName,mail", + noPagination: true, + $top: 999, + }, + queryKey: `UserNames-${userSettingsDefaults.currentTenant}`, + }); + + return ( + + + ({ + value: user.userPrincipalName, + label: `${user.displayName} (${user.userPrincipalName})`, + })) || [] + } + /> + {fullAccess && ( + + )} + + + ({ + value: user.userPrincipalName, + label: `${user.displayName} (${user.userPrincipalName})`, + })) || [] + } + /> + + + ({ + value: user.userPrincipalName, + label: `${user.displayName} (${user.userPrincipalName})`, + })) || [] + } + /> + + + ); +}; + +export default CippMailboxPermissionsDialog; diff --git a/src/pages/identity/administration/users/user/exchange.jsx b/src/pages/identity/administration/users/user/exchange.jsx index 9cc43ee9860b..4ce7fa786c26 100644 --- a/src/pages/identity/administration/users/user/exchange.jsx +++ b/src/pages/identity/administration/users/user/exchange.jsx @@ -54,26 +54,21 @@ import CippExchangeActions from "../../../../../components/CippComponents/CippEx import { CippApiDialog } from "../../../../../components/CippComponents/CippApiDialog"; import { useDialog } from "../../../../../hooks/use-dialog"; import { CippFormComponent } from "../../../../../components/CippComponents/CippFormComponent"; +import CippAliasDialog from "../../../../../components/CippComponents/CippAliasDialog"; +import CippMailboxPermissionsDialog from "../../../../../components/CippComponents/CippMailboxPermissionsDialog"; +import CippCalendarPermissionsDialog from "../../../../../components/CippComponents/CippCalendarPermissionsDialog"; const Page = () => { const userSettingsDefaults = useSettings(); const [waiting, setWaiting] = useState(false); const [showDetails, setShowDetails] = useState(false); const [actionData, setActionData] = useState({ ready: false }); - const [showAddAliasDialog, setShowAddAliasDialog] = useState(false); - const [newAlias, setNewAlias] = useState(""); - const [aliasList, setAliasList] = useState([]); - const [submitResult, setSubmitResult] = useState(null); - const [showAddPermissionsDialog, setShowAddPermissionsDialog] = useState(false); - const [isSubmittingPermissions, setIsSubmittingPermissions] = useState(false); - const [submitPermissionsResult, setSubmitPermissionsResult] = useState(null); - const [autoMap, setAutoMap] = useState(true); const createDialog = useDialog(); + const aliasDialog = useDialog(); + const permissionsDialog = useDialog(); + const calendarPermissionsDialog = useDialog(); const router = useRouter(); const { userId } = router.query; - const [showAddCalendarPermissionsDialog, setShowAddCalendarPermissionsDialog] = useState(false); - const [isSubmittingCalendarPermissions, setIsSubmittingCalendarPermissions] = useState(false); - const [submitCalendarPermissionsResult, setSubmitCalendarPermissionsResult] = useState(null); const formControl = useForm({ mode: "onChange", @@ -122,39 +117,40 @@ const Page = () => { waiting: waiting, }); - const permissionsFormControl = useForm({ - mode: "onChange", - defaultValues: { - fullAccess: "", - sendAs: "", - sendOnBehalf: "", - autoMap: true, - }, - }); + // Define API configurations for the dialogs + const aliasApiConfig = { + type: "POST", + url: "/api/SetUserAliases", + relatedQueryKeys: `ListUsers-${userId}`, + confirmText: "Add the specified proxy addresses to this user?", + }; - const fullAccessValue = permissionsFormControl.watch("fullAccess"); + const permissionsApiConfig = { + type: "POST", + url: "/api/ExecModifyMBPerms", + relatedQueryKeys: `Mailbox-${userId}`, + confirmText: "Add the specified permissions to this mailbox?", + }; + + const calendarPermissionsApiConfig = { + type: "POST", + url: "/api/ExecModifyCalPerms", + relatedQueryKeys: `CalendarPermissions-${userId}`, + confirmText: "Add the specified permissions to this calendar?", + }; const setUserAliases = ApiPostCall({ relatedQueryKeys: `ListUsers-${userId}`, datafromUrl: true, }); - useEffect(() => { - const subscription = permissionsFormControl.watch((value, { name, type }) => {}); - return () => subscription.unsubscribe(); - }, [permissionsFormControl]); + // This effect is no longer needed since we use CippApiDialog for form handling useEffect(() => { - if (showAddPermissionsDialog) { - permissionsFormControl.reset({ - fullAccess: "", - sendAs: "", - sendOnBehalf: "", - autoMap: true, - }); + if (permissionsDialog.open) { usersList.refetch(); } - }, [showAddPermissionsDialog]); + }, [permissionsDialog.open]); useEffect(() => { if (oooRequest.isSuccess) { @@ -251,74 +247,7 @@ const Page = () => { relatedQueryKeys: `Mailbox-${userId}`, }); - const handleAddPermissions = () => { - const values = formControl.getValues(); - const permissions = []; - - // Build permissions array based on form values - if (values.permissions?.AddFullAccess) { - permissions.push({ - UserID: values.permissions.AddFullAccess, - PermissionLevel: "FullAccess", - Modification: "Add", - AutoMap: autoMap, - }); - } - if (values.permissions?.AddSendAs) { - permissions.push({ - UserID: values.permissions.AddSendAs, - PermissionLevel: "SendAs", - Modification: "Add", - }); - } - if (values.permissions?.AddSendOnBehalf) { - permissions.push({ - UserID: values.permissions.AddSendOnBehalf, - PermissionLevel: "SendOnBehalf", - Modification: "Add", - }); - } - - if (permissions.length === 0) return; - - setIsSubmittingPermissions(true); - setSubmitPermissionsResult(null); - - addPermissionsMutation.mutate( - { - url: "/api/ExecModifyMBPerms", - data: { - userID: graphUserRequest.data?.[0]?.userPrincipalName, - tenantFilter: userSettingsDefaults.currentTenant, - permissions: permissions, - }, - }, - { - onSuccess: (response) => { - setSubmitPermissionsResult({ - success: true, - message: response.data?.Results?.join("\n") || "Permissions added successfully", - }); - userRequest.refetch(); - setTimeout(() => { - setShowAddPermissionsDialog(false); - formControl.reset(); - setSubmitPermissionsResult(null); - }, 1500); - }, - onError: (error) => { - setSubmitPermissionsResult({ - success: false, - message: error.message || "Failed to add permissions", - }); - }, - } - ); - }; - - const handleOpenPermissionsDialog = () => { - setShowAddPermissionsDialog(true); - }; + // Permissions dialog functionality is now handled by the CippPermissionsDialog component const permissions = [ { @@ -341,7 +270,7 @@ const Page = () => { cardLabelBoxActions: ( - - - {aliasList.length === 0 ? ( - - No aliases added yet - - ) : ( - aliasList.map((alias) => ( - handleDeleteAlias(alias)} - color="primary" - variant="outlined" - /> - )) - )} - - - - - - - - - - setShowAddPermissionsDialog(false)} - maxWidth="sm" - fullWidth + {({ formHook }) => } + + + { + const permissions = []; + const { permissions: permissionValues } = data; + const autoMap = data.autoMap === undefined ? true : data.autoMap; + + // Build permissions array based on form values + if (permissionValues?.AddFullAccess) { + permissions.push({ + UserID: permissionValues.AddFullAccess, + PermissionLevel: "FullAccess", + Modification: "Add", + AutoMap: autoMap, + }); + } + if (permissionValues?.AddSendAs) { + permissions.push({ + UserID: permissionValues.AddSendAs, + PermissionLevel: "SendAs", + Modification: "Add", + }); + } + if (permissionValues?.AddSendOnBehalf) { + permissions.push({ + UserID: permissionValues.AddSendOnBehalf, + PermissionLevel: "SendOnBehalf", + Modification: "Add", + }); + } + + return { + userID: graphUserRequest.data?.[0]?.userPrincipalName, + tenantFilter: userSettingsDefaults.currentTenant, + permissions: permissions, + }; + }} > - - - Add Mailbox Permissions - setShowAddPermissionsDialog(false)} size="small"> - - - - - - - - ({ - value: user.userPrincipalName, - label: `${user.displayName} (${user.userPrincipalName})`, - })) || [] - } - formControl={formControl} - /> - {formControl.watch("permissions.AddFullAccess") && ( - { - setAutoMap(e.target.checked); - }} - /> - } - label="Enable Automapping" - sx={{ mt: 0.5, ml: 0.5 }} - /> - )} - - - ({ - value: user.userPrincipalName, - label: `${user.displayName} (${user.userPrincipalName})`, - })) || [] - } - formControl={formControl} - /> - - - ({ - value: user.userPrincipalName, - label: `${user.displayName} (${user.userPrincipalName})`, - })) || [] - } - formControl={formControl} - /> - - - - - - - - - - setShowAddCalendarPermissionsDialog(false)} - maxWidth="sm" - fullWidth + /> + )} + + + { + if (!data.UserToGetPermissions || !data.Permissions) return null; + + // Build permission object dynamically + const permission = { + UserID: data.UserToGetPermissions, + PermissionLevel: data.Permissions, + Modification: "Add", + }; + + if (data.CanViewPrivateItems) { + permission.CanViewPrivateItems = true; + } + + return { + userID: graphUserRequest.data?.[0]?.userPrincipalName, + tenantFilter: userSettingsDefaults.currentTenant, + permissions: [permission], + }; + }} > - - - Add Calendar Permissions - setShowAddCalendarPermissionsDialog(false)} size="small"> - - - - - - - - ({ - value: user.userPrincipalName, - label: `${user.displayName} (${user.userPrincipalName})`, - })) || [] - } - multiple={false} - formControl={calendarPermissionsFormControl} - /> - - - - value ? true : "Select the permission level for the calendar", - }} - isFetching={userRequest.isFetching || usersList.isFetching} - options={[ - { value: "Author", label: "Author" }, - { value: "Contributor", label: "Contributor" }, - { value: "Editor", label: "Editor" }, - { value: "Owner", label: "Owner" }, - { value: "NonEditingAuthor", label: "Non Editing Author" }, - { value: "PublishingAuthor", label: "Publishing Author" }, - { value: "PublishingEditor", label: "Publishing Editor" }, - { value: "Reviewer", label: "Reviewer" }, - { value: "LimitedDetails", label: "Limited Details" }, - { value: "AvailabilityOnly", label: "Availability Only" }, - ]} - multiple={false} - formControl={calendarPermissionsFormControl} - /> - - - {(() => { - const permissionLevel = calendarPermissionsFormControl.watch("Permissions"); - const isEditor = permissionLevel?.value === "Editor"; - - useEffect(() => { - if (!isEditor) { - calendarPermissionsFormControl.setValue("CanViewPrivateItems", false); - } - }, [isEditor, calendarPermissionsFormControl]); - - return ( - - - - - - ); - })()} - - - - - - - - - + {({ formHook }) => } + ); }; From a05b65ec0aabf8502b6ce5fffb96954dcb04063b Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 4 Jun 2025 22:42:35 -0400 Subject: [PATCH 36/42] fix bulk actions for mailbox/calendar permissions --- .../CippComponents/CippApiDialog.jsx | 58 ++--- .../administration/users/user/exchange.jsx | 238 +++++++++--------- 2 files changed, 151 insertions(+), 145 deletions(-) diff --git a/src/components/CippComponents/CippApiDialog.jsx b/src/components/CippComponents/CippApiDialog.jsx index 779a0cd320fb..001d00d167b0 100644 --- a/src/components/CippComponents/CippApiDialog.jsx +++ b/src/components/CippComponents/CippApiDialog.jsx @@ -27,7 +27,6 @@ export const CippApiDialog = (props) => { dialogAfterEffect, allowResubmit = false, children, - customDataformatter, ...other } = props; const router = useRouter(); @@ -113,8 +112,8 @@ export const CippApiDialog = (props) => { const handleActionClick = (row, action, formData) => { setIsFormSubmitted(true); let finalData = {}; - if (typeof customDataformatter === "function") { - finalData = customDataformatter(row, action, formData); + if (typeof api?.customDataformatter === "function") { + finalData = api.customDataformatter(row, action, formData); } else { if (action.multiPost === undefined) action.multiPost = false; @@ -131,36 +130,39 @@ export const CippApiDialog = (props) => { }; const processedActionData = processActionData(action.data, row, action.replacementBehaviour); - // MULTI ROW CASES - if (Array.isArray(row)) { - const arrayData = row.map((singleRow) => { - const itemData = { ...commonData }; - Object.keys(processedActionData).forEach((key) => { - const rowValue = singleRow[processedActionData[key]]; - itemData[key] = rowValue !== undefined ? rowValue : processedActionData[key]; + if (!processedActionData || Object.keys(processedActionData).length === 0) { + console.warn("No data to process for action:", action); + } else { + // MULTI ROW CASES + if (Array.isArray(row)) { + const arrayData = row.map((singleRow) => { + const itemData = { ...commonData }; + Object.keys(processedActionData).forEach((key) => { + const rowValue = singleRow[processedActionData[key]]; + itemData[key] = rowValue !== undefined ? rowValue : processedActionData[key]; + }); + return itemData; }); - return itemData; - }); - const payload = { - url: action.url, - bulkRequest: !action.multiPost, - data: arrayData, - }; + const payload = { + url: action.url, + bulkRequest: !action.multiPost, + data: arrayData, + }; - if (action.type === "POST") { - actionPostRequest.mutate(payload); - } else if (action.type === "GET") { - setGetRequestInfo({ - ...payload, - waiting: true, - queryKey: Date.now(), - }); - } + if (action.type === "POST") { + actionPostRequest.mutate(payload); + } else if (action.type === "GET") { + setGetRequestInfo({ + ...payload, + waiting: true, + queryKey: Date.now(), + }); + } - return; + return; + } } - // ✅ FIXED: DIRECT MERGE INSTEAD OF CORRUPT TRANSFORMATION finalData = { ...commonData, diff --git a/src/pages/identity/administration/users/user/exchange.jsx b/src/pages/identity/administration/users/user/exchange.jsx index 4ce7fa786c26..cde8534e81e5 100644 --- a/src/pages/identity/administration/users/user/exchange.jsx +++ b/src/pages/identity/administration/users/user/exchange.jsx @@ -12,7 +12,6 @@ import { Launch, Delete, Star, - Close, CalendarToday, AlternateEmail, PersonAdd, @@ -28,23 +27,7 @@ import { CippExchangeInfoCard } from "../../../../../components/CippCards/CippEx import { useEffect, useState } from "react"; import CippExchangeSettingsForm from "../../../../../components/CippFormPages/CippExchangeSettingsForm"; import { useForm } from "react-hook-form"; -import { - Alert, - Button, - Collapse, - CircularProgress, - Typography, - TextField, - Dialog, - DialogTitle, - DialogContent, - DialogActions, - IconButton, - FormControlLabel, - Switch, - Tooltip, - Chip, -} from "@mui/material"; +import { Alert, Button, Collapse, CircularProgress, Typography } from "@mui/material"; import { CippApiResults } from "../../../../../components/CippComponents/CippApiResults"; import { Block, PlayArrow, Add } from "@mui/icons-material"; import { CippPropertyListCard } from "../../../../../components/CippCards/CippPropertyListCard"; @@ -53,7 +36,6 @@ import { getCippFormatting } from "../../../../../utils/get-cipp-formatting"; import CippExchangeActions from "../../../../../components/CippComponents/CippExchangeActions"; import { CippApiDialog } from "../../../../../components/CippComponents/CippApiDialog"; import { useDialog } from "../../../../../hooks/use-dialog"; -import { CippFormComponent } from "../../../../../components/CippComponents/CippFormComponent"; import CippAliasDialog from "../../../../../components/CippComponents/CippAliasDialog"; import CippMailboxPermissionsDialog from "../../../../../components/CippComponents/CippMailboxPermissionsDialog"; import CippCalendarPermissionsDialog from "../../../../../components/CippComponents/CippCalendarPermissionsDialog"; @@ -123,6 +105,14 @@ const Page = () => { url: "/api/SetUserAliases", relatedQueryKeys: `ListUsers-${userId}`, confirmText: "Add the specified proxy addresses to this user?", + customDataformatter: (row, action, formData) => { + return { + id: userId, + tenantFilter: userSettingsDefaults.currentTenant, + AddedAliases: formData?.AddedAliases?.join(",") || "", + userPrincipalName: graphUserRequest?.data?.[0]?.userPrincipalName, + }; + }, }; const permissionsApiConfig = { @@ -130,6 +120,41 @@ const Page = () => { url: "/api/ExecModifyMBPerms", relatedQueryKeys: `Mailbox-${userId}`, confirmText: "Add the specified permissions to this mailbox?", + customDataformatter: (row, action, data) => { + const permissions = []; + const { permissions: permissionValues } = data; + const autoMap = data.autoMap === undefined ? true : data.autoMap; + + // Build permissions array based on form values + if (permissionValues?.AddFullAccess) { + permissions.push({ + UserID: permissionValues.AddFullAccess, + PermissionLevel: "FullAccess", + Modification: "Add", + AutoMap: autoMap, + }); + } + if (permissionValues?.AddSendAs) { + permissions.push({ + UserID: permissionValues.AddSendAs, + PermissionLevel: "SendAs", + Modification: "Add", + }); + } + if (permissionValues?.AddSendOnBehalf) { + permissions.push({ + UserID: permissionValues.AddSendOnBehalf, + PermissionLevel: "SendOnBehalf", + Modification: "Add", + }); + } + + return { + userID: graphUserRequest.data?.[0]?.userPrincipalName, + tenantFilter: userSettingsDefaults.currentTenant, + permissions: permissions, + }; + }, }; const calendarPermissionsApiConfig = { @@ -137,12 +162,27 @@ const Page = () => { url: "/api/ExecModifyCalPerms", relatedQueryKeys: `CalendarPermissions-${userId}`, confirmText: "Add the specified permissions to this calendar?", - }; + customDataformatter: (row, action, data) => { + if (!data.UserToGetPermissions || !data.Permissions) return null; - const setUserAliases = ApiPostCall({ - relatedQueryKeys: `ListUsers-${userId}`, - datafromUrl: true, - }); + // Build permission object dynamically + const permission = { + UserID: data.UserToGetPermissions, + PermissionLevel: data.Permissions, + Modification: "Add", + }; + + if (data.CanViewPrivateItems) { + permission.CanViewPrivateItems = true; + } + + return { + userID: graphUserRequest.data?.[0]?.userPrincipalName, + tenantFilter: userSettingsDefaults.currentTenant, + permissions: [permission], + }; + }, + }; // This effect is no longer needed since we use CippApiDialog for form handling @@ -226,16 +266,32 @@ const Page = () => { type: "POST", icon: , url: "/api/ExecModifyMBPerms", - data: { - userID: graphUserRequest.data?.[0]?.userPrincipalName, - tenantFilter: userSettingsDefaults.currentTenant, - permissions: [ - { - UserID: "User", - PermissionLevel: "AccessRights", + customDataformatter: (row, action, formData) => { + // build permissions + var permissions = []; + // if the row is an array, iterate through it + if (Array.isArray(row)) { + row.forEach((item) => { + permissions.push({ + UserID: item.User, + PermissionLevel: item.AccessRights, + Modification: "Remove", + }); + }); + } else { + // if it's a single object, just push it + permissions.push({ + UserID: row.User, + PermissionLevel: row.AccessRights, Modification: "Remove", - }, - ], + }); + } + + return { + userID: graphUserRequest.data?.[0]?.userPrincipalName, + tenantFilter: userSettingsDefaults.currentTenant, + permissions: permissions, + }; }, confirmText: "Are you sure you want to remove this permission?", multiPost: false, @@ -243,12 +299,6 @@ const Page = () => { }, ]; - const addPermissionsMutation = ApiPostCall({ - relatedQueryKeys: `Mailbox-${userId}`, - }); - - // Permissions dialog functionality is now handled by the CippPermissionsDialog component - const permissions = [ { id: 1, @@ -364,23 +414,40 @@ const Page = () => { type: "POST", icon: , url: "/api/ExecModifyCalPerms", - data: { - userID: graphUserRequest.data?.[0]?.userPrincipalName, - tenantFilter: userSettingsDefaults.currentTenant, - permissions: [ - { - UserID: "User", - PermissionLevel: "AccessRights", - FolderName: "FolderName", + customDataFormatter: (row, action, formData) => { + // build permissions + var permissions = []; + // if the row is an array, iterate through it + if (Array.isArray(row)) { + row.forEach((item) => { + permissions.push({ + UserID: item.User, + PermissionLevel: item.AccessRights, + FolderName: item.FolderName, + Modification: "Remove", + }); + }); + } else { + // if it's a single object, just push it + permissions.push({ + UserID: row.User, + PermissionLevel: row.AccessRights, + FolderName: row.FolderName, Modification: "Remove", - }, - ], + }); + } + return { + userID: graphUserRequest.data?.[0]?.userPrincipalName, + tenantFilter: userSettingsDefaults.currentTenant, + permissions: permissions, + }; }, + confirmText: "Are you sure you want to remove this calendar permission?", multiPost: false, relatedQueryKeys: `CalendarPermissions-${userId}`, hideBulk: true, - condition: (row) => row.User !== "Default" && row.User !== "Anonymous", + condition: (row) => row.User !== "Default" && row.User == "Anonymous", }, ], offCanvas: { @@ -699,22 +766,22 @@ const Page = () => { { title="Add Proxy Addresses" api={aliasApiConfig} row={graphUserRequest.data?.[0]} - customDataformatter={(row, action, formData) => { - return { - id: userId, - tenantFilter: userSettingsDefaults.currentTenant, - AddedAliases: formData?.AddedAliases?.join(",") || "", - userPrincipalName: graphUserRequest?.data?.[0]?.userPrincipalName, - }; - }} > {({ formHook }) => } @@ -761,41 +820,6 @@ const Page = () => { api={permissionsApiConfig} row={graphUserRequest.data?.[0]} allowResubmit={true} - customDataformatter={(row, action, data) => { - const permissions = []; - const { permissions: permissionValues } = data; - const autoMap = data.autoMap === undefined ? true : data.autoMap; - - // Build permissions array based on form values - if (permissionValues?.AddFullAccess) { - permissions.push({ - UserID: permissionValues.AddFullAccess, - PermissionLevel: "FullAccess", - Modification: "Add", - AutoMap: autoMap, - }); - } - if (permissionValues?.AddSendAs) { - permissions.push({ - UserID: permissionValues.AddSendAs, - PermissionLevel: "SendAs", - Modification: "Add", - }); - } - if (permissionValues?.AddSendOnBehalf) { - permissions.push({ - UserID: permissionValues.AddSendOnBehalf, - PermissionLevel: "SendOnBehalf", - Modification: "Add", - }); - } - - return { - userID: graphUserRequest.data?.[0]?.userPrincipalName, - tenantFilter: userSettingsDefaults.currentTenant, - permissions: permissions, - }; - }} > {({ formHook }) => ( { api={calendarPermissionsApiConfig} row={graphUserRequest.data?.[0]} allowResubmit={true} - customDataformatter={(row, action, data) => { - if (!data.UserToGetPermissions || !data.Permissions) return null; - - // Build permission object dynamically - const permission = { - UserID: data.UserToGetPermissions, - PermissionLevel: data.Permissions, - Modification: "Add", - }; - - if (data.CanViewPrivateItems) { - permission.CanViewPrivateItems = true; - } - - return { - userID: graphUserRequest.data?.[0]?.userPrincipalName, - tenantFilter: userSettingsDefaults.currentTenant, - permissions: [permission], - }; - }} > {({ formHook }) => } From 8eacfcb111d2052371d0c342caac5963a6055d8b Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 4 Jun 2025 22:47:45 -0400 Subject: [PATCH 37/42] restore bulk calendar removal functionality --- src/pages/identity/administration/users/user/exchange.jsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/pages/identity/administration/users/user/exchange.jsx b/src/pages/identity/administration/users/user/exchange.jsx index cde8534e81e5..da62762ed8e9 100644 --- a/src/pages/identity/administration/users/user/exchange.jsx +++ b/src/pages/identity/administration/users/user/exchange.jsx @@ -414,7 +414,8 @@ const Page = () => { type: "POST", icon: , url: "/api/ExecModifyCalPerms", - customDataFormatter: (row, action, formData) => { + customDataformatter: (row, action, formData) => { + console.log(row); // build permissions var permissions = []; // if the row is an array, iterate through it @@ -442,11 +443,9 @@ const Page = () => { permissions: permissions, }; }, - confirmText: "Are you sure you want to remove this calendar permission?", multiPost: false, relatedQueryKeys: `CalendarPermissions-${userId}`, - hideBulk: true, condition: (row) => row.User !== "Default" && row.User == "Anonymous", }, ], From 69ba2de372b14a77b87ab93ed0b74adb0d1fb6dc Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 4 Jun 2025 22:56:29 -0400 Subject: [PATCH 38/42] Update exchange.jsx --- src/pages/identity/administration/users/user/exchange.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/identity/administration/users/user/exchange.jsx b/src/pages/identity/administration/users/user/exchange.jsx index da62762ed8e9..9b8faac78fe7 100644 --- a/src/pages/identity/administration/users/user/exchange.jsx +++ b/src/pages/identity/administration/users/user/exchange.jsx @@ -415,7 +415,6 @@ const Page = () => { icon: , url: "/api/ExecModifyCalPerms", customDataformatter: (row, action, formData) => { - console.log(row); // build permissions var permissions = []; // if the row is an array, iterate through it From 1b149403b039ac2bb67c4ae2e3a52215026b47e6 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 4 Jun 2025 23:07:47 -0400 Subject: [PATCH 39/42] align buttons --- src/components/CippFormPages/CippExchangeSettingsForm.jsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/CippFormPages/CippExchangeSettingsForm.jsx b/src/components/CippFormPages/CippExchangeSettingsForm.jsx index 0e6ab44d81bd..66974e381073 100644 --- a/src/components/CippFormPages/CippExchangeSettingsForm.jsx +++ b/src/components/CippFormPages/CippExchangeSettingsForm.jsx @@ -20,6 +20,7 @@ import { useSettings } from "../../hooks/use-settings"; import { Grid } from "@mui/system"; import { CippApiResults } from "../CippComponents/CippApiResults"; import { useWatch } from "react-hook-form"; +import { ChevronDownIcon } from "@heroicons/react/24/outline"; const CippExchangeSettingsForm = (props) => { const userSettingsDefaults = useSettings(); @@ -295,7 +296,9 @@ const CippExchangeSettingsForm = (props) => { alignItems: "center", display: "flex", justifyContent: "space-between", - p: 2, + py: 3, + pl: 2, + pr: 4, cursor: "pointer", "&:hover": { bgcolor: "action.hover", @@ -338,7 +341,7 @@ const CippExchangeSettingsForm = (props) => { transform: isExpanded ? "rotate(180deg)" : "rotate(0deg)", }} > - + From f285dece922808534931b9cee9e83656b77722fb Mon Sep 17 00:00:00 2001 From: Jack Date: Thu, 5 Jun 2025 12:14:33 +0100 Subject: [PATCH 40/42] Out of Office - Set date fields to disabled when Auto Reply State is not Scheduled --- .../CippExchangeSettingsForm.jsx | 65 +++++++++++++++---- 1 file changed, 53 insertions(+), 12 deletions(-) diff --git a/src/components/CippFormPages/CippExchangeSettingsForm.jsx b/src/components/CippFormPages/CippExchangeSettingsForm.jsx index 66974e381073..767e3cf43e85 100644 --- a/src/components/CippFormPages/CippExchangeSettingsForm.jsx +++ b/src/components/CippFormPages/CippExchangeSettingsForm.jsx @@ -29,6 +29,31 @@ const CippExchangeSettingsForm = (props) => { const [expandedPanel, setExpandedPanel] = useState(null); const [relatedQueryKeys, setRelatedQueryKeys] = useState([]); + // Watch the Auto Reply State value + const autoReplyState = useWatch({ + control: formControl.control, + name: "ooo.AutoReplyState", + }); + + // Calculate if date fields should be disabled + const areDateFieldsDisabled = autoReplyState?.value !== "Scheduled"; + + useEffect(() => { + console.log('Auto Reply State changed:', { + autoReplyState, + areDateFieldsDisabled, + fullFormValues: formControl.getValues() + }); + }, [autoReplyState]); + + // Add debug logging for form values + useEffect(() => { + const subscription = formControl.watch((value, { name, type }) => { + console.log('Form value changed:', { name, type, value }); + }); + return () => subscription.unsubscribe(); + }, [formControl]); + const handleExpand = (panel) => { setExpandedPanel((prev) => (prev === panel ? null : panel)); }; @@ -199,20 +224,36 @@ const CippExchangeSettingsForm = (props) => { /> - + + + + + - + + + + + Date: Thu, 5 Jun 2025 14:44:05 -0400 Subject: [PATCH 41/42] add tenantfilter to compare --- src/pages/tenant/standards/compare/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/tenant/standards/compare/index.js b/src/pages/tenant/standards/compare/index.js index b26632b4befd..c64ff14a4bbb 100644 --- a/src/pages/tenant/standards/compare/index.js +++ b/src/pages/tenant/standards/compare/index.js @@ -79,7 +79,7 @@ const Page = () => { url: "/api/ListStandardsCompare", data: { TemplateId: templateId, - CompareTenantId: formControl.watch("compareTenantId"), + tenantFilter: currentTenant, CompareToStandard: true, // Always compare to standard, even in tenant comparison mode }, queryKey: `ListStandardsCompare-${templateId}-${ From a91c84a0535046b28ae36242ff41671ba0abb94d Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 5 Jun 2025 14:49:07 -0400 Subject: [PATCH 42/42] up version --- public/version.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/version.json b/public/version.json index d18f79dee972..f47e65940e3b 100644 --- a/public/version.json +++ b/public/version.json @@ -1,3 +1,3 @@ { - "version": "8.0.1" -} + "version": "8.0.2" +} \ No newline at end of file