+ ), []);
+
+ return (
+
+
+
+ );
+};
+
+Page.getLayout = (page) =>
{page};
+
+export default Page;
\ No newline at end of file
diff --git a/src/pages/email/administration/exchange-retention/tabOptions.json b/src/pages/email/administration/exchange-retention/tabOptions.json
new file mode 100644
index 000000000000..e6e203b5c611
--- /dev/null
+++ b/src/pages/email/administration/exchange-retention/tabOptions.json
@@ -0,0 +1,10 @@
+[
+ {
+ "label": "Policies",
+ "path": "/email/administration/exchange-retention/policies"
+ },
+ {
+ "label": "Tags",
+ "path": "/email/administration/exchange-retention/tags"
+ }
+]
diff --git a/src/pages/email/administration/exchange-retention/tags/add.jsx b/src/pages/email/administration/exchange-retention/tags/add.jsx
new file mode 100644
index 000000000000..97fb1218aafa
--- /dev/null
+++ b/src/pages/email/administration/exchange-retention/tags/add.jsx
@@ -0,0 +1,276 @@
+import { useForm } from "react-hook-form";
+import { useEffect } from "react";
+import { useRouter } from "next/router";
+import { Layout as DashboardLayout } from "/src/layouts/index.js";
+import CippFormPage from "/src/components/CippFormPages/CippFormPage";
+import CippFormSkeleton from "/src/components/CippFormPages/CippFormSkeleton";
+import { useSettings } from "/src/hooks/use-settings";
+import { Grid } from "@mui/system";
+import { Divider } from "@mui/material";
+import CippFormComponent from "/src/components/CippComponents/CippFormComponent";
+import { ApiGetCall } from "/src/api/ApiCall";
+
+const AddRetentionTag = () => {
+ const userSettingsDefaults = useSettings();
+ const router = useRouter();
+ const { name } = router.query;
+ const isEdit = !!name;
+
+ const formControl = useForm({
+ mode: "onChange",
+ defaultValues: {
+ tenantFilter: userSettingsDefaults.currentTenant,
+ Name: "",
+ Type: "",
+ Comment: "",
+ RetentionAction: "",
+ AgeLimitForRetention: "",
+ RetentionEnabled: true,
+ LocalizedComment: "",
+ LocalizedRetentionPolicyTagName: "",
+ },
+ });
+
+ // Get existing tag data if editing
+ const existingTagRequest = ApiGetCall({
+ url: `/api/ExecManageRetentionTags?tenantFilter=${userSettingsDefaults.currentTenant}${isEdit ? `&name=${encodeURIComponent(name)}` : ''}`,
+ queryKey: `RetentionTag-${name}-${userSettingsDefaults.currentTenant}`,
+ waiting: isEdit,
+ });
+
+ const tagTypes = [
+ { label: 'All', value: 'All' },
+ { label: 'Inbox', value: 'Inbox' },
+ { label: 'Sent Items', value: 'SentItems' },
+ { label: 'Deleted Items', value: 'DeletedItems' },
+ { label: 'Drafts', value: 'Drafts' },
+ { label: 'Outbox', value: 'Outbox' },
+ { label: 'Junk Email', value: 'JunkEmail' },
+ { label: 'Journal', value: 'Journal' },
+ { label: 'Sync Issues', value: 'SyncIssues' },
+ { label: 'Conversation History', value: 'ConversationHistory' },
+ { label: 'Personal', value: 'Personal' },
+ { label: 'Recoverable Items', value: 'RecoverableItems' },
+ { label: 'Non IPM Root', value: 'NonIpmRoot' },
+ { label: 'Legacy Archive Journals', value: 'LegacyArchiveJournals' },
+ { label: 'Clutter', value: 'Clutter' },
+ { label: 'Calendar', value: 'Calendar' },
+ { label: 'Notes', value: 'Notes' },
+ { label: 'Tasks', value: 'Tasks' },
+ { label: 'Contacts', value: 'Contacts' },
+ { label: 'RSS Subscriptions', value: 'RssSubscriptions' },
+ { label: 'Managed Custom Folder', value: 'ManagedCustomFolder' }
+ ];
+
+ const retentionActions = [
+ { label: 'Delete and Allow Recovery', value: 'DeleteAndAllowRecovery' },
+ { label: 'Permanently Delete', value: 'PermanentlyDelete' },
+ { label: 'Move to Archive', value: 'MoveToArchive' },
+ { label: 'Mark as Past Retention Limit', value: 'MarkAsPastRetentionLimit' }
+ ];
+
+ // Parse AgeLimitForRetention from TimeSpan format "90.00:00:00" to just days "90"
+ const parseAgeLimitDays = (ageLimit) => {
+ if (!ageLimit) return "";
+ const match = ageLimit.toString().match(/^(\d+)\./);
+ return match ? match[1] : "";
+ };
+
+ // Pre-fill form when editing
+ useEffect(() => {
+ if (isEdit && existingTagRequest.isSuccess && existingTagRequest.data) {
+ const tag = existingTagRequest.data;
+
+ // Find the matching options for dropdowns
+ const typeOption = tagTypes.find(option => option.value === tag.Type) || null;
+ const actionOption = retentionActions.find(option => option.value === tag.RetentionAction) || null;
+
+ // Handle localized fields (arrays in API, strings in form)
+ const localizedComment = Array.isArray(tag.LocalizedComment)
+ ? tag.LocalizedComment[0] || ""
+ : tag.LocalizedComment || "";
+ const localizedTagName = Array.isArray(tag.LocalizedRetentionPolicyTagName)
+ ? tag.LocalizedRetentionPolicyTagName[0] || ""
+ : tag.LocalizedRetentionPolicyTagName || "";
+
+ formControl.reset({
+ tenantFilter: userSettingsDefaults.currentTenant,
+ Name: tag.Name || "",
+ Type: typeOption,
+ Comment: tag.Comment || "",
+ RetentionAction: actionOption,
+ AgeLimitForRetention: parseAgeLimitDays(tag.AgeLimitForRetention),
+ RetentionEnabled: tag.RetentionEnabled !== false,
+ LocalizedComment: localizedComment,
+ LocalizedRetentionPolicyTagName: localizedTagName,
+ });
+ }
+ }, [isEdit, existingTagRequest.isSuccess, existingTagRequest.data, userSettingsDefaults.currentTenant, formControl]);
+
+ return (
+
{
+ const tagData = {
+ Name: values.Name,
+ Comment: values.Comment,
+ RetentionEnabled: values.RetentionEnabled,
+ };
+
+ // Extract .value from select objects and only include non-empty optional fields
+ if (values.RetentionAction) {
+ tagData.RetentionAction = typeof values.RetentionAction === 'string'
+ ? values.RetentionAction
+ : values.RetentionAction.value;
+ }
+ if (values.AgeLimitForRetention) {
+ tagData.AgeLimitForRetention = parseInt(values.AgeLimitForRetention);
+ }
+ if (values.LocalizedComment) {
+ tagData.LocalizedComment = values.LocalizedComment;
+ }
+ if (values.LocalizedRetentionPolicyTagName) {
+ tagData.LocalizedRetentionPolicyTagName = values.LocalizedRetentionPolicyTagName;
+ }
+
+ if (isEdit) {
+ return {
+ ModifyTags: [{
+ Identity: name,
+ ...tagData,
+ }],
+ tenantFilter: values.tenantFilter,
+ };
+ } else {
+ return {
+ CreateTags: [{
+ Type: typeof values.Type === 'string' ? values.Type : values.Type.value,
+ ...tagData,
+ }],
+ tenantFilter: values.tenantFilter,
+ };
+ }
+ }}
+ >
+ {existingTagRequest.isLoading && isEdit && }
+ {(!isEdit || !existingTagRequest.isLoading) && (
+
+ {/* Tag Name */}
+
+
+
+
+ {/* Tag Type */}
+
+
+
+
+
+
+ {/* Retention Action */}
+
+
+
+
+ {/* Age Limit */}
+
+
+
+
+ {/* Retention Enabled */}
+
+
+
+
+
+
+ {/* Comment */}
+
+
+
+
+ {/* Localized Fields */}
+
+
+
+
+
+
+
+
+
+
+
+ )}
+
+ );
+};
+
+AddRetentionTag.getLayout = (page) =>
{page};
+
+export default AddRetentionTag;
diff --git a/src/pages/email/administration/exchange-retention/tags/edit.jsx b/src/pages/email/administration/exchange-retention/tags/edit.jsx
new file mode 100644
index 000000000000..7df96e2df900
--- /dev/null
+++ b/src/pages/email/administration/exchange-retention/tags/edit.jsx
@@ -0,0 +1 @@
+export { default } from './add';
\ No newline at end of file
diff --git a/src/pages/email/administration/exchange-retention/tags/index.js b/src/pages/email/administration/exchange-retention/tags/index.js
new file mode 100644
index 000000000000..105a31c56e77
--- /dev/null
+++ b/src/pages/email/administration/exchange-retention/tags/index.js
@@ -0,0 +1,80 @@
+import { useMemo } from "react";
+import { Layout as DashboardLayout } from "/src/layouts/index";
+import { CippTablePage } from "/src/components/CippComponents/CippTablePage";
+import { Sell, Edit } from "@mui/icons-material";
+import { Button } from "@mui/material";
+import Link from "next/link";
+import TrashIcon from "@heroicons/react/24/outline/TrashIcon";
+import { HeaderedTabbedLayout } from "/src/layouts/HeaderedTabbedLayout";
+import tabOptions from "../tabOptions";
+import { useSettings } from "/src/hooks/use-settings";
+
+const Page = () => {
+ const pageTitle = "Retention Tag Management";
+ const tenant = useSettings().currentTenant;
+
+ const actions = useMemo(() => [
+ {
+ label: "Edit Tag",
+ link: "/email/administration/exchange-retention/tags/edit?name=[Name]",
+ multiPost: false,
+ postEntireRow: true,
+ icon:
,
+ color: "warning",
+ },
+ {
+ label: "Delete Tag",
+ type: "POST",
+ url: "/api/ExecManageRetentionTags",
+ confirmText: "Are you sure you want to delete retention tag [Name]? This action cannot be undone and may affect retention policies that use this tag.",
+ color: "danger",
+ icon:
,
+ customDataformatter: (rows) => {
+ const tags = Array.isArray(rows) ? rows : [rows];
+ return {
+ DeleteTags: tags.map(tag => tag.Name),
+ tenantFilter: tenant,
+ };
+ },
+ },
+ ], [tenant]);
+
+ const simpleColumns = useMemo(() => [
+ "Name",
+ "Type",
+ "RetentionAction",
+ "AgeLimitForRetention",
+ "RetentionEnabled",
+ "Comment"
+ ], []);
+
+ const cardButton = useMemo(() => (
+
}
+ >
+ Add Retention Tag
+
+ ), []);
+
+ return (
+
+
+
+ );
+};
+
+Page.getLayout = (page) =>
{page};
+
+export default Page;
\ No newline at end of file
From 9f3819eb9fefdd5a12168bcf5cbbbc1fd1c1d0b6 Mon Sep 17 00:00:00 2001
From: Zacgoose <107489668+Zacgoose@users.noreply.github.com>
Date: Tue, 12 Aug 2025 17:37:23 +0800
Subject: [PATCH 02/19] Update HeaderedTabbedLayout.jsx
Signed-off-by: Zacgoose <107489668+Zacgoose@users.noreply.github.com>
---
src/layouts/HeaderedTabbedLayout.jsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/layouts/HeaderedTabbedLayout.jsx b/src/layouts/HeaderedTabbedLayout.jsx
index 2b9965280666..50b1b2e1e034 100644
--- a/src/layouts/HeaderedTabbedLayout.jsx
+++ b/src/layouts/HeaderedTabbedLayout.jsx
@@ -60,7 +60,7 @@ export const HeaderedTabbedLayout = (props) => {
>
-
+
Date: Tue, 12 Aug 2025 18:41:04 +0800
Subject: [PATCH 03/19] corrected files layout
---
.../administration/exchange-retention/policies/edit.jsx | 1 -
.../administration/exchange-retention/policies/index.js | 4 ++--
.../exchange-retention/policies/{add.jsx => policy.jsx} | 6 +++---
.../email/administration/exchange-retention/tags/edit.jsx | 1 -
.../email/administration/exchange-retention/tags/index.js | 4 ++--
.../exchange-retention/tags/{add.jsx => tag.jsx} | 6 +++---
6 files changed, 10 insertions(+), 12 deletions(-)
delete mode 100644 src/pages/email/administration/exchange-retention/policies/edit.jsx
rename src/pages/email/administration/exchange-retention/policies/{add.jsx => policy.jsx} (96%)
delete mode 100644 src/pages/email/administration/exchange-retention/tags/edit.jsx
rename src/pages/email/administration/exchange-retention/tags/{add.jsx => tag.jsx} (98%)
diff --git a/src/pages/email/administration/exchange-retention/policies/edit.jsx b/src/pages/email/administration/exchange-retention/policies/edit.jsx
deleted file mode 100644
index 7df96e2df900..000000000000
--- a/src/pages/email/administration/exchange-retention/policies/edit.jsx
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from './add';
\ No newline at end of file
diff --git a/src/pages/email/administration/exchange-retention/policies/index.js b/src/pages/email/administration/exchange-retention/policies/index.js
index 31442c54c72d..c465475879cf 100644
--- a/src/pages/email/administration/exchange-retention/policies/index.js
+++ b/src/pages/email/administration/exchange-retention/policies/index.js
@@ -16,7 +16,7 @@ const Page = () => {
const actions = useMemo(() => [
{
label: "Edit Policy",
- link: "/email/administration/exchange-retention/policies/edit?name=[Name]",
+ link: "/email/administration/exchange-retention/policies/policy?name=[Name]",
multiPost: false,
postEntireRow: true,
icon: ,
@@ -49,7 +49,7 @@ const Page = () => {
const cardButton = useMemo(() => (
}
>
Add Retention Policy
diff --git a/src/pages/email/administration/exchange-retention/policies/add.jsx b/src/pages/email/administration/exchange-retention/policies/policy.jsx
similarity index 96%
rename from src/pages/email/administration/exchange-retention/policies/add.jsx
rename to src/pages/email/administration/exchange-retention/policies/policy.jsx
index 00f03d3e2666..7abef58d6a57 100644
--- a/src/pages/email/administration/exchange-retention/policies/add.jsx
+++ b/src/pages/email/administration/exchange-retention/policies/policy.jsx
@@ -10,7 +10,7 @@ import { Divider } from "@mui/material";
import CippFormComponent from "/src/components/CippComponents/CippFormComponent";
import { ApiGetCall } from "/src/api/ApiCall";
-const AddRetentionPolicy = () => {
+const RetentionPolicy = () => {
const userSettingsDefaults = useSettings();
const router = useRouter();
const { name } = router.query;
@@ -146,6 +146,6 @@ const AddRetentionPolicy = () => {
);
};
-AddRetentionPolicy.getLayout = (page) => {page};
+RetentionPolicy.getLayout = (page) => {page};
-export default AddRetentionPolicy;
+export default RetentionPolicy;
\ No newline at end of file
diff --git a/src/pages/email/administration/exchange-retention/tags/edit.jsx b/src/pages/email/administration/exchange-retention/tags/edit.jsx
deleted file mode 100644
index 7df96e2df900..000000000000
--- a/src/pages/email/administration/exchange-retention/tags/edit.jsx
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from './add';
\ No newline at end of file
diff --git a/src/pages/email/administration/exchange-retention/tags/index.js b/src/pages/email/administration/exchange-retention/tags/index.js
index 105a31c56e77..e8299b401eca 100644
--- a/src/pages/email/administration/exchange-retention/tags/index.js
+++ b/src/pages/email/administration/exchange-retention/tags/index.js
@@ -16,7 +16,7 @@ const Page = () => {
const actions = useMemo(() => [
{
label: "Edit Tag",
- link: "/email/administration/exchange-retention/tags/edit?name=[Name]",
+ link: "/email/administration/exchange-retention/tags/tag?name=[Name]",
multiPost: false,
postEntireRow: true,
icon: ,
@@ -51,7 +51,7 @@ const Page = () => {
const cardButton = useMemo(() => (
}
>
Add Retention Tag
diff --git a/src/pages/email/administration/exchange-retention/tags/add.jsx b/src/pages/email/administration/exchange-retention/tags/tag.jsx
similarity index 98%
rename from src/pages/email/administration/exchange-retention/tags/add.jsx
rename to src/pages/email/administration/exchange-retention/tags/tag.jsx
index 97fb1218aafa..81fb60b87505 100644
--- a/src/pages/email/administration/exchange-retention/tags/add.jsx
+++ b/src/pages/email/administration/exchange-retention/tags/tag.jsx
@@ -10,7 +10,7 @@ import { Divider } from "@mui/material";
import CippFormComponent from "/src/components/CippComponents/CippFormComponent";
import { ApiGetCall } from "/src/api/ApiCall";
-const AddRetentionTag = () => {
+const RetentionTag = () => {
const userSettingsDefaults = useSettings();
const router = useRouter();
const { name } = router.query;
@@ -271,6 +271,6 @@ const AddRetentionTag = () => {
);
};
-AddRetentionTag.getLayout = (page) => {page};
+RetentionTag.getLayout = (page) => {page};
-export default AddRetentionTag;
+export default RetentionTag;
\ No newline at end of file
From a914b8acc976129c7c29995f150d4841295af611 Mon Sep 17 00:00:00 2001
From: John Duprey
Date: Mon, 18 Aug 2025 17:42:51 -0400
Subject: [PATCH 04/19] fix rendering issues with standards dialog
---
.../CippCards/CippStandardsDialog.jsx | 464 +++++++++++++-----
1 file changed, 341 insertions(+), 123 deletions(-)
diff --git a/src/components/CippCards/CippStandardsDialog.jsx b/src/components/CippCards/CippStandardsDialog.jsx
index 07e362fe955c..5e3c2cc879d8 100644
--- a/src/components/CippCards/CippStandardsDialog.jsx
+++ b/src/components/CippCards/CippStandardsDialog.jsx
@@ -50,6 +50,8 @@ const getCategoryIcon = (category) => {
return ;
case "Intune Standards":
return ;
+ case "Templates":
+ return ;
default:
return ;
}
@@ -101,19 +103,38 @@ export const CippStandardsDialog = ({ open, onClose, standardsData, currentTenan
(excludedTenantsArr.length === 0 ||
!excludedTenantsArr.some((et) => et.value === currentTenant));
- return tenantInFilter || allTenantsTemplate;
+ const isApplicable = tenantInFilter || allTenantsTemplate;
+
+ return isApplicable;
});
// Combine standards from all applicable templates
const combinedStandards = {};
for (const template of applicableTemplates) {
for (const [standardKey, standardValue] of Object.entries(template.standards)) {
- combinedStandards[standardKey] = standardValue;
+ if (combinedStandards[standardKey]) {
+ // If the standard already exists, we need to merge it
+ const existing = combinedStandards[standardKey];
+ const incoming = standardValue;
+
+ // If both are arrays (like IntuneTemplate, ConditionalAccessTemplate), concatenate them
+ if (Array.isArray(existing) && Array.isArray(incoming)) {
+ combinedStandards[standardKey] = [...existing, ...incoming];
+ }
+ // If one is array and other is not, or both are objects, keep the last one (existing behavior)
+ else {
+ combinedStandards[standardKey] = standardValue;
+ }
+ } else {
+ combinedStandards[standardKey] = standardValue;
+ }
}
}
// Group standards by category
const standardsByCategory = {};
+ let totalStandardsCount = 0;
+
Object.entries(combinedStandards).forEach(([standardKey, standardConfig]) => {
const standardInfo = standards.find((s) => s.name === `standards.${standardKey}`);
if (standardInfo) {
@@ -126,6 +147,13 @@ export const CippStandardsDialog = ({ open, onClose, standardsData, currentTenan
config: standardConfig,
info: standardInfo,
});
+
+ // Count template instances separately
+ if (Array.isArray(standardConfig) && standardConfig.length > 0) {
+ totalStandardsCount += standardConfig.length;
+ } else {
+ totalStandardsCount += 1;
+ }
}
});
@@ -167,143 +195,333 @@ export const CippStandardsDialog = ({ open, onClose, standardsData, currentTenan
Total templates applied: {applicableTemplates.length} | Total
- standards: {Object.keys(combinedStandards).length}
+ standards: {totalStandardsCount}
- {Object.entries(standardsByCategory).map(([category, categoryStandards], idx) => (
- `1px solid ${theme.palette.divider}`,
- "&:before": { display: "none" },
- }}
- >
- }
- aria-controls={`${category}-content`}
- id={`${category}-header`}
+ {Object.entries(standardsByCategory).map(([category, categoryStandards], idx) => {
+ // Calculate the actual count of standards in this category (counting template instances)
+ const categoryCount = categoryStandards.reduce((count, { config }) => {
+ if (Array.isArray(config) && config.length > 0) {
+ return count + config.length;
+ }
+ return count + 1;
+ }, 0);
+
+ return (
+ `1px solid ${theme.palette.divider}`,
+ "&:before": { display: "none" },
}}
>
-
- {getCategoryIcon(category)}
-
- {category}
-
-
-
-
-
-
- {categoryStandards.map(({ key, config, info }) => (
-
-
-
-
-
-
- {info.label}
-
-
- {info.helpText}
-
-
-
-
- {info.tag && info.tag.length > 0 && (
-
- )}
-
-
-
- Actions:
-
-
- {config.action && Array.isArray(config.action) ? (
- config.action.map((action, index) => (
+ }
+ aria-controls={`${category}-content`}
+ id={`${category}-header`}
+ sx={{
+ minHeight: 48,
+ "& .MuiAccordionSummary-content": { alignItems: "center", m: 0 },
+ }}
+ >
+
+ {getCategoryIcon(category)}
+
+ {category}
+
+
+
+
+
+
+ {categoryStandards.map(({ key, config, info }) => {
+ // Handle template arrays by rendering each template as a separate card
+ if (Array.isArray(config) && config.length > 0) {
+ return config.map((templateItem, templateIndex) => (
+
+
+
+
+
+
+ {info.label} {config.length > 1 && `(${templateIndex + 1})`}
+
+
+ {info.helpText}
+
+
+
- ))
- ) : (
-
- No actions configured
-
- )}
-
-
-
- {info.addedComponent && info.addedComponent.length > 0 && (
-
-
- Fields:
-
-
- {info.addedComponent.map((component, index) => {
- const componentValue = _.get(config, component.name);
- const displayValue =
- componentValue?.label || componentValue || "N/A";
- return (
-
+ {info.tag && info.tag.length > 0 && (
+
+ )}
+
+
+
+ Actions:
+
+
+ {templateItem.action && Array.isArray(templateItem.action) ? (
+ templateItem.action.map((action, actionIndex) => (
+
+ ))
+ ) : (
- {component.label || component.name}:
+ No actions configured
+ )}
+
+
+
+ {info.addedComponent && info.addedComponent.length > 0 && (
+
+
+ Fields:
+
+
+ {info.addedComponent.map((component, componentIndex) => {
+ const value = _.get(templateItem, component.name);
+ let displayValue = "N/A";
+
+ if (value) {
+ if (typeof value === "object" && value !== null) {
+ displayValue =
+ value.label || value.value || JSON.stringify(value);
+ } else {
+ displayValue = String(value);
+ }
+ }
+
+ return (
+
+
+ {component.label || component.name}:
+
+
+
+ );
+ })}
+
+
+ )}
+
+
+
+
+ ));
+ }
+
+ // Handle regular standards (non-template arrays)
+ return (
+
+
+
+
+
+
+ {info.label}
+
+
+ {info.helpText}
+
+
+
+
+ {info.tag && info.tag.length > 0 && (
+
+ )}
+
+
+
+ Actions:
+
+
+ {config.action && Array.isArray(config.action) ? (
+ config.action.map((action, index) => (
-
- );
- })}
-
-
- )}
-
-
-
-
- ))}
-
-
-
- ))}
+ ))
+ ) : (
+
+ No actions configured
+
+ )}
+
+
+
+ {info.addedComponent && info.addedComponent.length > 0 && (
+
+
+ Fields:
+
+
+ {info.addedComponent.map((component, index) => {
+ let componentValue;
+ let displayValue = "N/A";
+
+ // Handle regular standards and nested standards structures
+ let extractedValue = null;
+
+ // Try direct access first
+ componentValue = _.get(config, component.name);
+
+ // If direct access fails and component name contains dots (nested structure)
+ if (
+ (componentValue === undefined ||
+ componentValue === null) &&
+ component.name.includes(".")
+ ) {
+ const pathParts = component.name.split(".");
+
+ // Handle structures like: standards.AuthMethodsSettings.ReportSuspiciousActivity
+ if (pathParts[0] === "standards" && config.standards) {
+ // Remove 'standards.' prefix and try to find the value in config.standards
+ const nestedPath = pathParts.slice(1).join(".");
+ extractedValue = _.get(config.standards, nestedPath);
+
+ // If still not found, try alternative nested structures
+ // Some standards have double nesting like: config.standards.StandardName.fieldName
+ if (
+ (extractedValue === undefined ||
+ extractedValue === null) &&
+ pathParts.length >= 3
+ ) {
+ const standardName = pathParts[1];
+ const fieldPath = pathParts.slice(2).join(".");
+ extractedValue = _.get(
+ config.standards,
+ `${standardName}.${fieldPath}`
+ );
+ }
+ }
+ } else {
+ extractedValue = componentValue;
+ }
+
+ if (extractedValue) {
+ if (Array.isArray(extractedValue)) {
+ // Handle array of objects
+ const arrayValues = extractedValue.map((item) => {
+ if (typeof item === "object" && item !== null) {
+ return (
+ item.label || item.value || JSON.stringify(item)
+ );
+ }
+ return String(item);
+ });
+ displayValue = arrayValues.join(", ");
+ } else if (
+ typeof extractedValue === "object" &&
+ extractedValue !== null
+ ) {
+ if (extractedValue.label) {
+ displayValue = extractedValue.label;
+ } else if (extractedValue.value) {
+ displayValue = extractedValue.value;
+ } else {
+ displayValue = JSON.stringify(extractedValue);
+ }
+ } else {
+ displayValue = String(extractedValue);
+ }
+ }
+
+ return (
+
+
+ {component.label || component.name}:
+
+
+
+ );
+ })}
+
+
+ )}
+
+
+
+
+ );
+ })}
+
+
+
+ );
+ })}
{Object.keys(standardsByCategory).length === 0 && (
From e90fc8d0aa8d920ff9d662613e0fb340b482a829 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?=
Date: Tue, 19 Aug 2025 19:40:03 +0200
Subject: [PATCH 05/19] Add SendNotificationToUser handling in calendar
permissions dialog and user exchange page
---
.../CippCalendarPermissionsDialog.jsx | 32 +++++++++++++++++++
.../administration/users/user/exchange.jsx | 3 ++
2 files changed, 35 insertions(+)
diff --git a/src/components/CippComponents/CippCalendarPermissionsDialog.jsx b/src/components/CippComponents/CippCalendarPermissionsDialog.jsx
index d2d4c89d71fe..b8c63e2994d2 100644
--- a/src/components/CippComponents/CippCalendarPermissionsDialog.jsx
+++ b/src/components/CippComponents/CippCalendarPermissionsDialog.jsx
@@ -17,6 +17,15 @@ const CippCalendarPermissionsDialog = ({ formHook, combinedOptions, isUserGroupL
}
}, [isEditor, formHook]);
+ // default SendNotificationToUser to false on mount
+ useEffect(() => {
+ formHook.setValue("SendNotificationToUser", false);
+ }, [formHook]);
+
+ // Only certain permission levels support sending a notification when calendar permissions are added
+ const notifyAllowed = ["AvailabilityOnly", "LimitedDetails", "Reviewer", "Editor"];
+ const isNotifyAllowed = notifyAllowed.includes(permissionLevel?.value ?? permissionLevel);
+
return (
@@ -80,6 +89,29 @@ const CippCalendarPermissionsDialog = ({ formHook, combinedOptions, isUserGroupL
+
+
+
+
+
+
+
+
);
};
diff --git a/src/pages/identity/administration/users/user/exchange.jsx b/src/pages/identity/administration/users/user/exchange.jsx
index 22f77d21e093..f4ffafe2d2ed 100644
--- a/src/pages/identity/administration/users/user/exchange.jsx
+++ b/src/pages/identity/administration/users/user/exchange.jsx
@@ -235,6 +235,9 @@ const Page = () => {
permission.CanViewPrivateItems = true;
}
+ // Always include SendNotificationToUser explicitly (default false)
+ permission.SendNotificationToUser = Boolean(data.SendNotificationToUser);
+
return {
userID: graphUserRequest.data?.[0]?.userPrincipalName,
tenantFilter: userSettingsDefaults.currentTenant,
From 52d33edab2c758ca831e0939ae3557c78498c808 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?=
Date: Tue, 19 Aug 2025 21:44:38 +0200
Subject: [PATCH 06/19] Enhance CippAutoComplete preselection logic and enable
preselected option in CippPolicyDeployDrawer
---
.../CippComponents/CippAutocomplete.jsx | 28 +++++++++++++++----
.../CippComponents/CippPolicyDeployDrawer.jsx | 1 +
2 files changed, 23 insertions(+), 6 deletions(-)
diff --git a/src/components/CippComponents/CippAutocomplete.jsx b/src/components/CippComponents/CippAutocomplete.jsx
index 106b3f0f646d..9ae5d74287e8 100644
--- a/src/components/CippComponents/CippAutocomplete.jsx
+++ b/src/components/CippComponents/CippAutocomplete.jsx
@@ -209,13 +209,29 @@ export const CippAutoComplete = (props) => {
// Dedicated effect for handling preselected value
useEffect(() => {
- if (preselectedValue && !defaultValue && !value && memoizedOptions.length > 0) {
- const preselectedOption = memoizedOptions.find((option) => option.value === preselectedValue);
+ if (preselectedValue && memoizedOptions.length > 0) {
+ // Check if we should skip preselection due to existing defaultValue
+ const hasDefaultValue =
+ defaultValue && (Array.isArray(defaultValue) ? defaultValue.length > 0 : true);
- if (preselectedOption) {
- const newValue = multiple ? [preselectedOption] : preselectedOption;
- if (onChange) {
- onChange(newValue, newValue?.addedFields);
+ if (!hasDefaultValue) {
+ // For multiple mode, check if value is empty array or null/undefined
+ // For single mode, check if value is null/undefined
+ const shouldPreselect = multiple
+ ? !value || (Array.isArray(value) && value.length === 0)
+ : !value;
+
+ if (shouldPreselect) {
+ const preselectedOption = memoizedOptions.find(
+ (option) => option.value === preselectedValue
+ );
+
+ if (preselectedOption) {
+ const newValue = multiple ? [preselectedOption] : preselectedOption;
+ if (onChange) {
+ onChange(newValue, newValue?.addedFields);
+ }
+ }
}
}
}
diff --git a/src/components/CippComponents/CippPolicyDeployDrawer.jsx b/src/components/CippComponents/CippPolicyDeployDrawer.jsx
index a92ccda882d0..6b9635fe5bb4 100644
--- a/src/components/CippComponents/CippPolicyDeployDrawer.jsx
+++ b/src/components/CippComponents/CippPolicyDeployDrawer.jsx
@@ -110,6 +110,7 @@ export const CippPolicyDeployDrawer = ({
required={true}
disableClearable={false}
allTenants={true}
+ preselectedEnabled={true}
type="multiple"
/>
Date: Tue, 19 Aug 2025 22:01:13 +0200
Subject: [PATCH 07/19] Add useRef for preselection handling in
CippAutoComplete
---
src/components/CippComponents/CippAutocomplete.jsx | 8 +++++---
1 file changed, 5 insertions(+), 3 deletions(-)
diff --git a/src/components/CippComponents/CippAutocomplete.jsx b/src/components/CippComponents/CippAutocomplete.jsx
index 9ae5d74287e8..6ac86f53d4ba 100644
--- a/src/components/CippComponents/CippAutocomplete.jsx
+++ b/src/components/CippComponents/CippAutocomplete.jsx
@@ -6,7 +6,7 @@ import {
TextField,
IconButton,
} from "@mui/material";
-import { useEffect, useState, useMemo, useCallback } from "react";
+import { useEffect, useState, useMemo, useCallback, useRef } from "react";
import { useSettings } from "../../hooks/use-settings";
import { getCippError } from "../../utils/get-cipp-error";
import { ApiGetCallWithPagination } from "../../api/ApiCall";
@@ -78,6 +78,7 @@ export const CippAutoComplete = (props) => {
const [usedOptions, setUsedOptions] = useState(options);
const [getRequestInfo, setGetRequestInfo] = useState({ url: "", waiting: false, queryKey: "" });
+ const hasPreselectedRef = useRef(false);
const filter = createFilterOptions({
stringify: (option) => JSON.stringify(option),
});
@@ -207,9 +208,9 @@ export const CippAutoComplete = (props) => {
return finalOptions;
}, [api, usedOptions, options, removeOptions, sortOptions]);
- // Dedicated effect for handling preselected value
+ // Dedicated effect for handling preselected value - only runs once
useEffect(() => {
- if (preselectedValue && memoizedOptions.length > 0) {
+ if (preselectedValue && memoizedOptions.length > 0 && !hasPreselectedRef.current) {
// Check if we should skip preselection due to existing defaultValue
const hasDefaultValue =
defaultValue && (Array.isArray(defaultValue) ? defaultValue.length > 0 : true);
@@ -228,6 +229,7 @@ export const CippAutoComplete = (props) => {
if (preselectedOption) {
const newValue = multiple ? [preselectedOption] : preselectedOption;
+ hasPreselectedRef.current = true; // Mark that we've preselected
if (onChange) {
onChange(newValue, newValue?.addedFields);
}
From f26a0b813502b53fbf26f84a58ba8a998b694b3c Mon Sep 17 00:00:00 2001
From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com>
Date: Wed, 20 Aug 2025 09:28:07 +0200
Subject: [PATCH 08/19] allow cloning of drift standards
---
.../tenant/standards/list-standards/classic-standards/index.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/pages/tenant/standards/list-standards/classic-standards/index.js b/src/pages/tenant/standards/list-standards/classic-standards/index.js
index b650b4325433..7c9b55b3bee4 100644
--- a/src/pages/tenant/standards/list-standards/classic-standards/index.js
+++ b/src/pages/tenant/standards/list-standards/classic-standards/index.js
@@ -37,7 +37,7 @@ const Page = () => {
},
{
label: "Clone & Edit Template",
- link: "/tenant/standards/template?id=[GUID]&clone=true",
+ link: "/tenant/standards/template?id=[GUID]&clone=true&type=[type]",
icon: ,
color: "success",
target: "_self",
From e894150eda89b08a4ee71cbee49ca505cea9fb89 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 20 Aug 2025 08:16:46 +0000
Subject: [PATCH 09/19] Initial plan
From 219fa8c64adbad6a9117530be2a3bf79077834bd Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 20 Aug 2025 08:22:39 +0000
Subject: [PATCH 10/19] Initial plan: Remove redundant 'Add Status Page' menu
item
Co-authored-by: kris6673 <31723128+kris6673@users.noreply.github.com>
---
.gitignore | 3 +++
1 file changed, 3 insertions(+)
diff --git a/.gitignore b/.gitignore
index 5ee28a7a617b..eb063c4f22dd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -31,3 +31,6 @@ app.log
# AI rules
.*/rules
+
+package-lock.json
+node_modules/
From 2155107e24d1c9d7c1fafbb4dc49dbce03df4266 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 20 Aug 2025 08:27:46 +0000
Subject: [PATCH 11/19] Remove redundant 'Add Status Page' menu item -
functionality already integrated
Co-authored-by: kris6673 <31723128+kris6673@users.noreply.github.com>
---
generate-placeholders.js | 1 -
src/layouts/config.js | 5 -----
2 files changed, 6 deletions(-)
diff --git a/generate-placeholders.js b/generate-placeholders.js
index 34e28fb31ccf..2b34888fca7a 100644
--- a/generate-placeholders.js
+++ b/generate-placeholders.js
@@ -83,7 +83,6 @@ const pages = [
{ title: "Profiles", path: "/endpoint/autopilot/list-profiles" },
{ title: "Add Profile", path: "/endpoint/autopilot/add-profile" },
{ title: "Status Pages", path: "/endpoint/autopilot/list-status-pages" },
- { title: "Add Status Page", path: "/endpoint/autopilot/add-status-page" },
{ title: "Devices", path: "/endpoint/MEM/devices" },
{ title: "Configuration Policies", path: "/endpoint/MEM/list-policies" },
{ title: "Compliance Policies", path: "/endpoint/MEM/list-compliance-policies" },
diff --git a/src/layouts/config.js b/src/layouts/config.js
index 9be46306e92d..5025b4558b87 100644
--- a/src/layouts/config.js
+++ b/src/layouts/config.js
@@ -415,11 +415,6 @@ export const nativeMenuItems = [
path: "/endpoint/autopilot/list-status-pages",
permissions: ["Endpoint.Autopilot.*"],
},
- {
- title: "Add Status Page",
- path: "/endpoint/autopilot/add-status-page",
- permissions: ["Endpoint.Autopilot.*"],
- },
],
},
{
From 04a5648cd13146a9539be9c27bce8028f0b1f81b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?=
<31723128+kris6673@users.noreply.github.com>
Date: Wed, 20 Aug 2025 10:43:50 +0200
Subject: [PATCH 12/19] Fix whoops
---
.gitignore | 3 ---
1 file changed, 3 deletions(-)
diff --git a/.gitignore b/.gitignore
index eb063c4f22dd..5ee28a7a617b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -31,6 +31,3 @@ app.log
# AI rules
.*/rules
-
-package-lock.json
-node_modules/
From c5eb0e1941f816a310fc2f21387a0c21af061b7e Mon Sep 17 00:00:00 2001
From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com>
Date: Wed, 20 Aug 2025 12:50:24 +0200
Subject: [PATCH 13/19] clone to drift
---
.../list-standards/classic-standards/index.js | 14 +++++++++++++-
1 file changed, 13 insertions(+), 1 deletion(-)
diff --git a/src/pages/tenant/standards/list-standards/classic-standards/index.js b/src/pages/tenant/standards/list-standards/classic-standards/index.js
index 7c9b55b3bee4..c160d449526b 100644
--- a/src/pages/tenant/standards/list-standards/classic-standards/index.js
+++ b/src/pages/tenant/standards/list-standards/classic-standards/index.js
@@ -3,7 +3,7 @@ import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"
import { Layout as DashboardLayout } from "/src/layouts/index.js"; // had to add an extra path here because I added an extra folder structure. We should switch to absolute pathing so we dont have to deal with relative.
import { TabbedLayout } from "/src/layouts/TabbedLayout";
import Link from "next/link";
-import { CopyAll, Delete, PlayArrow, AddBox, Edit, GitHub } from "@mui/icons-material";
+import { CopyAll, Delete, PlayArrow, AddBox, Edit, GitHub, ContentCopy } from "@mui/icons-material";
import { ApiGetCall, ApiPostCall } from "../../../../../api/ApiCall";
import { Grid } from "@mui/system";
import { CippApiResults } from "../../../../../components/CippComponents/CippApiResults";
@@ -42,6 +42,18 @@ const Page = () => {
color: "success",
target: "_self",
},
+ {
+ label: "Create Drift Clone",
+ type: "POST",
+ url: "/api/ExecDriftClone",
+ icon: ,
+ color: "warning",
+ data: {
+ id: "GUID",
+ },
+ confirmText: "Are you sure you want to create a drift clone of [templateName]? This will create a new drift template based on this template.",
+ multiPost: false,
+ },
{
label: "Run Template Now (Currently Selected Tenant only)",
type: "GET",
From 9d6d311ea7089c58b692b56c31b5dc84be693256 Mon Sep 17 00:00:00 2001
From: Dirk Haex
Date: Wed, 20 Aug 2025 14:34:38 +0200
Subject: [PATCH 14/19] Added pointer to AlertSmtpAuthSuccess
Added pointer to AlertSmtpAuthSuccess
Signed-off-by: Dirk Haex
---
src/data/alerts.json | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/src/data/alerts.json b/src/data/alerts.json
index aa5936ca4578..b7e6c0428a3f 100644
--- a/src/data/alerts.json
+++ b/src/data/alerts.json
@@ -13,6 +13,11 @@
"name": "LicenseAssignmentErrors",
"label": "Alert on license assignment errors",
"recommendedRunInterval": "1d"
+ },
+ {
+ "name": "AlertSmtpAuthSuccess",
+ "label": "Alert on SMTP AUTH usage with success, helps to phase out SMTP AUTH",
+ "recommendedRunInterval": "1d"
},
{
"name": "NoCAConfig",
From a7cb450b1f8971a5bde2f128e67e6a699fda9b9e Mon Sep 17 00:00:00 2001
From: Dirk Haex
Date: Wed, 20 Aug 2025 14:55:24 +0200
Subject: [PATCH 15/19] Update alerts.json
Added p1 requirement.
Signed-off-by: Dirk Haex
---
src/data/alerts.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/data/alerts.json b/src/data/alerts.json
index b7e6c0428a3f..17794fa7b32a 100644
--- a/src/data/alerts.json
+++ b/src/data/alerts.json
@@ -16,7 +16,7 @@
},
{
"name": "AlertSmtpAuthSuccess",
- "label": "Alert on SMTP AUTH usage with success, helps to phase out SMTP AUTH",
+ "label": "Alert on SMTP AUTH usage with success, helps to phase out SMTP AUTH (Entra P1 Required)",
"recommendedRunInterval": "1d"
},
{
From 08fabd78130b8eaee4c983eb9b01b8601c085788 Mon Sep 17 00:00:00 2001
From: jspern <41965203+jspern@users.noreply.github.com>
Date: Wed, 20 Aug 2025 12:09:31 -0400
Subject: [PATCH 16/19] Add functionality for user to select HaloPSA ticket
outcome and add handling for deleted tickets
---
src/data/Extensions.json | 65 +++++++++++++++++++++++++++-------------
1 file changed, 45 insertions(+), 20 deletions(-)
diff --git a/src/data/Extensions.json b/src/data/Extensions.json
index 4d8bcc01386c..0b56cc72dfde 100644
--- a/src/data/Extensions.json
+++ b/src/data/Extensions.json
@@ -334,10 +334,37 @@
"action": "disable"
}
},
+ {
+ "type": "textField",
+ "name": "HaloPSA.ClientID",
+ "label": "HaloPSA Client ID",
+ "placeholder": "Enter your HaloPSA Client ID",
+ "required": true,
+ "condition": {
+ "field": "HaloPSA.Enabled",
+ "compareType": "is",
+ "compareValue": true,
+ "action": "disable"
+ }
+ },
+ {
+ "type": "password",
+ "name": "HaloPSA.APIKey",
+ "label": "HaloPSA Client Secret",
+ "placeholder": "Enter your client Secret. Leave blank to keep your current key.",
+ "required": true,
+ "condition": {
+ "field": "HaloPSA.Enabled",
+ "compareType": "is",
+ "compareValue": true,
+ "action": "disable"
+ }
+ },
{
"type": "autoComplete",
"name": "HaloPSA.TicketType",
- "label": "Select your HaloPSA Ticket Type, leave blank for default.",
+ "label": "HaloPSA Ticket Type",
+ "placeholder": "Select your HaloPSA Ticket Type, leave blank for default",
"multiple": false,
"api": {
"url": "/api/ExecExtensionMapping",
@@ -358,26 +385,24 @@
}
},
{
- "type": "textField",
- "name": "HaloPSA.ClientID",
- "label": "HaloPSA Client ID",
- "placeholder": "Enter your HaloPSA Client ID",
- "required": true,
- "condition": {
- "field": "HaloPSA.Enabled",
- "compareType": "is",
- "compareValue": true,
- "action": "disable"
- }
- },
- {
- "type": "password",
- "name": "HaloPSA.APIKey",
- "label": "HaloPSA Client Secret",
- "placeholder": "Enter your client Secret. Leave blank to keep your current key.",
- "required": true,
+ "type": "autoComplete",
+ "name": "HaloPSA.Outcome",
+ "label": "HaloPSA Outcome",
+ "placeholder": "Select your HaloPSA Outcome, leave blank for default",
+ "multiple": false,
+ "api": {
+ "url": "/api/ExecExtensionMapping",
+ "data": {
+ "List": "HaloPSAFields"
+ },
+ "queryKey": "HaloOutcomes",
+ "dataKey": "Outcomes",
+ "labelField": "buttonname",
+ "valueField": "id",
+ "showRefresh": true
+ },
"condition": {
- "field": "HaloPSA.Enabled",
+ "field": "HaloPSA.ConsolidateTickets",
"compareType": "is",
"compareValue": true,
"action": "disable"
From 08b27f99783b71d32aaf795a807cf3ab70f865b4 Mon Sep 17 00:00:00 2001
From: John Duprey
Date: Wed, 20 Aug 2025 12:27:59 -0400
Subject: [PATCH 17/19] null safety for property missing from user table
---
.../CippComponents/CippUserActions.jsx | 22 ++++++++++---------
1 file changed, 12 insertions(+), 10 deletions(-)
diff --git a/src/components/CippComponents/CippUserActions.jsx b/src/components/CippComponents/CippUserActions.jsx
index 59312d5401f4..40e53ff24ea9 100644
--- a/src/components/CippComponents/CippUserActions.jsx
+++ b/src/components/CippComponents/CippUserActions.jsx
@@ -435,7 +435,7 @@ export const CippUserActions = () => {
},
confirmText: "Are you sure you want to clear the Immutable ID for [userPrincipalName]?",
multiPost: false,
- condition: (row) => !row.onPremisesSyncEnabled && row?.onPremisesImmutableId && canWriteUser,
+ condition: (row) => !row?.onPremisesSyncEnabled && row?.onPremisesImmutableId && canWriteUser,
},
{
label: "Revoke all user sessions",
@@ -465,17 +465,19 @@ export const CippUserActions = () => {
customFunction: (users, action, formData) => {
// Handle both single user and multiple users
const userData = Array.isArray(users) ? users : [users];
-
+
// Store users in session storage to avoid URL length limits
- sessionStorage.setItem('patchWizardUsers', JSON.stringify(userData));
-
+ sessionStorage.setItem("patchWizardUsers", JSON.stringify(userData));
+
// Use Next.js router for internal navigation
- import('next/router').then(({ default: router }) => {
- router.push('/identity/administration/users/patch-wizard');
- }).catch(() => {
- // Fallback to window.location if router is not available
- window.location.href = '/identity/administration/users/patch-wizard';
- });
+ import("next/router")
+ .then(({ default: router }) => {
+ router.push("/identity/administration/users/patch-wizard");
+ })
+ .catch(() => {
+ // Fallback to window.location if router is not available
+ window.location.href = "/identity/administration/users/patch-wizard";
+ });
},
condition: () => canWriteUser,
},
From 7187ad09d26aff9812edecdf435f74a087e2ab65 Mon Sep 17 00:00:00 2001
From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com>
Date: Wed, 20 Aug 2025 23:18:05 +0200
Subject: [PATCH 18/19] Fix copy
---
src/components/CippTable/CippDataTable.js | 106 ++++++++++++----------
1 file changed, 60 insertions(+), 46 deletions(-)
diff --git a/src/components/CippTable/CippDataTable.js b/src/components/CippTable/CippDataTable.js
index 8409e4dd31a6..3d2cc8a89f64 100644
--- a/src/components/CippTable/CippDataTable.js
+++ b/src/components/CippTable/CippDataTable.js
@@ -200,6 +200,20 @@ export const CippDataTable = (props) => {
};
const table = useMaterialReactTable({
+ muiTableBodyCellProps: {
+ onCopy: (e) => {
+ const sel = window.getSelection()?.toString() ?? "";
+ if (sel) {
+ e.preventDefault();
+ e.stopPropagation();
+ e.nativeEvent?.stopImmediatePropagation?.();
+ e.clipboardData.setData("text/plain", sel);
+ if (navigator.clipboard?.writeText) {
+ navigator.clipboard.writeText(sel).catch(() => {});
+ }
+ }
+ },
+ },
mrtTheme: (theme) => ({
baseBackgroundColor: theme.palette.background.paper,
}),
@@ -215,66 +229,66 @@ export const CippDataTable = (props) => {
muiTableHeadCellProps: {
sx: {
// Target the filter row cells
- '& .MuiTableCell-root': {
- padding: '8px 16px',
+ "& .MuiTableCell-root": {
+ padding: "8px 16px",
},
// Target the Autocomplete component in filter cells
- '& .MuiAutocomplete-root': {
- width: '100%',
+ "& .MuiAutocomplete-root": {
+ width: "100%",
},
// Force the tags container to be single line with ellipsis
- '& .MuiAutocomplete-root .MuiInputBase-root': {
- height: '40px !important',
- overflow: 'hidden',
- textOverflow: 'ellipsis',
- whiteSpace: 'nowrap',
- display: 'flex',
- flexWrap: 'nowrap',
+ "& .MuiAutocomplete-root .MuiInputBase-root": {
+ height: "40px !important",
+ overflow: "hidden",
+ textOverflow: "ellipsis",
+ whiteSpace: "nowrap",
+ display: "flex",
+ flexWrap: "nowrap",
},
// Target the tags container specifically
- '& .MuiAutocomplete-root .MuiInputBase-root .MuiInputBase-input': {
- height: '24px',
- minHeight: '24px',
- maxHeight: '24px',
+ "& .MuiAutocomplete-root .MuiInputBase-root .MuiInputBase-input": {
+ height: "24px",
+ minHeight: "24px",
+ maxHeight: "24px",
},
// Target regular input fields (not in Autocomplete)
- '& .MuiInputBase-root': {
- height: '40px !important',
+ "& .MuiInputBase-root": {
+ height: "40px !important",
},
// Ensure all input fields have consistent styling
- '& .MuiInputBase-input': {
- height: '24px',
- minHeight: '24px',
- maxHeight: '24px',
+ "& .MuiInputBase-input": {
+ height: "24px",
+ minHeight: "24px",
+ maxHeight: "24px",
},
// Target the specific chip class mentioned
- '& .MuiChip-label.MuiChip-labelMedium': {
- maxWidth: '80px',
- overflow: 'hidden',
- textOverflow: 'ellipsis',
- whiteSpace: 'nowrap',
- padding: '0 4px',
+ "& .MuiChip-label.MuiChip-labelMedium": {
+ maxWidth: "80px",
+ overflow: "hidden",
+ textOverflow: "ellipsis",
+ whiteSpace: "nowrap",
+ padding: "0 4px",
},
// Make chips smaller overall and add title attribute for tooltip
- '& .MuiChip-root': {
- height: '24px',
- maxHeight: '24px',
+ "& .MuiChip-root": {
+ height: "24px",
+ maxHeight: "24px",
// This adds a tooltip effect using the browser's native tooltip
- '&::before': {
- content: 'attr(data-label)',
- display: 'none',
+ "&::before": {
+ content: "attr(data-label)",
+ display: "none",
},
- '&:hover::before': {
- display: 'block',
- position: 'absolute',
- top: '-25px',
- left: '0',
- backgroundColor: 'rgba(0, 0, 0, 0.8)',
- color: 'white',
- padding: '4px 8px',
- borderRadius: '4px',
- fontSize: '12px',
- whiteSpace: 'nowrap',
+ "&:hover::before": {
+ display: "block",
+ position: "absolute",
+ top: "-25px",
+ left: "0",
+ backgroundColor: "rgba(0, 0, 0, 0.8)",
+ color: "white",
+ padding: "4px 8px",
+ borderRadius: "4px",
+ fontSize: "12px",
+ whiteSpace: "nowrap",
zIndex: 9999,
},
},
@@ -570,7 +584,7 @@ export const CippDataTable = (props) => {
) : (
// Render the table inside a Card
- (
+
{cardButton || !hideTitle ? (
<>
@@ -602,7 +616,7 @@ export const CippDataTable = (props) => {
)}
- )
+
)}
Date: Wed, 20 Aug 2025 18:08:18 -0400
Subject: [PATCH 19/19] Update version.json
---
public/version.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/public/version.json b/public/version.json
index dc8317517d9b..711bdd5d11ca 100644
--- a/public/version.json
+++ b/public/version.json
@@ -1,3 +1,3 @@
{
- "version": "8.3.1"
+ "version": "8.3.2"
}
\ No newline at end of file