From 1e5f450b9bf08ebf17d6a040f35fb4962c9c2091 Mon Sep 17 00:00:00 2001
From: Zacgoose <107489668+Zacgoose@users.noreply.github.com>
Date: Tue, 16 Jun 2026 18:00:06 +0800
Subject: [PATCH 01/19] bookmark cleanup
---
src/hooks/use-user-bookmarks.js | 86 +------------------------------
src/layouts/side-nav-bookmarks.js | 22 ++++----
src/layouts/top-nav.js | 9 +---
3 files changed, 16 insertions(+), 101 deletions(-)
diff --git a/src/hooks/use-user-bookmarks.js b/src/hooks/use-user-bookmarks.js
index 7427ea5c06f0..bbb977522942 100644
--- a/src/hooks/use-user-bookmarks.js
+++ b/src/hooks/use-user-bookmarks.js
@@ -1,9 +1,7 @@
-import { useCallback, useEffect, useMemo, useRef } from "react";
+import { useCallback, useMemo } from "react";
import { useQueryClient } from "@tanstack/react-query";
import { ApiGetCall, ApiPostCall } from "../api/ApiCall";
-const SETTINGS_STORAGE_KEY = "app.settings";
-
const sanitizeBookmark = (bookmark) => {
if (!bookmark || typeof bookmark !== "object") {
return null;
@@ -43,47 +41,6 @@ const normalizeBookmarks = (value) => {
return [];
};
-const getLocalStoredBookmarks = () => {
- if (typeof window === "undefined") {
- return [];
- }
-
- try {
- const restored = window.localStorage.getItem(SETTINGS_STORAGE_KEY);
- if (!restored) {
- return [];
- }
-
- const parsed = JSON.parse(restored);
- return normalizeBookmarks(parsed?.bookmarks);
- } catch {
- return [];
- }
-};
-
-const clearLocalStoredBookmarks = () => {
- if (typeof window === "undefined") {
- return;
- }
-
- try {
- const restored = window.localStorage.getItem(SETTINGS_STORAGE_KEY);
- if (!restored) {
- return;
- }
-
- const parsed = JSON.parse(restored);
- if (!parsed || typeof parsed !== "object" || !Object.prototype.hasOwnProperty.call(parsed, "bookmarks")) {
- return;
- }
-
- delete parsed.bookmarks;
- window.localStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(parsed));
- } catch {
- return;
- }
-};
-
const getBookmarksFromSettings = (settingsData) => {
if (!settingsData) {
return [];
@@ -102,8 +59,6 @@ const getBookmarksFromSettings = (settingsData) => {
export const useUserBookmarks = () => {
const queryClient = useQueryClient();
- const localMigrationComplete = useRef(false);
- const localMigrationInFlight = useRef(false);
const userSettings = ApiGetCall({
url: "/api/ListUserSettings",
@@ -163,47 +118,10 @@ export const useUserBookmarks = () => {
[persistBookmarks]
);
- useEffect(() => {
- if (localMigrationComplete.current || localMigrationInFlight.current) {
- return;
- }
-
- if (!auth.data?.clientPrincipal?.userDetails) {
- return;
- }
-
- if (bookmarks.length > 0) {
- localMigrationComplete.current = true;
- return;
- }
-
- const localBookmarks = getLocalStoredBookmarks();
- if (localBookmarks.length === 0) {
- localMigrationComplete.current = true;
- return;
- }
-
- localMigrationInFlight.current = true;
- const didPost = persistBookmarks(localBookmarks, {
- onSuccess: () => {
- clearLocalStoredBookmarks();
- localMigrationInFlight.current = false;
- localMigrationComplete.current = true;
- },
- onError: () => {
- localMigrationInFlight.current = false;
- },
- });
-
- if (!didPost) {
- localMigrationInFlight.current = false;
- }
- }, [auth.data?.clientPrincipal?.userDetails, bookmarks.length, persistBookmarks]);
-
return {
bookmarks,
setBookmarks,
isLoading: userSettings.isLoading,
isSaving: saveBookmarksPost.isPending,
};
-};
\ No newline at end of file
+};
diff --git a/src/layouts/side-nav-bookmarks.js b/src/layouts/side-nav-bookmarks.js
index 04f4a978609a..0ae0ec7abdec 100644
--- a/src/layouts/side-nav-bookmarks.js
+++ b/src/layouts/side-nav-bookmarks.js
@@ -522,16 +522,18 @@ export const SideNavBookmarks = ({ collapse = false }) => {
>
)}
- {
- e.preventDefault();
- removeBookmark(bookmark.path);
- }}
- sx={{ p: "2px" }}
- >
-
-
+ {!locked && (
+ {
+ e.preventDefault();
+ removeBookmark(bookmark.path);
+ }}
+ sx={{ p: "2px" }}
+ >
+
+
+ )}
diff --git a/src/layouts/top-nav.js b/src/layouts/top-nav.js
index 10bdcefd9581..fb5c7483e20f 100644
--- a/src/layouts/top-nav.js
+++ b/src/layouts/top-nav.js
@@ -54,7 +54,7 @@ export const TopNav = (props) => {
const mdDown = useMediaQuery((theme) => theme.breakpoints.down('md'))
const showPopoverBookmarks = settings.bookmarkPopover === true
const reorderMode = settings.bookmarkReorderMode || 'arrows'
- const locked = settings.bookmarkLocked ?? false
+ const locked = settings.bookmarkLocked ?? true
const handleThemeSwitch = useCallback(() => {
const themeName = settings.currentTheme?.value === 'light' ? 'dark' : 'light'
settings.handleUpdate({
@@ -590,18 +590,13 @@ export const TopNav = (props) => {
>
)}
- {!(reorderMode === 'drag' && locked) && (
+ {!locked && (
{
e.preventDefault()
- if (locked) {
- triggerLockFlash()
- return
- }
removeBookmark(bookmark.path)
}}
- sx={{ ...(locked && { opacity: 0.4 }) }}
>
From 3c82750d10f50983c940e9b4069eb5b4b002320f Mon Sep 17 00:00:00 2001
From: Zacgoose <107489668+Zacgoose@users.noreply.github.com>
Date: Tue, 16 Jun 2026 18:21:04 +0800
Subject: [PATCH 02/19] Warning when converting mailbox that is over 49GB
---
.../CippWizard/CippWizardOffboarding.jsx | 55 ++++++++++++++++++-
1 file changed, 54 insertions(+), 1 deletion(-)
diff --git a/src/components/CippWizard/CippWizardOffboarding.jsx b/src/components/CippWizard/CippWizardOffboarding.jsx
index 990cb9d35b11..2fc5947c22bf 100644
--- a/src/components/CippWizard/CippWizardOffboarding.jsx
+++ b/src/components/CippWizard/CippWizardOffboarding.jsx
@@ -12,9 +12,13 @@ import CippWizardStepButtons from './CippWizardStepButtons'
import CippFormComponent from '../CippComponents/CippFormComponent'
import { CippFormCondition } from '../CippComponents/CippFormCondition'
import { useWatch } from 'react-hook-form'
-import { useEffect, useState } from 'react'
+import { useEffect, useMemo, useState } from 'react'
import { Grid } from '@mui/system'
import { useSettings } from '../../hooks/use-settings'
+import { ApiGetCall } from '../../api/ApiCall'
+
+// Shared mailboxes are capped at 50 GiB without a license; warn at 49 GiB.
+const SHARED_MAILBOX_WARN_BYTES = 49 * 1024 ** 3
export const CippWizardOffboarding = (props) => {
const { postUrl, formControl, onPreviousStep, onNextStep, currentStep } = props
@@ -24,6 +28,40 @@ export const CippWizardOffboarding = (props) => {
const userSettingsDefaults = useSettings().userSettingsDefaults
const disableForwarding = useWatch({ control: formControl.control, name: 'disableForwarding' })
const deleteUser = useWatch({ control: formControl.control, name: 'DeleteUser' })
+ const convertToShared = useWatch({ control: formControl.control, name: 'ConvertToShared' })
+
+ // Pull cached mailbox sizes (storageUsedInBytes, keyed by UPN) only when relevant
+ const mailboxUsage = ApiGetCall({
+ url: '/api/ListMailboxes',
+ data: { tenantFilter: currentTenant?.value, UseReportDB: true },
+ queryKey: `OffboardingMailboxUsage-${currentTenant?.value}`,
+ waiting: !!convertToShared && !!currentTenant?.value && selectedUsers?.length > 0,
+ })
+
+ // Selected mailboxes whose cached size would exceed the shared-mailbox limit
+ const oversizedMailboxes = useMemo(() => {
+ if (!convertToShared || !mailboxUsage.isSuccess || !Array.isArray(mailboxUsage.data)) {
+ return []
+ }
+ const selectedUpns = (selectedUsers || []).map((u) =>
+ (u?.value ?? u)?.toString().toLowerCase(),
+ )
+ return mailboxUsage.data
+ .filter((mb) => {
+ const upn = mb?.UPN?.toString().toLowerCase()
+ const bytes = Number(mb?.storageUsedInBytes)
+ return (
+ upn &&
+ selectedUpns.includes(upn) &&
+ Number.isFinite(bytes) &&
+ bytes >= SHARED_MAILBOX_WARN_BYTES
+ )
+ })
+ .map((mb) => ({
+ upn: mb.UPN,
+ sizeGB: (Number(mb.storageUsedInBytes) / 1024 ** 3).toFixed(1),
+ }))
+ }, [convertToShared, mailboxUsage.isSuccess, mailboxUsage.data, selectedUsers])
useEffect(() => {
if (selectedUsers.length >= 3) {
@@ -383,6 +421,21 @@ export const CippWizardOffboarding = (props) => {
formControl={formControl}
/>
+ {convertToShared && oversizedMailboxes.length > 0 && (
+
+ The following mailbox{oversizedMailboxes.length > 1 ? 'es' : ''} exceed or are near
+ the 50 GB shared mailbox limit. Converting to shared may fail, or the mailbox may
+ stop receiving mail once unlicensed, unless an Exchange Online Plan 2 license is
+ retained:
+
+ {oversizedMailboxes.map((mb) => (
+
+ {mb.upn} ({mb.sizeGB} GB)
+
+ ))}
+
+
+ )}
From ca78e05c8d02c71e4916af1d9a4a3f241d0d3fd0 Mon Sep 17 00:00:00 2001
From: Zacgoose <107489668+Zacgoose@users.noreply.github.com>
Date: Tue, 16 Jun 2026 20:19:42 +0800
Subject: [PATCH 03/19] multi post action for multiple spo site cleanup
---
src/pages/teams-share/sharepoint/index.js | 23 ++++++++++++++---------
1 file changed, 14 insertions(+), 9 deletions(-)
diff --git a/src/pages/teams-share/sharepoint/index.js b/src/pages/teams-share/sharepoint/index.js
index 42f08fdc0486..b39b143c6eee 100644
--- a/src/pages/teams-share/sharepoint/index.js
+++ b/src/pages/teams-share/sharepoint/index.js
@@ -264,15 +264,20 @@ const Page = () => {
defaultvalues: {
BatchDeleteMode: '2',
},
- customDataformatter: (row, action, formData) => ({
- tenantFilter: row.Tenant ?? tenantFilter,
- SiteUrl: row.webUrl,
- BatchDeleteMode: parseInt(formData.BatchDeleteMode, 10),
- DeleteOlderThanDays:
- formData.BatchDeleteMode === '0' ? parseInt(formData.DeleteOlderThanDays, 10) : -1,
- MajorVersionLimit:
- formData.BatchDeleteMode === '1' ? parseInt(formData.MajorVersionLimit, 10) : -1,
- }),
+ customDataformatter: (row, action, formData) => {
+ const formatRow = (singleRow) => ({
+ tenantFilter: singleRow.Tenant ?? tenantFilter,
+ SiteUrl: singleRow.webUrl,
+ BatchDeleteMode: parseInt(formData.BatchDeleteMode, 10),
+ DeleteOlderThanDays:
+ formData.BatchDeleteMode === '0' ? parseInt(formData.DeleteOlderThanDays, 10) : -1,
+ MajorVersionLimit:
+ formData.BatchDeleteMode === '1' ? parseInt(formData.MajorVersionLimit, 10) : -1,
+ })
+ // When multiple rows are selected, row is an array. Returning an array
+ // makes CippApiDialog send one request per row (bulk request mode).
+ return Array.isArray(row) ? row.map(formatRow) : formatRow(row)
+ },
multiPost: false,
},
]
From ff8197060250dbffdd3a65b0392329695b00d184 Mon Sep 17 00:00:00 2001
From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com>
Date: Wed, 17 Jun 2026 14:18:06 +0200
Subject: [PATCH 04/19] required = true
---
src/pages/tenant/manage/drift.js | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/pages/tenant/manage/drift.js b/src/pages/tenant/manage/drift.js
index 97bcd6346c36..eabac2dfb1c6 100644
--- a/src/pages/tenant/manage/drift.js
+++ b/src/pages/tenant/manage/drift.js
@@ -2068,6 +2068,7 @@ const ManageDriftPage = () => {
type: 'textField',
name: 'reason',
label: 'Reason for change (Mandatory)',
+ required: true,
},
...(actionData.data?.deviations?.some((d) => d.status === 'DeniedRemediate')
? [
From 383df0af78fb667bc28b93ef943f7c49bc6c8770 Mon Sep 17 00:00:00 2001
From: Zacgoose <107489668+Zacgoose@users.noreply.github.com>
Date: Wed, 17 Jun 2026 22:17:16 +0800
Subject: [PATCH 05/19] repair gdap role mapping action
---
.../CippSettings/CippGDAPResults.jsx | 76 +++++++++++++++++--
1 file changed, 71 insertions(+), 5 deletions(-)
diff --git a/src/components/CippSettings/CippGDAPResults.jsx b/src/components/CippSettings/CippGDAPResults.jsx
index 46d505a4535d..306c7451eb32 100644
--- a/src/components/CippSettings/CippGDAPResults.jsx
+++ b/src/components/CippSettings/CippGDAPResults.jsx
@@ -1,15 +1,34 @@
-import { Alert, List, ListItem, Skeleton, SvgIcon, Typography } from "@mui/material";
+import { Alert, Button, List, ListItem, Skeleton, SvgIcon, Typography } from "@mui/material";
import { Cancel, CheckCircle, Warning } from "@mui/icons-material";
import { CippPropertyList } from "../CippComponents/CippPropertyList";
-import { XMarkIcon } from "@heroicons/react/24/outline";
+import { WrenchIcon, XMarkIcon } from "@heroicons/react/24/outline";
import { CippOffCanvas } from "../CippComponents/CippOffCanvas";
import { CippDataTable } from "../CippTable/CippDataTable";
+import { ApiPostCall } from "../../api/ApiCall";
+import { CippApiResults } from "../CippComponents/CippApiResults";
import { useEffect, useState } from "react";
export const CippGDAPResults = (props) => {
const { executeCheck, offcanvasVisible, setOffcanvasVisible, importReport, setCardIcon } = props;
const [results, setResults] = useState({});
+ const repairRoleMappings = ApiPostCall({
+ urlFromData: true,
+ relatedQueryKeys: ["ExecAccessChecks-GDAP"],
+ });
+
+ const handleRepairRoleMappings = () => {
+ repairRoleMappings.mutate({
+ url: "/api/ExecGDAPRepairRoleMappings",
+ data: {},
+ queryKey: "RepairGDAPRoleMappings",
+ });
+ };
+
+ const hasRoleMappingIssues = results?.Results?.RoleMappingResults?.some(
+ (item) => item?.Status === "Stale" || item?.Status === "Missing",
+ );
+
useEffect(() => {
if (importReport) {
setResults(importReport);
@@ -19,7 +38,11 @@ export const CippGDAPResults = (props) => {
}, [executeCheck, importReport]);
useEffect(() => {
- if (results?.Results?.GDAPIssues?.length > 0 || results?.Results?.MissingGroups?.length > 0) {
+ if (
+ results?.Results?.GDAPIssues?.length > 0 ||
+ results?.Results?.MissingGroups?.length > 0 ||
+ hasRoleMappingIssues
+ ) {
setCardIcon();
} else {
setCardIcon();
@@ -77,6 +100,15 @@ export const CippGDAPResults = (props) => {
successMessage: "No Global Admin relationships found",
failureMessage: "Global Admin relationships found",
},
+ {
+ resultProperty: "RoleMappingResults",
+ matchProperty: "Status",
+ match: "^(Stale|Missing)$",
+ count: 0,
+ successMessage: "All GDAP role mappings reference existing security groups",
+ failureMessage:
+ "One or more GDAP role mappings reference stale or missing security groups. Click Details to repair.",
+ },
];
const propertyItems = [
@@ -154,13 +186,16 @@ export const CippGDAPResults = (props) => {
}}
extendedInfo={[]}
>
- {results?.Results?.GDAPIssues?.length > 0 && (
+ {results?.Results?.GDAPIssues?.filter((issue) => issue.Category !== "RoleMapping")
+ .length > 0 && (
<>
issue.Category !== "RoleMapping",
+ )}
simpleColumns={["Tenant", "Type", "Issue", "Link"]}
/>
>
@@ -178,6 +213,37 @@ export const CippGDAPResults = (props) => {
>
)}
+ {results?.Results?.RoleMappingResults?.length > 0 && (
+ <>
+
+
+
+
+ }
+ >
+ Repair Role Mappings
+
+ )
+ }
+ data={results?.Results?.RoleMappingResults}
+ simpleColumns={["RoleName", "GroupName", "GroupId", "Status", "Message"]}
+ />
+ >
+ )}
+
{results?.Results?.Memberships?.filter(
(membership) => membership?.["@odata.type"] === "#microsoft.graph.group",
).length > 0 && (
From 0e110a2e416c96f1353c978774c4f3fa1f12e407 Mon Sep 17 00:00:00 2001
From: Zacgoose <107489668+Zacgoose@users.noreply.github.com>
Date: Wed, 17 Jun 2026 22:19:13 +0800
Subject: [PATCH 06/19] Colliding query keys
---
src/components/CippComponents/CippAutocomplete.jsx | 5 ++++-
src/components/CippFormPages/CippAddEditUser.jsx | 2 +-
2 files changed, 5 insertions(+), 2 deletions(-)
diff --git a/src/components/CippComponents/CippAutocomplete.jsx b/src/components/CippComponents/CippAutocomplete.jsx
index 075f47ed29a1..04dd59da8e44 100644
--- a/src/components/CippComponents/CippAutocomplete.jsx
+++ b/src/components/CippComponents/CippAutocomplete.jsx
@@ -146,7 +146,10 @@ export const CippAutoComplete = React.forwardRef((props, ref) => {
const currentTenant = api?.tenantFilter ? api.tenantFilter : useSettings().currentTenant
useEffect(() => {
if (actionGetRequest.isSuccess && !actionGetRequest.isFetching) {
- const lastPage = actionGetRequest.data?.pages[actionGetRequest.data.pages.length - 1]
+ // Guard against a non-paginated cache shape (e.g. when a queryKey is accidentally shared
+ // with a useQuery/ApiGetCall consumer that stores a plain array instead of { pages }).
+ const pages = actionGetRequest.data?.pages
+ const lastPage = Array.isArray(pages) ? pages[pages.length - 1] : undefined
const nextLinkExists = lastPage?.Metadata?.nextLink
if (nextLinkExists) {
actionGetRequest.fetchNextPage()
diff --git a/src/components/CippFormPages/CippAddEditUser.jsx b/src/components/CippFormPages/CippAddEditUser.jsx
index 9a3559190372..15a5a52782c1 100644
--- a/src/components/CippFormPages/CippAddEditUser.jsx
+++ b/src/components/CippFormPages/CippAddEditUser.jsx
@@ -50,7 +50,7 @@ const CippAddEditUser = (props) => {
// Get all groups for the tenant
const tenantGroups = ApiGetCall({
url: `/api/ListGroups?tenantFilter=${tenantDomain}`,
- queryKey: `ListGroups-${tenantDomain}`,
+ queryKey: `TenantGroupsList-${tenantDomain}`,
refetchOnMount: false,
refetchOnReconnect: false,
})
From 61ddc970c2db1ea63ec52e3ec0ec4f6b8f385b98 Mon Sep 17 00:00:00 2001
From: Johan Aantjes <47614276+TargetCrafter@users.noreply.github.com>
Date: Wed, 17 Jun 2026 14:38:24 +0000
Subject: [PATCH 07/19] Change "All Deviations" labels to "Selected Deviations"
to prevent confusion.
---
src/pages/tenant/manage/drift.js | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/pages/tenant/manage/drift.js b/src/pages/tenant/manage/drift.js
index df2a3869dc38..dd0bf49a6959 100644
--- a/src/pages/tenant/manage/drift.js
+++ b/src/pages/tenant/manage/drift.js
@@ -1948,11 +1948,11 @@ const ManageDriftPage = () => {
onClick={() => handleBulkAction('accept-all-customer-specific')}
>
- Accept All Deviations - Customer Specific
+ Accept Selected Deviations - Customer Specific
{/* Only show delete option if there are template deviations that support deletion */}
{processedDriftData.currentDeviations.some(
@@ -1965,12 +1965,12 @@ const ManageDriftPage = () => {
) && (
)}