diff --git a/generate-placeholders.js b/generate-placeholders.js
index 2b34888fca7a..304e6402e4ed 100644
--- a/generate-placeholders.js
+++ b/generate-placeholders.js
@@ -43,7 +43,7 @@ const pages = [
{ title: "BPA Report Builder", path: "/tenant/tools/bpa-report-builder" },
{ title: "Standards", path: "/tenant/standards" },
{ title: "Edit Standards", path: "/tenant/standards/list-applied-standards" },
- { title: "List Standards", path: "/tenant/standards/list-standards" },
+ { title: "List Standards", path: "/tenant/standards" },
{ title: "Best Practice Analyser", path: "/tenant/standards/bpa-report" },
{ title: "Domains Analyser", path: "/tenant/standards/domains-analyser" },
{ title: "Conditional Access", path: "/tenant/administration" },
diff --git a/package.json b/package.json
index bfce16283445..6d01e9706b32 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "cipp",
- "version": "8.8.1",
+ "version": "8.8.2",
"author": "CIPP Contributors",
"homepage": "https://cipp.app/",
"bugs": {
diff --git a/public/version.json b/public/version.json
index e38593790096..7a3adc4ddaba 100644
--- a/public/version.json
+++ b/public/version.json
@@ -1,3 +1,3 @@
{
- "version": "8.8.1"
+ "version": "8.8.2"
}
\ No newline at end of file
diff --git a/src/components/CippComponents/CippBreadcrumbNav.jsx b/src/components/CippComponents/CippBreadcrumbNav.jsx
index ed7e4be84c1b..ff146073ae1c 100644
--- a/src/components/CippComponents/CippBreadcrumbNav.jsx
+++ b/src/components/CippComponents/CippBreadcrumbNav.jsx
@@ -16,7 +16,7 @@ async function loadTabOptions() {
"/email/administration/exchange-retention",
"/cipp/custom-data",
"/cipp/super-admin",
- "/tenant/standards/list-standards",
+ "/tenant/standards",
"/tenant/manage",
"/tenant/administration/applications",
"/tenant/administration/tenants",
@@ -499,6 +499,46 @@ export const CippBreadcrumbNav = () => {
return result;
};
+ // Check if a path is valid and return its title from navigation or tabs
+ const getPathInfo = (path) => {
+ if (!path) return { isValid: false, title: null };
+
+ const normalizedPath = path.replace(/\/$/, "");
+
+ // Helper function to recursively search menu items
+ const findInMenu = (items) => {
+ for (const item of items) {
+ if (item.path) {
+ const normalizedItemPath = item.path.replace(/\/$/, "");
+ if (normalizedItemPath === normalizedPath) {
+ return { isValid: true, title: item.title };
+ }
+ }
+ if (item.items && item.items.length > 0) {
+ const found = findInMenu(item.items);
+ if (found.isValid) {
+ return found;
+ }
+ }
+ }
+ return { isValid: false, title: null };
+ };
+
+ // Check if path exists in navigation
+ const menuResult = findInMenu(nativeMenuItems);
+ if (menuResult.isValid) {
+ return menuResult;
+ }
+
+ // Check if path exists in tab options
+ const matchingTab = tabOptions.find((tab) => tab.path.replace(/\/$/, "") === normalizedPath);
+ if (matchingTab) {
+ return { isValid: true, title: matchingTab.title };
+ }
+
+ return { isValid: false, title: null };
+ };
+
// Handle click for hierarchical breadcrumbs
const handleHierarchicalClick = (path, query) => {
if (path) {
@@ -580,6 +620,9 @@ export const CippBreadcrumbNav = () => {
>
{breadcrumbs.map((crumb, index) => {
const isLast = index === breadcrumbs.length - 1;
+ const pathInfo = getPathInfo(crumb.path);
+ // Use title from nav/tabs if available, otherwise use the crumb's title
+ const displayTitle = pathInfo.title || crumb.title;
// Items without paths (headers/groups) - show as text
if (!crumb.path) {
@@ -590,31 +633,46 @@ export const CippBreadcrumbNav = () => {
variant="subtitle2"
sx={{ fontWeight: isLast ? 500 : 400 }}
>
- {crumb.title}
+ {displayTitle}
);
}
- // All items with paths are clickable, including the last one
- return (
- handleHierarchicalClick(crumb.path, crumb.query)}
- sx={{
- textDecoration: "none",
- color: isLast ? "text.primary" : "text.secondary",
- fontWeight: isLast ? 500 : 400,
- "&:hover": {
- textDecoration: "underline",
- color: "primary.main",
- },
- }}
- >
- {crumb.title}
-
- );
+ // Items with valid paths are clickable
+ // Items with invalid paths (fallback) are shown as plain text
+ if (pathInfo.isValid) {
+ return (
+ handleHierarchicalClick(crumb.path, crumb.query)}
+ sx={{
+ textDecoration: "none",
+ color: isLast ? "text.primary" : "text.secondary",
+ fontWeight: isLast ? 500 : 400,
+ "&:hover": {
+ textDecoration: "underline",
+ color: "primary.main",
+ },
+ }}
+ >
+ {displayTitle}
+
+ );
+ } else {
+ // Invalid path - show as text only
+ return (
+
+ {displayTitle}
+
+ );
+ }
})}
diff --git a/src/components/CippComponents/CippCADeployDrawer.jsx b/src/components/CippComponents/CippCADeployDrawer.jsx
index 508f86df92e7..6b2a4a633ff4 100644
--- a/src/components/CippComponents/CippCADeployDrawer.jsx
+++ b/src/components/CippComponents/CippCADeployDrawer.jsx
@@ -9,6 +9,7 @@ import CippJsonView from "../CippFormPages/CippJSONView";
import { CippApiResults } from "./CippApiResults";
import { useSettings } from "../../hooks/use-settings";
import { CippFormTenantSelector } from "./CippFormTenantSelector";
+import { CippFormCondition } from "./CippFormCondition";
export const CippCADeployDrawer = ({
buttonText = "Deploy CA Policy",
@@ -24,6 +25,10 @@ export const CippCADeployDrawer = ({
const CATemplates = ApiGetCall({ url: "/api/ListCATemplates", queryKey: "CATemplates" });
const [JSONData, setJSONData] = useState();
const watcher = useWatch({ control: formControl.control, name: "TemplateList" });
+ const selectedReplaceMode = useWatch({
+ control: formControl.control,
+ name: "replacename",
+ });
// Use external open state if provided, otherwise use internal state
const drawerVisible = open !== null ? open : internalDrawerVisible;
@@ -199,13 +204,25 @@ export const CippCADeployDrawer = ({
label="Disable Security Defaults if enabled when creating policy"
formControl={formControl}
/>
-
-
+ field="replacename"
+ compareType="is"
+ compareValue="displayName"
+ action="disable"
+ >
+
+
>
diff --git a/src/components/CippComponents/CippCentralSearch.jsx b/src/components/CippComponents/CippCentralSearch.jsx
index 523615e7c659..0c7bf858e88b 100644
--- a/src/components/CippComponents/CippCentralSearch.jsx
+++ b/src/components/CippComponents/CippCentralSearch.jsx
@@ -46,7 +46,7 @@ async function loadTabOptions() {
"/email/administration/exchange-retention",
"/cipp/custom-data",
"/cipp/super-admin",
- "/tenant/standards/list-standards",
+ "/tenant/standards",
"/tenant/manage",
"/tenant/administration/applications",
"/tenant/administration/tenants",
diff --git a/src/components/CippComponents/CippFormCondition.jsx b/src/components/CippComponents/CippFormCondition.jsx
index 9ec49a57ef5c..dd9a48cbf95d 100644
--- a/src/components/CippComponents/CippFormCondition.jsx
+++ b/src/components/CippComponents/CippFormCondition.jsx
@@ -18,7 +18,7 @@ export const CippFormCondition = (props) => {
if (
field === undefined ||
- compareValue === undefined ||
+ (compareValue === undefined && compareType !== "hasValue") ||
children === undefined ||
formControl === undefined
) {
@@ -148,10 +148,18 @@ export const CippFormCondition = (props) => {
watcher.length >= compareValue
);
case "hasValue":
- return (
- (watcher !== undefined && watcher !== null && watcher !== "") ||
- (watcher?.value !== undefined && watcher?.value !== null && watcher?.value !== "")
- );
+ // Check watchedValue (the extracted value based on propertyName)
+ // For simple values (strings, numbers)
+ if (watchedValue === undefined || watchedValue === null || watchedValue === "") {
+ return false;
+ }
+ // If it's an array, check if it has elements
+ if (Array.isArray(watchedValue)) {
+ return watchedValue.length > 0;
+ }
+ console.log("watched value:", watchedValue);
+ // For any other truthy value (objects, numbers, strings), consider it as having a value
+ return true;
case "labelEq":
return Array.isArray(watcher) && watcher.some((item) => item?.label === compareValue);
case "labelContains":
diff --git a/src/components/CippComponents/CippRestoreBackupDrawer.jsx b/src/components/CippComponents/CippRestoreBackupDrawer.jsx
index f011b499820f..b8782aa6d401 100644
--- a/src/components/CippComponents/CippRestoreBackupDrawer.jsx
+++ b/src/components/CippComponents/CippRestoreBackupDrawer.jsx
@@ -13,6 +13,7 @@ import { ApiPostCall } from "../../api/ApiCall";
export const CippRestoreBackupDrawer = ({
buttonText = "Restore Backup",
backupName = null,
+ backupData = null,
requiredPermissions = [],
PermissionButton = Button,
...props
@@ -85,7 +86,12 @@ export const CippRestoreBackupDrawer = ({
const values = formControl.getValues();
const startDate = new Date();
const unixTime = Math.floor(startDate.getTime() / 1000) - 45;
- const tenantFilterValue = tenantFilter;
+
+ // If in AllTenants context, use the tenant from the backup data
+ let tenantFilterValue = tenantFilter;
+ if (tenantFilter === "AllTenants" && backupData?.tenantSource) {
+ tenantFilterValue = backupData.tenantSource;
+ }
const shippedValues = {
TenantFilter: tenantFilterValue,
@@ -202,7 +208,12 @@ export const CippRestoreBackupDrawer = ({
queryKey: `BackupList-${tenantFilter}-autocomplete`,
labelField: (option) => {
const match = option.BackupName.match(/.*_(\d{4}-\d{2}-\d{2})-(\d{2})(\d{2})/);
- return match ? `${match[1]} @ ${match[2]}:${match[3]}` : option.BackupName;
+ const dateTime = match
+ ? `${match[1]} @ ${match[2]}:${match[3]}`
+ : option.BackupName;
+ const tenantDisplay =
+ tenantFilter === "AllTenants" ? ` (${option.TenantFilter})` : "";
+ return `${dateTime}${tenantDisplay}`;
},
valueField: "BackupName",
data: {
diff --git a/src/components/CippStandards/CippStandardsSideBar.jsx b/src/components/CippStandards/CippStandardsSideBar.jsx
index 53a6d571b8a8..7e2ccdcb6647 100644
--- a/src/components/CippStandards/CippStandardsSideBar.jsx
+++ b/src/components/CippStandards/CippStandardsSideBar.jsx
@@ -144,34 +144,12 @@ const CippStandardsSideBar = ({
}
// Filter out current template if editing
- console.log("Duplicate detection debug:", {
- edit,
- currentGUID: watchForm.GUID,
- allTemplates: driftValidationApi.data?.map((t) => ({
- GUID: t.GUID,
- standardId: t.standardId,
- standardName: t.standardName,
- })),
- });
-
const existingTemplates = driftValidationApi.data.filter((template) => {
const shouldInclude =
edit && watchForm.GUID ? template.standardId !== watchForm.GUID : true;
- console.log(
- `Template ${template.standardId} (${template.standardName}): shouldInclude=${shouldInclude}, currentGUID=${watchForm.GUID}`
- );
return shouldInclude;
});
- console.log(
- "Filtered templates:",
- existingTemplates?.map((t) => ({
- GUID: t.GUID,
- standardId: t.standardId,
- standardName: t.standardName,
- }))
- );
-
// Get tenant groups data
const groups = tenantGroupsApi.data?.Results || [];
@@ -198,45 +176,27 @@ const CippStandardsSideBar = ({
});
// Check for conflicts with unique templates
- console.log("Checking conflicts with unique templates:", uniqueTemplates);
- console.log("Selected tenant list:", selectedTenantList);
-
for (const templateId in uniqueTemplates) {
const template = uniqueTemplates[templateId];
const templateTenants = template.tenants;
- console.log(
- `Checking template ${templateId} (${template.standardName}) with tenants:`,
- templateTenants
- );
-
const hasConflict = selectedTenantList.some((selectedTenant) => {
// Check if any template tenant matches the selected tenant
const conflict = templateTenants.some((templateTenant) => {
if (selectedTenant === "AllTenants" || templateTenant === "AllTenants") {
- console.log(
- `Conflict found: ${selectedTenant} vs ${templateTenant} (AllTenants match)`
- );
return true;
}
const match = selectedTenant === templateTenant;
- if (match) {
- console.log(`Conflict found: ${selectedTenant} vs ${templateTenant} (exact match)`);
- }
return match;
});
return conflict;
});
- console.log(`Template ${templateId} has conflict: ${hasConflict}`);
-
if (hasConflict) {
conflicts.push(template.standardName || "Unknown Template");
}
}
- console.log("Final conflicts:", conflicts);
-
if (conflicts.length > 0) {
setDriftError(
`This template has tenants that are assigned to another Drift Template. You can only assign one Drift Template to each tenant. Please check the ${conflicts.join(
diff --git a/src/data/standards.json b/src/data/standards.json
index 2d6ffb3f010a..92d605cf2036 100644
--- a/src/data/standards.json
+++ b/src/data/standards.json
@@ -5056,10 +5056,7 @@
"queryKey": "ListIntuneTemplates-tag-autcomplete",
"url": "/api/ListIntuneTemplates?mode=Tag",
"labelField": "label",
- "valueField": "value",
- "addedField": {
- "templates": "templates"
- }
+ "valueField": "value"
}
},
{
diff --git a/src/hooks/use-securescore.js b/src/hooks/use-securescore.js
index f96c2bd232b7..a51dc18d9138 100644
--- a/src/hooks/use-securescore.js
+++ b/src/hooks/use-securescore.js
@@ -68,7 +68,7 @@ export function useSecureScore({ waiting = true } = {}) {
complianceInformation: translation?.complianceInformation,
actionUrl: remediation
? //this needs to be updated to be a direct url to apply this standard.
- "/tenant/standards/list-standards"
+ "/tenant/standards"
: translation?.actionUrl,
remediation: remediation
? `1. Enable the CIPP Standard: ${remediation.label}`
diff --git a/src/layouts/HeaderedTabbedLayout.jsx b/src/layouts/HeaderedTabbedLayout.jsx
index 04a756494d5c..ece1d0659924 100644
--- a/src/layouts/HeaderedTabbedLayout.jsx
+++ b/src/layouts/HeaderedTabbedLayout.jsx
@@ -115,7 +115,7 @@ export const HeaderedTabbedLayout = (props) => {
!mdDown && {
flexGrow: 1,
overflow: "auto",
- height: "calc(100vh - 30px)",
+ height: "calc(100vh - 350px)",
}
}
>
diff --git a/src/pages/tenant/manage/applied-standards.js b/src/pages/tenant/manage/applied-standards.js
index 2647ac4651f4..71e4865d8771 100644
--- a/src/pages/tenant/manage/applied-standards.js
+++ b/src/pages/tenant/manage/applied-standards.js
@@ -1027,7 +1027,7 @@ const Page = () => {
tabOptions={tabOptions}
title={title}
subtitle={subtitle}
- backUrl="/tenant/standards/list-standards"
+ backUrl="/tenant/standards"
actions={actions}
actionsData={{}}
isFetching={comparisonApi.isFetching || templateDetails.isFetching}
diff --git a/src/pages/tenant/manage/configuration-backup.js b/src/pages/tenant/manage/configuration-backup.js
index 5421a3314e79..d2f8e5e19ac4 100644
--- a/src/pages/tenant/manage/configuration-backup.js
+++ b/src/pages/tenant/manage/configuration-backup.js
@@ -1,5 +1,7 @@
import { Layout as DashboardLayout } from "/src/layouts/index.js";
import { HeaderedTabbedLayout } from "/src/layouts/HeaderedTabbedLayout";
+import { useState } from "react";
+import { useForm, useWatch } from "react-hook-form";
import {
Button,
Box,
@@ -31,6 +33,7 @@ import { CippBackupScheduleDrawer } from "/src/components/CippComponents/CippBac
import { CippRestoreBackupDrawer } from "/src/components/CippComponents/CippRestoreBackupDrawer";
import { CippApiDialog } from "/src/components/CippComponents/CippApiDialog";
import { CippTimeAgo } from "/src/components/CippComponents/CippTimeAgo";
+import { CippFormTenantSelector } from "/src/components/CippComponents/CippFormTenantSelector";
import { useDialog } from "/src/hooks/use-dialog";
import ReactTimeAgo from "react-time-ago";
import tabOptions from "./tabOptions.json";
@@ -42,6 +45,8 @@ const Page = () => {
const { templateId } = router.query;
const settings = useSettings();
const removeDialog = useDialog();
+ const tenantFilterForm = useForm({ defaultValues: { tenantFilter: null } });
+ const backupTenantFilter = useWatch({ control: tenantFilterForm.control, name: "tenantFilter" });
// Prioritize URL query parameter, then fall back to settings
const currentTenant = router.query.tenantFilter || settings.currentTenant;
@@ -79,19 +84,28 @@ const Page = () => {
return ["Configuration"];
};
- const backupDisplayItems = filteredBackupData.map((backup, index) => ({
+ // Filter backup data by selected tenant if in AllTenants view
+ const tenantFilteredBackupData =
+ settings.currentTenant === "AllTenants" &&
+ backupTenantFilter &&
+ backupTenantFilter !== "AllTenants"
+ ? filteredBackupData.filter((backup) => backup.TenantFilter === backupTenantFilter)
+ : filteredBackupData;
+
+ const backupDisplayItems = tenantFilteredBackupData.map((backup, index) => ({
id: backup.RowKey || index,
name: backup.BackupName || "Unnamed Backup",
timestamp: backup.Timestamp,
- tenantSource: backup.BackupName?.includes("AllTenants")
- ? "All Tenants"
- : backup.BackupName?.replace("CIPP Backup - ", "") || settings.currentTenant,
+ tenantSource: backup.TenantFilter || settings.currentTenant,
tags: generateBackupTags(backup),
}));
// Process existing backup configuration, find tenantFilter. by comparing settings.currentTenant with Tenant.value
const currentConfig = Array.isArray(existingBackupConfig.data)
- ? existingBackupConfig.data.find((tenant) => tenant.Tenant.value === settings.currentTenant)
+ ? existingBackupConfig.data.find(
+ (tenant) =>
+ tenant.Tenant.value === settings.currentTenant || tenant.Tenant.value === "AllTenants"
+ )
: null;
const hasExistingConfig = currentConfig && currentConfig.Parameters?.ScheduledBackupValues;
@@ -281,13 +295,33 @@ const Page = () => {
{/* Backup History */}
-
-
-
-
-
- Backup History
-
+
+
+
+
+
+
+ Backup History
+
+ {settings.currentTenant === "AllTenants" && (
+
+
+
+ )}
+
{settings.currentTenant === "AllTenants"
@@ -307,7 +341,7 @@ const Page = () => {
) : (
-
+
{backupDisplayItems.map((backup) => (
@@ -334,6 +368,14 @@ const Page = () => {
+ {settings.currentTenant === "AllTenants" && (
+
+ )}
{
subtitle={subtitle}
actions={actions}
actionsData={{}}
- backUrl="/tenant/standards/list-standards"
+ backUrl="/tenant/standards"
>
diff --git a/src/pages/tenant/standards/list-standards/index.js b/src/pages/tenant/standards/list-standards/index.js
deleted file mode 100644
index 5e25f0b96d76..000000000000
--- a/src/pages/tenant/standards/list-standards/index.js
+++ /dev/null
@@ -1,68 +0,0 @@
-import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx";
-import { Layout as DashboardLayout } from "/src/layouts/index.js";
-import { TabbedLayout } from "/src/layouts/TabbedLayout";
-import { Delete, Add } from "@mui/icons-material";
-import { EyeIcon } from "@heroicons/react/24/outline";
-import tabOptions from "./tabOptions.json";
-
-const Page = () => {
- const pageTitle = "Standard & Drift Alignment";
-
- const actions = [
- {
- label: "View Tenant Report",
- link: "/tenant/manage/applied-standards/?tenantFilter=[tenantFilter]&templateId=[standardId]",
- icon: ,
- color: "info",
- target: "_self",
- },
- {
- label: "Manage Drift",
- link: "/tenant/manage/drift?templateId=[standardId]&tenantFilter=[tenantFilter]",
- icon: ,
- color: "info",
- target: "_self",
- condition: (row) => row.standardType === "drift",
- },
- {
- label: "Remove Drift Customization",
- type: "POST",
- url: "/api/ExecUpdateDriftDeviation",
- icon: ,
- data: {
- RemoveDriftCustomization: "true",
- tenantFilter: "tenantFilter",
- },
- confirmText:
- "Are you sure you want to remove all drift customizations? This resets the Drift Standard to the default template, and will generate alerts for the drifted items.",
- multiPost: false,
- condition: (row) => row.standardType === "drift",
- },
- ];
-
- return (
-
- );
-};
-
-Page.getLayout = (page) => (
-
- {page}
-
-);
-
-export default Page;