From 0eabf9534a36e118cffd20304d7520738bd843b9 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Sun, 14 Sep 2025 12:53:45 +0800 Subject: [PATCH 001/112] App permission updates --- public/permissionsList.json | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/public/permissionsList.json b/public/permissionsList.json index c91c29126036..8ec55a707b7e 100644 --- a/public/permissionsList.json +++ b/public/permissionsList.json @@ -7969,5 +7969,25 @@ "userConsentDescription": "Allows the app to manage workforce integrations, to synchronize data from Microsoft Teams Shifts, on your behalf.", "userConsentDisplayName": "Read and write workforce integrations", "value": "WorkforceIntegration.ReadWrite.All" + }, + { + "description": "Read and Modify Tenant-Acquired Telephone Number Details", + "displayName": "Read and Modify Tenant-Acquired Telephone Number Details", + "id": "424b07a8-1209-4d17-9fe4-9018a93a1024", + "isEnabled": true, + "Origin": "Delegated", + "userConsentDescription": "Allows the app to read and modify your tenant's acquired telephone number details on behalf of the signed-in admin user. Acquired telephone numbers may include attributes related to assigned object, emergency location, network site, etc.", + "userConsentDisplayName": "Allows the app to read and modify your tenant's acquired telephone number details on behalf of the signed-in admin user. Acquired telephone numbers may include attributes related to assigned object, emergency location, network site, etc.", + "value": "TeamsTelephoneNumber.ReadWrite.All" + }, + { + "description": "Read and Modify Tenant-Acquired Telephone Number Details", + "displayName": "Read and Modify Tenant-Acquired Telephone Number Details", + "id": "0a42382f-155c-4eb1-9bdc-21548ccaa387", + "isEnabled": true, + "Origin": "Application", + "userConsentDescription": "Allows the app to read your tenant's acquired telephone number details, without a signed-in user. Acquired telephone numbers may include attributes related to assigned object, emergency location, network site, etc.", + "userConsentDisplayName": "Allows the app to read your tenant's acquired telephone number details, without a signed-in user. Acquired telephone numbers may include attributes related to assigned object, emergency location, network site, etc.", + "value": "TeamsTelephoneNumber.ReadWrite.All" } ] From 4769fb5c7dc928f4d0d4d932d50e063eddc726c1 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Sun, 14 Sep 2025 18:52:02 +0800 Subject: [PATCH 002/112] UI Support for Removing all Teams Phone DIDs from user --- .../CippComponents/CippOffboardingDefaultSettings.jsx | 10 ++++++++++ src/components/CippComponents/CippSettingsSideBar.jsx | 1 + src/components/CippWizard/CippWizardOffboarding.jsx | 6 ++++++ src/pages/tenant/administration/tenants/edit.js | 2 ++ 4 files changed, 19 insertions(+) diff --git a/src/components/CippComponents/CippOffboardingDefaultSettings.jsx b/src/components/CippComponents/CippOffboardingDefaultSettings.jsx index 4ddad43c7aff..9ac089dee39b 100644 --- a/src/components/CippComponents/CippOffboardingDefaultSettings.jsx +++ b/src/components/CippComponents/CippOffboardingDefaultSettings.jsx @@ -185,6 +185,16 @@ export const CippOffboardingDefaultSettings = (props) => { /> ), }, + { + label: "Remove Teams Phone DID", + value: ( + + ), + }, { label: "Clear Immutable ID", value: ( diff --git a/src/components/CippComponents/CippSettingsSideBar.jsx b/src/components/CippComponents/CippSettingsSideBar.jsx index 03621be6b25f..693c55673d77 100644 --- a/src/components/CippComponents/CippSettingsSideBar.jsx +++ b/src/components/CippComponents/CippSettingsSideBar.jsx @@ -96,6 +96,7 @@ export const CippSettingsSideBar = (props) => { RemoveMobile: formValues.offboardingDefaults?.RemoveMobile, DisableSignIn: formValues.offboardingDefaults?.DisableSignIn, RemoveMFADevices: formValues.offboardingDefaults?.RemoveMFADevices, + RemoveTeamsPhoneDID: formValues.offboardingDefaults?.RemoveTeamsPhoneDID, ClearImmutableId: formValues.offboardingDefaults?.ClearImmutableId, }, }; diff --git a/src/components/CippWizard/CippWizardOffboarding.jsx b/src/components/CippWizard/CippWizardOffboarding.jsx index 790e8443427a..80b1dd8855e5 100644 --- a/src/components/CippWizard/CippWizardOffboarding.jsx +++ b/src/components/CippWizard/CippWizardOffboarding.jsx @@ -164,6 +164,12 @@ export const CippWizardOffboarding = (props) => { type="switch" formControl={formControl} /> + { RemoveMobile: false, DisableSignIn: false, RemoveMFADevices: false, + RemoveTeamsPhoneDID: false, ClearImmutableId: false, }; @@ -110,6 +111,7 @@ const Page = () => { RemoveMobile: false, DisableSignIn: false, RemoveMFADevices: false, + RemoveTeamsPhoneDID: false, ClearImmutableId: false, }; From c87e098d85a47bcb2bc70dab7c78467b2281482c Mon Sep 17 00:00:00 2001 From: John Duprey Date: Mon, 15 Sep 2025 15:24:59 -0400 Subject: [PATCH 003/112] code cleanup add some default value handlers to prevent uncontrolled form components --- .../CippComponents/CippFormComponent.jsx | 4 +- .../CippTable/CIPPTableToptoolbar.js | 62 +++++++++---------- 2 files changed, 30 insertions(+), 36 deletions(-) diff --git a/src/components/CippComponents/CippFormComponent.jsx b/src/components/CippComponents/CippFormComponent.jsx index c41a7ce8ac89..097d9fa6d38f 100644 --- a/src/components/CippComponents/CippFormComponent.jsx +++ b/src/components/CippComponents/CippFormComponent.jsx @@ -204,11 +204,11 @@ export const CippFormComponent = (props) => { renderSwitchWithLabel( diff --git a/src/components/CippTable/CIPPTableToptoolbar.js b/src/components/CippTable/CIPPTableToptoolbar.js index 9abffd703fba..02ba791a705a 100644 --- a/src/components/CippTable/CIPPTableToptoolbar.js +++ b/src/components/CippTable/CIPPTableToptoolbar.js @@ -247,8 +247,6 @@ export const CIPPTableToptoolbar = ({ ) { const last = settings.lastUsedFilters[pageName]; if (last.type === "graph") { - console.log("Early restoring graph filter:", last, "for page:", pageName); - // Mark as restored to prevent infinite loops restoredFiltersRef.current.add(restorationKey); @@ -278,6 +276,16 @@ export const CIPPTableToptoolbar = ({ }); setCurrentEffectiveQueryKey(newQueryKey); setActiveFilterName(last.name); + + if (last.value?.$select) { + const selectColumns = last.value.$select + .split(",") + .map((col) => col.trim()) + .filter((col) => usedColumns.includes(col)); + if (selectColumns.length > 0) { + setConfiguredSimpleColumns(selectColumns); + } + } } } }, [settings.persistFilters, settings.lastUsedFilters, pageName, api?.url, queryKey, title]); @@ -301,7 +309,6 @@ export const CIPPTableToptoolbar = ({ // Use setTimeout to ensure the table is fully rendered const timeoutId = setTimeout(() => { const last = settings.lastUsedFilters[pageName]; - console.log("Restoring filter:", last, "for page:", pageName); if (last.type === "global") { table.setGlobalFilter(last.value); @@ -311,7 +318,6 @@ export const CIPPTableToptoolbar = ({ const allColumns = table.getAllColumns().map((col) => col.id); const filterColumns = Array.isArray(last.value) ? last.value.map((f) => f.id) : []; const allExist = filterColumns.every((colId) => allColumns.includes(colId)); - console.log("Column filter check:", { allColumns, filterColumns, allExist }); if (allExist) { table.setShowColumnFilters(true); table.setColumnFilters(last.value); @@ -417,6 +423,22 @@ export const CIPPTableToptoolbar = ({ return merged; }; + // Shared function for setting nested column visibility + const setNestedVisibility = (col) => { + if (typeof col === "object" && col !== null) { + Object.keys(col).forEach((key) => { + if (usedColumns.includes(key.trim())) { + setColumnVisibility((prev) => ({ ...prev, [key.trim()]: true })); + setNestedVisibility(col[key]); + } + }); + } else { + if (usedColumns.includes(col.trim())) { + setColumnVisibility((prev) => ({ ...prev, [col.trim()]: true })); + } + } + }; + const setTableFilter = (filter, filterType, filterName) => { if (filterType === "global" || filterType === undefined) { table.setGlobalFilter(filter); @@ -481,20 +503,6 @@ export const CIPPTableToptoolbar = ({ } else { selectedColumns = filter?.$select.split(","); } - const setNestedVisibility = (col) => { - if (typeof col === "object" && col !== null) { - Object.keys(col).forEach((key) => { - if (usedColumns.includes(key.trim())) { - setColumnVisibility((prev) => ({ ...prev, [key.trim()]: true })); - setNestedVisibility(col[key]); - } - }); - } else { - if (usedColumns.includes(col.trim())) { - setColumnVisibility((prev) => ({ ...prev, [col.trim()]: true })); - } - } - }; if (selectedColumns.length > 0) { setConfiguredSimpleColumns(selectedColumns); selectedColumns.forEach((col) => { @@ -743,7 +751,7 @@ export const CIPPTableToptoolbar = ({ }) } > - + ))} @@ -922,7 +930,7 @@ export const CIPPTableToptoolbar = ({ }) } > - + ))} @@ -1190,20 +1198,6 @@ export const CIPPTableToptoolbar = ({ } else { selectedColumns = filter?.$select.split(","); } - const setNestedVisibility = (col) => { - if (typeof col === "object" && col !== null) { - Object.keys(col).forEach((key) => { - if (usedColumns.includes(key.trim())) { - setColumnVisibility((prev) => ({ ...prev, [key.trim()]: true })); - setNestedVisibility(col[key]); - } - }); - } else { - if (usedColumns.includes(col.trim())) { - setColumnVisibility((prev) => ({ ...prev, [col.trim()]: true })); - } - } - }; if (selectedColumns.length > 0) { setConfiguredSimpleColumns(selectedColumns); selectedColumns.forEach((col) => { From d1e4ab73e34847b3c069b75640529db40cf3271f Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 16 Sep 2025 11:25:10 -0400 Subject: [PATCH 004/112] add parameter helperText --- src/components/CippFormPages/CippSchedulerForm.jsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/CippFormPages/CippSchedulerForm.jsx b/src/components/CippFormPages/CippSchedulerForm.jsx index aa8350f3a0f1..049a554b8626 100644 --- a/src/components/CippFormPages/CippSchedulerForm.jsx +++ b/src/components/CippFormPages/CippSchedulerForm.jsx @@ -403,12 +403,14 @@ const CippSchedulerForm = (props) => { name={`parameters.${param.Name}`} label={param.Name} formControl={formControl} + helperText={param.Description} /> ) : param.Type === "System.Collections.Hashtable" ? ( ) : param.Type?.startsWith("System.String") ? ( @@ -418,6 +420,7 @@ const CippSchedulerForm = (props) => { label={param.Name} formControl={formControl} placeholder={`Enter a value for ${param.Name}`} + helperText={param.Description} validators={fieldRequired(param)} required={param.Required} /> @@ -428,6 +431,7 @@ const CippSchedulerForm = (props) => { label={param.Name} formControl={formControl} placeholder={`Enter a value for ${param.Name}`} + helperText={param.Description} validators={fieldRequired(param)} required={param.Required} /> From b952c55dba7010a9e18b69a5a99694a4593ea0aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Tue, 16 Sep 2025 15:38:13 +0200 Subject: [PATCH 005/112] Add Avg CPU Load Factor and Cloud Block Level fields to DeployDefenderForm --- .../security/defender/deployment/index.js | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/pages/security/defender/deployment/index.js b/src/pages/security/defender/deployment/index.js index c429c3107e45..5562cc2cacce 100644 --- a/src/pages/security/defender/deployment/index.js +++ b/src/pages/security/defender/deployment/index.js @@ -217,6 +217,17 @@ const DeployDefenderForm = () => { name="Policy.LowCPU" formControl={formControl} /> + { name="Policy.DisableCatchupQuickScan" formControl={formControl} /> + + {/* Assign to Group */} From 7ca5d99a7717731aad79de8c447ac78cb26b5c7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Tue, 16 Sep 2025 17:00:27 +0200 Subject: [PATCH 006/112] Add Cloud Extended Timeout field and adjust spacing in DeployDefenderForm --- .../security/defender/deployment/index.js | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/pages/security/defender/deployment/index.js b/src/pages/security/defender/deployment/index.js index 5562cc2cacce..48b6270bca9b 100644 --- a/src/pages/security/defender/deployment/index.js +++ b/src/pages/security/defender/deployment/index.js @@ -227,6 +227,7 @@ const DeployDefenderForm = () => { min: { value: 0, message: "Value must be at least 0" }, max: { value: 100, message: "Value cannot exceed 100" }, }} + sx={{ my: 1 }} /> @@ -296,9 +297,20 @@ const DeployDefenderForm = () => { { label: "Zero Tolerance", value: "6" }, ]} formControl={formControl} - validators={{}} + sx={{ my: 1 }} + /> + - {/* Assign to Group */} @@ -572,8 +584,6 @@ const DeployDefenderForm = () => { - - {/* Remove the Review and Confirm section as per your request */} ); From c7b198161ae5acc9a7bd95387035567a9754d7ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Tue, 16 Sep 2025 17:19:30 +0200 Subject: [PATCH 007/112] fix network protection audit mode --- .../security/defender/deployment/index.js | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/pages/security/defender/deployment/index.js b/src/pages/security/defender/deployment/index.js index 48b6270bca9b..d357f43e66ed 100644 --- a/src/pages/security/defender/deployment/index.js +++ b/src/pages/security/defender/deployment/index.js @@ -255,16 +255,18 @@ const DeployDefenderForm = () => { name="Policy.AllowUI" formControl={formControl} /> + - Date: Tue, 16 Sep 2025 17:28:47 +0200 Subject: [PATCH 008/112] Add Signature Update Interval field to DeployDefenderForm --- src/pages/security/defender/deployment/index.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/pages/security/defender/deployment/index.js b/src/pages/security/defender/deployment/index.js index d357f43e66ed..8d1dda2c4c5a 100644 --- a/src/pages/security/defender/deployment/index.js +++ b/src/pages/security/defender/deployment/index.js @@ -275,6 +275,18 @@ const DeployDefenderForm = () => { name="Policy.CheckSigs" formControl={formControl} /> + Date: Tue, 16 Sep 2025 17:41:54 +0200 Subject: [PATCH 009/112] Add placeholder for Avg CPU Load Factor, Signature Update Interval, and Cloud Extended Timeout fields; introduce Allow Metered Connection Updates switch --- src/pages/security/defender/deployment/index.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/pages/security/defender/deployment/index.js b/src/pages/security/defender/deployment/index.js index 8d1dda2c4c5a..58d185c62fe2 100644 --- a/src/pages/security/defender/deployment/index.js +++ b/src/pages/security/defender/deployment/index.js @@ -222,13 +222,19 @@ const DeployDefenderForm = () => { label="Avg CPU Load Factor(%)" name="Policy.AvgCPULoadFactor" formControl={formControl} - defaultValue={50} + placeholder="50" validators={{ min: { value: 0, message: "Value must be at least 0" }, max: { value: 100, message: "Value cannot exceed 100" }, }} sx={{ my: 1 }} /> + { label="Signature Update Interval (hours)" name="Policy.SignatureUpdateInterval" formControl={formControl} - defaultValue={8} + placeholder="8" validators={{ min: { value: 0, message: "Value must be at least 0" }, max: { value: 24, message: "Value cannot exceed 24" }, @@ -318,7 +324,7 @@ const DeployDefenderForm = () => { label="Cloud Extended Timeout (seconds)" name="Policy.CloudExtendedTimeout" formControl={formControl} - defaultValue={50} + placeholder="0" validators={{ min: { value: 0, message: "Value must be at least 0" }, max: { value: 50, message: "Value cannot exceed 50" }, From 98774c3f57cf49a1393f19d499c9c0a8bf660571 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Tue, 16 Sep 2025 19:19:14 +0200 Subject: [PATCH 010/112] Add Allow On Access Protection and Disable Local Admin Merge fields to DeployDefenderForm --- .../security/defender/deployment/index.js | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/pages/security/defender/deployment/index.js b/src/pages/security/defender/deployment/index.js index 58d185c62fe2..4b780e25e8cc 100644 --- a/src/pages/security/defender/deployment/index.js +++ b/src/pages/security/defender/deployment/index.js @@ -235,6 +235,25 @@ const DeployDefenderForm = () => { name="Policy.MeteredConnectionUpdates" formControl={formControl} /> + + { label="Enable Network Protection" name="Policy.EnableNetworkProtection" multiple={false} + creatable={false} options={[ { label: "Disabled (Default)", value: "0" }, { label: "Enabled (block mode)", value: "1" }, @@ -309,6 +329,7 @@ const DeployDefenderForm = () => { type="autoComplete" label="Cloud Block Level" multiple={false} + creatable={false} name="Policy.CloudBlockLevel" options={[ { label: "Default", value: "0" }, From 564b3dbff3674ba7837b28593f48e8a0732d3e45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Tue, 16 Sep 2025 20:12:13 +0200 Subject: [PATCH 011/112] Add remediation action fields for varying threat severities to DeployDefenderForm Remove deprecated AllowIPS option Reordering of options --- .../security/defender/deployment/index.js | 223 ++++++++++++++---- 1 file changed, 173 insertions(+), 50 deletions(-) diff --git a/src/pages/security/defender/deployment/index.js b/src/pages/security/defender/deployment/index.js index 4b780e25e8cc..3ddb85e5d4cb 100644 --- a/src/pages/security/defender/deployment/index.js +++ b/src/pages/security/defender/deployment/index.js @@ -207,14 +207,20 @@ const DeployDefenderForm = () => { /> + { }} sx={{ my: 1 }} /> - { formControl={formControl} /> @@ -270,7 +279,7 @@ const DeployDefenderForm = () => { /> @@ -280,21 +289,6 @@ const DeployDefenderForm = () => { name="Policy.AllowUI" formControl={formControl} /> - - { name="Policy.DisableCatchupQuickScan" formControl={formControl} /> + + { formControl={formControl} sx={{ my: 1 }} /> + + + + + + + {/* Threat Remediation Actions Section */} + + + Threat Remediation Actions + + + + - - {/* Assign to Group */} - - Assign to Group + + + + + + {/* Assignment Section */} + + + Policy Assignment + + + From f6383a62a57e4419603212fcc04cc8fe711703e9 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 16 Sep 2025 20:54:33 -0400 Subject: [PATCH 012/112] fix numeric sorting --- src/components/CippTable/CippDataTable.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/components/CippTable/CippDataTable.js b/src/components/CippTable/CippDataTable.js index 51bfdca74610..51f0e3511bc9 100644 --- a/src/components/CippTable/CippDataTable.js +++ b/src/components/CippTable/CippDataTable.js @@ -459,6 +459,20 @@ export const CippDataTable = (props) => { } return aVal > bVal ? 1 : -1; }, + number: (a, b, id) => { + const aVal = a?.original?.[id] ?? null; + const bVal = b?.original?.[id] ?? null; + if (aVal === null && bVal === null) { + return 0; + } + if (aVal === null) { + return 1; + } + if (bVal === null) { + return -1; + } + return aVal - bVal; + }, }, filterFns: { notContains: (row, columnId, value) => { From 2ba54e9746c09767e0d014de7c400cd8b24710f4 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 16 Sep 2025 20:59:28 -0400 Subject: [PATCH 013/112] fix boolean sort --- src/utils/get-cipp-filter-variant.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/get-cipp-filter-variant.js b/src/utils/get-cipp-filter-variant.js index e60d933636e5..06d5dd3b83db 100644 --- a/src/utils/get-cipp-filter-variant.js +++ b/src/utils/get-cipp-filter-variant.js @@ -71,7 +71,7 @@ export const getCippFilterVariant = (providedColumnKeys, arg) => { case "accountEnabled": return { filterVariant: "select", - sortingFn: "boolean", + sortingFn: "alphanumeric", filterFn: "equals", }; case "primDomain": @@ -97,7 +97,7 @@ export const getCippFilterVariant = (providedColumnKeys, arg) => { if (typeOf === "boolean") { return { filterVariant: "select", - sortingFn: "boolean", + sortingFn: "alphanumeric", filterFn: "equals", }; } From 2d1006f77ede17f96ffe5809b3b55ca1edae3deb Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 18 Sep 2025 09:12:38 -0400 Subject: [PATCH 014/112] fix watcher for ooo --- src/components/CippComponents/CippUserActions.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/CippComponents/CippUserActions.jsx b/src/components/CippComponents/CippUserActions.jsx index d08c19ca69d0..b9fa60286717 100644 --- a/src/components/CippComponents/CippUserActions.jsx +++ b/src/components/CippComponents/CippUserActions.jsx @@ -31,7 +31,7 @@ const OutOfOfficeForm = ({ formControl }) => { // Watch the Auto Reply State value const autoReplyState = useWatch({ control: formControl.control, - name: "ooo.AutoReplyState", + name: "AutoReplyState", }); // Calculate if date fields should be disabled From 0272f96a7aedeeae18a1e404fef7d7c71eeb33fd Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 19 Sep 2025 15:24:39 +0200 Subject: [PATCH 015/112] Standards --- src/data/standards.json | 129 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) diff --git a/src/data/standards.json b/src/data/standards.json index 785cdcb8ef4c..e7cd47603a5a 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -4876,5 +4876,134 @@ "addedDate": "2025-08-26", "powershellEquivalent": "None", "recommendedBy": ["Microsoft"] + }, + { + "name": "standards.DeployCheckChromeExtension", + "cat": "Intune Standards", + "tag": [], + "helpText": "Deploys the Check Chrome extension via Intune OMA-URI custom policies for both Chrome and Edge browsers with configurable settings. Chrome ID: benimdeioplgkhanklclahllklceahbe, Edge ID: knepjpocdagponkonnbggpcnhnaikajg", + "docsDescription": "Creates Intune OMA-URI custom policies that automatically install and configure the Check Chrome extension on managed devices for both Google Chrome and Microsoft Edge browsers. This ensures the extension is deployed consistently across all corporate devices with customizable settings.", + "executiveText": "Automatically deploys the Check browser extension across all company devices with configurable security and branding settings, ensuring consistent security monitoring and compliance capabilities. This extension provides enhanced security features and monitoring tools that help protect against threats while maintaining user productivity.", + "addedComponent": [ + { + "type": "switch", + "name": "standards.DeployCheckChromeExtension.enableValidPageBadge", + "label": "Enable valid page badge", + "defaultValue": true + }, + { + "type": "switch", + "name": "standards.DeployCheckChromeExtension.enablePageBlocking", + "label": "Enable page blocking", + "defaultValue": true + }, + { + "type": "switch", + "name": "standards.DeployCheckChromeExtension.enableCippReporting", + "label": "Enable CIPP reporting", + "defaultValue": true + }, + { + "type": "textField", + "name": "standards.DeployCheckChromeExtension.cippServerUrl", + "label": "CIPP Server URL", + "placeholder": "https://YOUR-CIPP-SERVER-URL", + "required": false + }, + + { + "type": "textField", + "name": "standards.DeployCheckChromeExtension.customRulesUrl", + "label": "Custom Rules URL", + "placeholder": "https://YOUR-CIPP-SERVER-URL/rules.json", + "required": false + }, + { + "type": "number", + "name": "standards.DeployCheckChromeExtension.updateInterval", + "label": "Update interval (hours)", + "defaultValue": 12 + }, + { + "type": "switch", + "name": "standards.DeployCheckChromeExtension.enableDebugLogging", + "label": "Enable debug logging", + "defaultValue": false + }, + { + "type": "textField", + "name": "standards.DeployCheckChromeExtension.companyName", + "label": "Company Name", + "placeholder": "YOUR-COMPANY", + "required": false + }, + { + "type": "textField", + "name": "standards.DeployCheckChromeExtension.productName", + "label": "Product Name", + "placeholder": "YOUR-PRODUCT-NAME", + "required": false + }, + { + "type": "textField", + "name": "standards.DeployCheckChromeExtension.supportEmail", + "label": "Support Email", + "placeholder": "support@yourcompany.com", + "required": false + }, + { + "type": "textField", + "name": "standards.DeployCheckChromeExtension.primaryColor", + "label": "Primary Color", + "placeholder": "#0044CC", + "required": false + }, + { + "type": "textField", + "name": "standards.DeployCheckChromeExtension.logoUrl", + "label": "Logo URL", + "placeholder": "https://yourcompany.com/logo.png", + "required": false + }, + { + "name": "AssignTo", + "label": "Who should this policy be assigned to?", + "type": "radio", + "options": [ + { + "label": "Do not assign", + "value": "On" + }, + { + "label": "Assign to all users", + "value": "allLicensedUsers" + }, + { + "label": "Assign to all devices", + "value": "AllDevices" + }, + { + "label": "Assign to all users and devices", + "value": "AllDevicesAndUsers" + }, + { + "label": "Assign to Custom Group", + "value": "customGroup" + } + ] + }, + { + "type": "textField", + "required": false, + "name": "customGroup", + "label": "Enter the custom group name if you selected 'Assign to Custom Group'. Wildcards are allowed." + } + ], + "label": "Deploy Check Chrome Extension", + "impact": "Low Impact", + "impactColour": "info", + "addedDate": "2025-09-18", + "powershellEquivalent": "New-GraphPostRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/configurationPolicies'", + "recommendedBy": ["CIPP"] } ] From 4573a32e2636ac6c068895b45c603f27ce85744b Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sun, 21 Sep 2025 21:37:57 +0200 Subject: [PATCH 016/112] Backup in tenant overview information --- .../CippBackupScheduleDrawer.jsx | 271 ++++++++++++ .../CippRestoreBackupDrawer.jsx | 363 ++++++++++++++++ src/layouts/config.js | 12 - .../manage-drift/configuration-backup.js | 387 ++++++++++++++++++ .../standards/manage-drift/tabOptions.json | 6 +- 5 files changed, 1026 insertions(+), 13 deletions(-) create mode 100644 src/components/CippComponents/CippBackupScheduleDrawer.jsx create mode 100644 src/components/CippComponents/CippRestoreBackupDrawer.jsx create mode 100644 src/pages/tenant/standards/manage-drift/configuration-backup.js diff --git a/src/components/CippComponents/CippBackupScheduleDrawer.jsx b/src/components/CippComponents/CippBackupScheduleDrawer.jsx new file mode 100644 index 000000000000..e73a795715d4 --- /dev/null +++ b/src/components/CippComponents/CippBackupScheduleDrawer.jsx @@ -0,0 +1,271 @@ +import { useState, useEffect } from "react"; +import { Button, Box, Typography, Alert, AlertTitle } from "@mui/material"; +import { Grid } from "@mui/system"; +import { useForm, useFormState } from "react-hook-form"; +import { Backup } from "@mui/icons-material"; +import { CippOffCanvas } from "./CippOffCanvas"; +import CippFormComponent from "./CippFormComponent"; +import { CippFormTenantSelector } from "./CippFormTenantSelector"; +import { CippApiResults } from "./CippApiResults"; +import { useSettings } from "../../hooks/use-settings"; +import { ApiPostCall } from "../../api/ApiCall"; +import { omit } from "lodash"; + +export const CippBackupScheduleDrawer = ({ + buttonText = "Add Backup Schedule", + requiredPermissions = [], + PermissionButton = Button, +}) => { + const [drawerVisible, setDrawerVisible] = useState(false); + const userSettingsDefaults = useSettings(); + + const formControl = useForm({ + mode: "onBlur", + defaultValues: { + tenantFilter: userSettingsDefaults.currentTenant, + users: true, + groups: true, + ca: true, + intuneconfig: true, + intunecompliance: true, + intuneprotection: true, + antispam: true, + antiphishing: true, + CippWebhookAlerts: true, + CippScriptedAlerts: true, + }, + }); + + const createBackup = ApiPostCall({ + urlFromData: true, + relatedQueryKeys: ["BackupList", "BackupTasks"], + }); + + const { isValid, isDirty } = useFormState({ control: formControl.control }); + + useEffect(() => { + if (createBackup.isSuccess) { + formControl.reset({ + tenantFilter: userSettingsDefaults.currentTenant, + users: true, + groups: true, + ca: true, + intuneconfig: true, + intunecompliance: true, + intuneprotection: true, + antispam: true, + antiphishing: true, + CippWebhookAlerts: true, + CippScriptedAlerts: true, + }); + } + }, [createBackup.isSuccess]); + + const handleSubmit = () => { + formControl.trigger(); + if (!isValid) { + return; + } + const values = formControl.getValues(); + const startDate = new Date(); + startDate.setHours(0, 0, 0, 0); + const unixTime = Math.floor(startDate.getTime() / 1000) - 45; + const tenantFilter = values.tenantFilter || userSettingsDefaults.currentTenant; + + const shippedValues = { + TenantFilter: tenantFilter, + Name: `CIPP Backup - ${tenantFilter}`, + Command: { value: `New-CIPPBackup` }, + Parameters: { + backupType: "Scheduled", + ScheduledBackupValues: { ...omit(values, ["tenantFilter"]) }, + }, + ScheduledTime: unixTime, + Recurrence: { value: "1d" }, + }; + + createBackup.mutate({ + url: "/api/AddScheduledItem?hidden=true&DisallowDuplicateName=true", + data: shippedValues, + }); + }; + + const handleCloseDrawer = () => { + setDrawerVisible(false); + formControl.reset({ + tenantFilter: userSettingsDefaults.currentTenant, + users: true, + groups: true, + ca: true, + intuneconfig: true, + intunecompliance: true, + intuneprotection: true, + antispam: true, + antiphishing: true, + CippWebhookAlerts: true, + CippScriptedAlerts: true, + }); + }; + + return ( + <> + setDrawerVisible(true)} + startIcon={} + > + {buttonText} + + + + + + } + > + + + Backup Schedule Information + Create a scheduled backup task that will automatically backup your tenant configuration. + Backups are stored securely and can be restored using the restore functionality. + + + + + Tenant Selection + + + + + Identity + + + + + + + + + + Conditional Access + + + + + + + + Intune + + + + + + + + + + + + + + Email Security + + + + + + + + + + CIPP + + + + + + + + + + + + + ); +}; diff --git a/src/components/CippComponents/CippRestoreBackupDrawer.jsx b/src/components/CippComponents/CippRestoreBackupDrawer.jsx new file mode 100644 index 000000000000..56a0600e6d29 --- /dev/null +++ b/src/components/CippComponents/CippRestoreBackupDrawer.jsx @@ -0,0 +1,363 @@ +import React, { useState, useEffect } from "react"; +import { Button, Box, Typography, Alert, AlertTitle, Divider, Chip, Stack } from "@mui/material"; +import { Grid } from "@mui/system"; +import { useForm, useFormState } from "react-hook-form"; +import { SettingsBackupRestore } from "@mui/icons-material"; +import { CippOffCanvas } from "./CippOffCanvas"; +import CippFormComponent from "./CippFormComponent"; +import { CippFormCondition } from "./CippFormCondition"; +import { CippApiResults } from "./CippApiResults"; +import { useSettings } from "../../hooks/use-settings"; +import { ApiPostCall } from "../../api/ApiCall"; + +export const CippRestoreBackupDrawer = ({ + buttonText = "Restore Backup", + backupName = null, + requiredPermissions = [], + PermissionButton = Button, + ...props +}) => { + const [drawerVisible, setDrawerVisible] = useState(false); + const userSettingsDefaults = useSettings(); + const tenantFilter = userSettingsDefaults.currentTenant || ""; + + const formControl = useForm({ + mode: "onChange", + defaultValues: { + tenantFilter: tenantFilter, + users: true, + groups: true, + ca: true, + intuneconfig: true, + intunecompliance: true, + intuneprotection: true, + antispam: true, + antiphishing: true, + CippWebhookAlerts: true, + CippScriptedAlerts: true, + CippStandards: true, + overwrite: false, + webhook: false, + email: false, + psa: false, + backup: backupName ? { value: backupName, label: backupName } : null, + }, + }); + + const restoreBackup = ApiPostCall({ + urlFromData: true, + relatedQueryKeys: ["BackupList", "BackupTasks"], + }); + + const { isValid, isDirty } = useFormState({ control: formControl.control }); + + useEffect(() => { + if (restoreBackup.isSuccess) { + formControl.reset({ + tenantFilter: tenantFilter, + users: true, + groups: true, + ca: true, + intuneconfig: true, + intunecompliance: true, + intuneprotection: true, + antispam: true, + antiphishing: true, + CippWebhookAlerts: true, + CippScriptedAlerts: true, + CippStandards: true, + overwrite: false, + webhook: false, + email: false, + psa: false, + backup: backupName ? { value: backupName, label: backupName } : null, + }); + } + }, [restoreBackup.isSuccess]); + + const handleSubmit = () => { + formControl.trigger(); + if (!isValid) { + return; + } + const values = formControl.getValues(); + const startDate = new Date(); + const unixTime = Math.floor(startDate.getTime() / 1000) - 45; + const tenantFilterValue = tenantFilter; + + const shippedValues = { + TenantFilter: tenantFilterValue, + Name: `CIPP Restore ${tenantFilterValue}`, + Command: { value: `New-CIPPRestore` }, + Parameters: { + Type: "Scheduled", + RestoreValues: { + backup: values.backup?.value || values.backup, + users: values.users, + groups: values.groups, + ca: values.ca, + intuneconfig: values.intuneconfig, + intunecompliance: values.intunecompliance, + intuneprotection: values.intuneprotection, + antispam: values.antispam, + antiphishing: values.antiphishing, + CippWebhookAlerts: values.CippWebhookAlerts, + CippScriptedAlerts: values.CippScriptedAlerts, + overwrite: values.overwrite, + }, + }, + ScheduledTime: unixTime, + PostExecution: { + Webhook: values.webhook, + Email: values.email, + PSA: values.psa, + }, + DisallowDuplicateName: true, + }; + + restoreBackup.mutate({ + url: "/api/AddScheduledItem", + data: shippedValues, + }); + }; + + const handleCloseDrawer = () => { + setDrawerVisible(false); + formControl.reset({ + tenantFilter: tenantFilter, + users: true, + groups: true, + ca: true, + intuneconfig: true, + intunecompliance: true, + intuneprotection: true, + antispam: true, + antiphishing: true, + CippWebhookAlerts: true, + CippScriptedAlerts: true, + CippStandards: true, + overwrite: false, + webhook: false, + email: false, + psa: false, + backup: backupName ? { value: backupName, label: backupName } : null, + }); + }; + + return ( + <> + setDrawerVisible(true)} + startIcon={} + {...props} + > + {buttonText} + + + + + + } + > + + + Use this form to restore a backup for a tenant. Please select the backup and restore + options. + + + + {/* Backup Selector */} + + { + 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; + }, + valueField: "BackupName", + data: { + Type: "Scheduled", + NameOnly: true, + tenantFilter: tenantFilter, + }, + }} + formControl={formControl} + required={true} + validators={{ + validate: (value) => !!value || "Please select a backup", + }} + /> + + + {/* Restore Settings */} + + Restore Settings + + + {/* Identity */} + + Identity + + + + + {/* Conditional Access */} + + Conditional Access + + + + {/* Intune */} + + Intune + + + + + + {/* Email Security */} + + Email Security + + + + + {/* CIPP */} + + CIPP + + + + + {/* Overwrite Existing Entries */} + + + + + + Warning: Overwriting existing entries will remove the current + settings and replace them with the backup settings. If you have selected to + restore users, all properties will be overwritten with the backup settings. To + prevent and skip already existing entries, deselect the setting from the list + above, or disable overwrite. + + + + + + {/* Send Results To */} + + Send Restore results to: + + + + + + + + + + + + + + + + ); +}; diff --git a/src/layouts/config.js b/src/layouts/config.js index 2add01700bb5..879244bf207f 100644 --- a/src/layouts/config.js +++ b/src/layouts/config.js @@ -183,18 +183,6 @@ export const nativeMenuItems = [ path: "/tenant/gdap-management/", permissions: ["Tenant.Relationship.*"], }, - { - title: "Configuration Backup", - path: "/tenant/backup", - permissions: ["CIPP.Backup.*"], - items: [ - { - title: "Backups", - path: "/tenant/backup/backup-wizard", - permissions: ["CIPP.Backup.*"], - }, - ], - }, { title: "Standards & Drift", path: "/tenant/standards", diff --git a/src/pages/tenant/standards/manage-drift/configuration-backup.js b/src/pages/tenant/standards/manage-drift/configuration-backup.js new file mode 100644 index 000000000000..f3db1903ce80 --- /dev/null +++ b/src/pages/tenant/standards/manage-drift/configuration-backup.js @@ -0,0 +1,387 @@ +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { HeaderedTabbedLayout } from "/src/layouts/HeaderedTabbedLayout"; +import { + Button, + Box, + Typography, + Alert, + AlertTitle, + Card, + CardContent, + Stack, + Skeleton, + Chip, +} from "@mui/material"; +import { Grid } from "@mui/system"; +import { + Storage, + History, + EventRepeat, + Schedule, + RestoreFromTrash, + SettingsBackupRestore, + Settings, + CheckCircle, + Cancel, + Delete, +} from "@mui/icons-material"; +import { useSettings } from "/src/hooks/use-settings"; +import { ApiGetCall } from "/src/api/ApiCall"; +import { CippPropertyListCard } from "/src/components/CippCards/CippPropertyListCard"; +import { CippBackupScheduleDrawer } from "/src/components/CippComponents/CippBackupScheduleDrawer"; +import { CippRestoreBackupDrawer } from "/src/components/CippComponents/CippRestoreBackupDrawer"; +import { CippApiDialog } from "/src/components/CippComponents/CippApiDialog"; +import { CippTimeAgo } from "/src/components/CippComponents/CippTimeAgo"; +import { useDialog } from "/src/hooks/use-dialog"; +import ReactTimeAgo from "react-time-ago"; +import tabOptions from "./tabOptions.json"; +import { useRouter } from "next/router"; +import { CippHead } from "/src/components/CippComponents/CippHead"; + +const Page = () => { + const router = useRouter(); + const { templateId } = router.query; + const settings = useSettings(); + const removeDialog = useDialog(); + + // API call to get backup files + const backupList = ApiGetCall({ + url: "/api/ExecListBackup", + data: { + tenantFilter: settings.currentTenant, + Type: "Scheduled", + NameOnly: true, + }, + queryKey: "BackupList", + }); + + // API call to get existing backup configuration/schedule + const existingBackupConfig = ApiGetCall({ + url: "/api/ListScheduledItems", + data: { + showHidden: true, + Type: "New-CIPPBackup", + }, + queryKey: "BackupTasks", + }); + + // Use the actual backup files as the backup data + const filteredBackupData = backupList.data || []; + // Generate backup tags from actual API response items - use raw items directly + const generateBackupTags = (backup) => { + // Use the Items array directly from the API response without any translation + if (backup.Items && Array.isArray(backup.Items)) { + return backup.Items; + } + + // Fallback if no items found + return ["Configuration"]; + }; + + const backupDisplayItems = filteredBackupData.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, + tags: generateBackupTags(backup), + })); + + // Process existing backup configuration + const currentConfig = existingBackupConfig.data?.[0]; + const hasExistingConfig = currentConfig && currentConfig.Parameters?.ScheduledBackupValues; + + // Create property items for current configuration + const configPropertyItems = hasExistingConfig + ? [ + { label: "Backup Name", value: currentConfig.Name }, + { + label: "Tenant", + value: + currentConfig.Tenant?.value || + currentConfig.Tenant || + currentConfig.TenantFilter || + settings.currentTenant, + }, + { label: "Recurrence", value: currentConfig.Recurrence?.value || "Daily" }, + { label: "Task State", value: currentConfig.TaskState || "Unknown" }, + { + label: "Last Executed", + value: currentConfig.ExecutedTime ? ( + + ) : ( + "Never" + ), + }, + { + label: "Next Run", + value: currentConfig.ScheduledTime ? ( + + ) : ( + "Not scheduled" + ), + }, + ] + : []; + + // Create component status tags + const getEnabledComponents = () => { + if (!hasExistingConfig) return []; + + const values = currentConfig.Parameters.ScheduledBackupValues; + const enabledComponents = []; + + if (values.users) enabledComponents.push("Users"); + if (values.groups) enabledComponents.push("Groups"); + if (values.ca) enabledComponents.push("Conditional Access"); + if (values.intuneconfig) enabledComponents.push("Intune Configuration"); + if (values.intunecompliance) enabledComponents.push("Intune Compliance"); + if (values.intuneprotection) enabledComponents.push("Intune Protection"); + if (values.antispam) enabledComponents.push("Anti-Spam"); + if (values.antiphishing) enabledComponents.push("Anti-Phishing"); + if (values.CippWebhookAlerts) enabledComponents.push("CIPP Webhook Alerts"); + if (values.CippScriptedAlerts) enabledComponents.push("CIPP Scripted Alerts"); + + return enabledComponents; + }; + + // Info bar data following CIPP patterns + const infoBarData = [ + { + icon: , + name: "Total Backups", + data: filteredBackupData?.length || 0, + }, + { + icon: , + name: "Last Backup", + data: filteredBackupData?.[0]?.Timestamp ? ( + + ) : ( + "No Backups" + ), + }, + { + icon: , + name: "Tenant Scope", + data: settings.currentTenant === "AllTenants" ? "All Tenants" : settings.currentTenant, + }, + { + icon: , + name: "Configuration", + data: hasExistingConfig ? "Configured" : "Not Configured", + }, + ]; + + const title = "Manage Drift"; + const subtitle = [ + { + text: `Template ID: ${templateId || "Loading..."}`, + }, + ]; + + return ( + + + + + {/* Two Side-by-Side Displays */} + + + {/* Current Configuration Header */} + + + + + + Current Configuration + + {!hasExistingConfig ? ( + + ) : ( + + )} + + + + + {/* Configuration Details */} + {existingBackupConfig.isFetching ? ( + + + + + ) : hasExistingConfig ? ( + + + + + + Backup Components + + + {getEnabledComponents().map((component, idx) => ( + } + /> + ))} + {getEnabledComponents().length === 0 && ( + } + /> + )} + + + + + ) : ( + + No Backup Configuration + No backup schedule is currently configured for{" "} + {settings.currentTenant === "AllTenants" ? "any tenant" : settings.currentTenant}. + Click "Add Backup Schedule" to create an automated backup configuration. + + )} + + + + + {/* Backup History */} + + + + + + Backup History + + + + {settings.currentTenant === "AllTenants" + ? "Viewing backups for all tenants." + : `Viewing backups for ${settings.currentTenant} and global backups.`} + + + {filteredBackupData.length === 0 && !backupList.isFetching ? ( + + No Backup History + {settings.currentTenant === "AllTenants" + ? "No backups exist for any tenant." + : `No backups found for ${settings.currentTenant}.`} + + ) : backupList.isFetching ? ( + + + + ) : ( + + + {backupDisplayItems.map((backup) => ( + + + + + + + {(() => { + const match = backup.name.match( + /.*_(\d{4}-\d{2}-\d{2})-(\d{2})(\d{2})/ + ); + return match + ? `${match[1]} @ ${match[2]}:${match[3]}` + : backup.name; + })()} + + + + + + + } + /> + + + + {backup.tags.map((tag, idx) => ( + + ))} + + + + + ))} + + + )} + + + + + + + + {/* Remove Backup Schedule Dialog */} + + + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; diff --git a/src/pages/tenant/standards/manage-drift/tabOptions.json b/src/pages/tenant/standards/manage-drift/tabOptions.json index 50b3adfd16dc..cb350fb8352a 100644 --- a/src/pages/tenant/standards/manage-drift/tabOptions.json +++ b/src/pages/tenant/standards/manage-drift/tabOptions.json @@ -12,7 +12,11 @@ "path": "/tenant/standards/manage-drift/history" }, { - "label": "Tenant Report", + "label": "Applied Standards Report", "path": "/tenant/standards/manage-drift/compare" + }, + { + "label": "Configuration Backup", + "path": "/tenant/standards/manage-drift/configuration-backup" } ] From ba37ea1a023ec5783935c4fa89ccf443e575bce6 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sun, 21 Sep 2025 22:19:41 +0200 Subject: [PATCH 017/112] date correction --- .../tenant/standards/manage-drift/configuration-backup.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/tenant/standards/manage-drift/configuration-backup.js b/src/pages/tenant/standards/manage-drift/configuration-backup.js index f3db1903ce80..007563772db1 100644 --- a/src/pages/tenant/standards/manage-drift/configuration-backup.js +++ b/src/pages/tenant/standards/manage-drift/configuration-backup.js @@ -52,7 +52,7 @@ const Page = () => { Type: "Scheduled", NameOnly: true, }, - queryKey: "BackupList", + queryKey: `BackupList-${settings.currentTenant}`, }); // API call to get existing backup configuration/schedule @@ -117,7 +117,7 @@ const Page = () => { { label: "Next Run", value: currentConfig.ScheduledTime ? ( - + ) : ( "Not scheduled" ), From 9afc997b159aa02d56d45466d94eb0177f52377c Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Mon, 22 Sep 2025 00:42:27 +0200 Subject: [PATCH 018/112] Fixing tenant --- .../manage-drift/configuration-backup.js | 2 +- .../standards/manage-drift/edit-tenant.js | 342 ++++++++++++++++++ .../standards/manage-drift/tabOptions.json | 20 +- 3 files changed, 355 insertions(+), 9 deletions(-) create mode 100644 src/pages/tenant/standards/manage-drift/edit-tenant.js diff --git a/src/pages/tenant/standards/manage-drift/configuration-backup.js b/src/pages/tenant/standards/manage-drift/configuration-backup.js index 007563772db1..b65f6b4459ea 100644 --- a/src/pages/tenant/standards/manage-drift/configuration-backup.js +++ b/src/pages/tenant/standards/manage-drift/configuration-backup.js @@ -192,7 +192,7 @@ const Page = () => { isFetching={backupList.isFetching || existingBackupConfig.isFetching} > - + {/* Two Side-by-Side Displays */} diff --git a/src/pages/tenant/standards/manage-drift/edit-tenant.js b/src/pages/tenant/standards/manage-drift/edit-tenant.js new file mode 100644 index 000000000000..e54dec9a3e6f --- /dev/null +++ b/src/pages/tenant/standards/manage-drift/edit-tenant.js @@ -0,0 +1,342 @@ +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { HeaderedTabbedLayout } from "/src/layouts/HeaderedTabbedLayout"; +import { useForm, useFormState } from "react-hook-form"; +import { ApiGetCall, ApiPostCall } from "/src/api/ApiCall"; +import { useEffect } from "react"; +import { useRouter } from "next/router"; +import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; +import { Stack, Box, Typography, Button, Card, CardContent } from "@mui/material"; +import { Grid } from "@mui/system"; +import { CippPropertyListCard } from "/src/components/CippCards/CippPropertyListCard"; +import CippButtonCard from "/src/components/CippCards/CippButtonCard"; +import { getCippFormatting } from "/src/utils/get-cipp-formatting"; +import CippCustomVariables from "/src/components/CippComponents/CippCustomVariables"; +import { CippOffboardingDefaultSettings } from "/src/components/CippComponents/CippOffboardingDefaultSettings"; +import { CippApiResults } from "/src/components/CippComponents/CippApiResults"; +import { useSettings } from "/src/hooks/use-settings"; +import { Business, Save } from "@mui/icons-material"; +import tabOptions from "./tabOptions.json"; +import { CippHead } from "/src/components/CippComponents/CippHead"; + +const Page = () => { + const router = useRouter(); + const { templateId } = router.query; + const settings = useSettings(); + const currentTenant = settings.currentTenant; + + const formControl = useForm({ + mode: "onChange", + }); + + const offboardingFormControl = useForm({ + mode: "onChange", + }); + + // API call for updating tenant properties + const updateTenant = ApiPostCall({ + urlFromData: true, + relatedQueryKeys: [ + `TenantProperties_${currentTenant}`, + "ListTenants-notAllTenants", + "TenantSelector", + ], + }); + + // API call for updating offboarding defaults + const updateOffboardingDefaults = ApiPostCall({ + urlFromData: true, + relatedQueryKeys: [`TenantProperties_${currentTenant}`], + }); + + const { isValid: isFormValid } = useFormState({ control: formControl.control }); + const { isValid: isOffboardingFormValid } = useFormState({ + control: offboardingFormControl.control, + }); + + const tenantDetails = ApiGetCall({ + url: + currentTenant && currentTenant !== "AllTenants" + ? `/api/ListTenantDetails?tenantFilter=${currentTenant}` + : null, + queryKey: + currentTenant && currentTenant !== "AllTenants" ? `TenantProperties_${currentTenant}` : null, + }); + + useEffect(() => { + if (tenantDetails.isSuccess && tenantDetails.data && currentTenant !== "AllTenants") { + formControl.reset({ + customerId: currentTenant, + Alias: tenantDetails?.data?.customProperties?.Alias ?? "", + Groups: + tenantDetails.data.Groups?.map((group) => ({ + label: group.Name, + value: group.Id, + })) || [], + }); + + // Set up offboarding defaults with default values + const tenantOffboardingDefaults = tenantDetails.data?.customProperties?.OffboardingDefaults; + const defaultOffboardingValues = { + ConvertToShared: false, + RemoveGroups: false, + HideFromGAL: false, + RemoveLicenses: false, + removeCalendarInvites: false, + RevokeSessions: false, + removePermissions: false, + RemoveRules: false, + ResetPass: false, + KeepCopy: false, + DeleteUser: false, + RemoveMobile: false, + DisableSignIn: false, + RemoveMFADevices: false, + RemoveTeamsPhoneDID: false, + ClearImmutableId: false, + }; + + let offboardingDefaults = {}; + + if (tenantOffboardingDefaults) { + try { + const parsed = JSON.parse(tenantOffboardingDefaults); + offboardingDefaults = { + offboardingDefaults: { ...defaultOffboardingValues, ...parsed }, + }; + } catch { + offboardingDefaults = { offboardingDefaults: defaultOffboardingValues }; + } + } else { + offboardingDefaults = { offboardingDefaults: defaultOffboardingValues }; + } + + offboardingFormControl.reset(offboardingDefaults); + } + }, [tenantDetails.isSuccess, tenantDetails.data, currentTenant]); + + const handleResetOffboardingDefaults = () => { + const defaultOffboardingValues = { + ConvertToShared: false, + RemoveGroups: false, + HideFromGAL: false, + RemoveLicenses: false, + removeCalendarInvites: false, + RevokeSessions: false, + removePermissions: false, + RemoveRules: false, + ResetPass: false, + KeepCopy: false, + DeleteUser: false, + RemoveMobile: false, + DisableSignIn: false, + RemoveMFADevices: false, + RemoveTeamsPhoneDID: false, + ClearImmutableId: false, + }; + + offboardingFormControl.reset({ offboardingDefaults: defaultOffboardingValues }); + }; + + const title = "Manage Tenant"; + + // Show message for AllTenants + if (currentTenant === "AllTenants") { + return ( + + + + + + + + + Select a Specific Tenant + + + Tenant editing is not available when "All Tenants" is selected. Please select a + specific tenant to edit its configuration. + + + + + + + ); + } + + return ( + + + + + {/* First Row - Tenant Details and Edit Form */} + + + + + + } + onClick={formControl.handleSubmit((values) => { + const formattedValues = { + tenantAlias: values.Alias, + tenantGroups: values.Groups.map((group) => ({ + groupId: group.value, + groupName: group.label, + })), + customerId: currentTenant, + }; + updateTenant.mutate({ + url: "/api/EditTenant", + data: formattedValues, + }); + })} + disabled={updateTenant.isPending || !isFormValid || tenantDetails.isFetching} + > + {updateTenant.isPending ? "Saving..." : "Save Changes"} + + } + isFetching={tenantDetails.isFetching} + > + + + + + + + + + {/* Second Row - Offboarding Defaults and Custom Variables */} + + } + onClick={offboardingFormControl.handleSubmit((values) => { + const offboardingSettings = values.offboardingDefaults || values; + const formattedValues = { + customerId: currentTenant, + offboardingDefaults: offboardingSettings, + }; + updateOffboardingDefaults.mutate({ + url: "/api/EditTenantOffboardingDefaults", + data: formattedValues, + }); + })} + disabled={ + updateOffboardingDefaults.isPending || + !isOffboardingFormValid || + tenantDetails.isFetching + } + > + {updateOffboardingDefaults.isPending ? "Saving..." : "Save Changes"} + + } + isFetching={tenantDetails.isFetching} + > + + + Configure default offboarding settings specifically for this tenant. These + settings will override user defaults when offboarding users in this tenant. + + + + + + + + Click "Reset All to Off" to turn off all options, then click "Save" to clear + tenant defaults. + + + + + + + + + + + + + Custom Variables + + + + + + + + + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; diff --git a/src/pages/tenant/standards/manage-drift/tabOptions.json b/src/pages/tenant/standards/manage-drift/tabOptions.json index cb350fb8352a..53417621e322 100644 --- a/src/pages/tenant/standards/manage-drift/tabOptions.json +++ b/src/pages/tenant/standards/manage-drift/tabOptions.json @@ -1,22 +1,26 @@ [ { - "label": "Manage Drift", - "path": "/tenant/standards/manage-drift" + "label": "Edit Tenant", + "path": "/tenant/standards/manage-drift/edit-tenant" }, { - "label": "Policies and Settings Deployed", - "path": "/tenant/standards/manage-drift/policies-deployed" + "label": "Configuration Backup", + "path": "/tenant/standards/manage-drift/configuration-backup" }, { - "label": "History", - "path": "/tenant/standards/manage-drift/history" + "label": "Manage Drift", + "path": "/tenant/standards/manage-drift" }, { "label": "Applied Standards Report", "path": "/tenant/standards/manage-drift/compare" }, { - "label": "Configuration Backup", - "path": "/tenant/standards/manage-drift/configuration-backup" + "label": "Policies and Settings Deployed", + "path": "/tenant/standards/manage-drift/policies-deployed" + }, + { + "label": "History", + "path": "/tenant/standards/manage-drift/history" } ] From 93c4a4586ca66826093be79cd2ef01f87ce416cf Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Mon, 22 Sep 2025 00:47:18 +0200 Subject: [PATCH 019/112] names --- .../standards/manage-drift/configuration-backup.js | 9 +-------- src/pages/tenant/standards/manage-drift/history.js | 10 +--------- .../tenant/standards/manage-drift/policies-deployed.js | 4 ++-- 3 files changed, 4 insertions(+), 19 deletions(-) diff --git a/src/pages/tenant/standards/manage-drift/configuration-backup.js b/src/pages/tenant/standards/manage-drift/configuration-backup.js index b65f6b4459ea..aa3644bbc21a 100644 --- a/src/pages/tenant/standards/manage-drift/configuration-backup.js +++ b/src/pages/tenant/standards/manage-drift/configuration-backup.js @@ -18,7 +18,6 @@ import { History, EventRepeat, Schedule, - RestoreFromTrash, SettingsBackupRestore, Settings, CheckCircle, @@ -174,18 +173,12 @@ const Page = () => { }, ]; - const title = "Manage Drift"; - const subtitle = [ - { - text: `Template ID: ${templateId || "Loading..."}`, - }, - ]; + const title = "Manage Backups"; return ( { currentTenant: tenant, }); - const title = "Manage Drift"; - const subtitle = [ - { - icon: , - text: `Template ID: ${templateId || "Loading..."}`, - }, - ]; - + const title = "View History"; // Sort logs by date (newest first) const sortedLogs = logsData.data ? [...logsData.data].sort((a, b) => new Date(b.DateTime) - new Date(a.DateTime)) @@ -150,7 +143,6 @@ const Page = () => { { }, currentTenant, }); - const title = "Manage Drift"; + const title = "View Deployed Policies"; const subtitle = [ { icon: , - text: `Template ID: ${templateId || "Loading..."}`, + text: `These are the policies deployed via a standard`, }, ]; From 742b54e08a4fd7345c7b99b1c8e42d2222deb5eb Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Mon, 22 Sep 2025 01:02:37 +0200 Subject: [PATCH 020/112] find bug --- .../tenant/standards/manage-drift/configuration-backup.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/pages/tenant/standards/manage-drift/configuration-backup.js b/src/pages/tenant/standards/manage-drift/configuration-backup.js index aa3644bbc21a..7439404b57b4 100644 --- a/src/pages/tenant/standards/manage-drift/configuration-backup.js +++ b/src/pages/tenant/standards/manage-drift/configuration-backup.js @@ -61,7 +61,7 @@ const Page = () => { showHidden: true, Type: "New-CIPPBackup", }, - queryKey: "BackupTasks", + queryKey: `BackupTasks-${settings.currentTenant}`, }); // Use the actual backup files as the backup data @@ -87,8 +87,10 @@ const Page = () => { tags: generateBackupTags(backup), })); - // Process existing backup configuration - const currentConfig = existingBackupConfig.data?.[0]; + // Process existing backup configuration, find tenantFilter. by comparing settings.currentTenant with Tenant.value + const currentConfig = existingBackupConfig.data?.find( + (tenant) => tenant.Tenant.value === settings.currentTenant + ); const hasExistingConfig = currentConfig && currentConfig.Parameters?.ScheduledBackupValues; // Create property items for current configuration From 342ef3f94cef483d16a418c8d528dab8f70f326f Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Mon, 22 Sep 2025 01:14:01 +0200 Subject: [PATCH 021/112] links improvements --- src/pages/tenant/administration/tenants/index.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/pages/tenant/administration/tenants/index.js b/src/pages/tenant/administration/tenants/index.js index f3688b658de0..4d386c1c5eca 100644 --- a/src/pages/tenant/administration/tenants/index.js +++ b/src/pages/tenant/administration/tenants/index.js @@ -26,7 +26,12 @@ const Page = () => { const actions = [ { label: "Edit Tenant", - link: "/tenant/administration/tenants/edit?id=[customerId]", + link: "/tenant/standards/manage-drift/edit-tenant?tenantFilter=[defaultDomainName]", + icon: , + }, + { + label: "Configure Backup", + link: "/tenant/standards/manage-drift/configuration-backup?tenantFilter=[defaultDomainName]", icon: , }, ]; From e66230c05a55260890209bdea1b07cd0ec1e47c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Mon, 22 Sep 2025 11:49:02 +0200 Subject: [PATCH 022/112] Feat: Add "None" option to permission levels in calendar and contact dialogs --- src/components/CippComponents/CippCalendarPermissionsDialog.jsx | 1 + src/components/CippComponents/CippContactPermissionsDialog.jsx | 1 + 2 files changed, 2 insertions(+) diff --git a/src/components/CippComponents/CippCalendarPermissionsDialog.jsx b/src/components/CippComponents/CippCalendarPermissionsDialog.jsx index b8c63e2994d2..8cc9ad829ae3 100644 --- a/src/components/CippComponents/CippCalendarPermissionsDialog.jsx +++ b/src/components/CippComponents/CippCalendarPermissionsDialog.jsx @@ -66,6 +66,7 @@ const CippCalendarPermissionsDialog = ({ formHook, combinedOptions, isUserGroupL { value: "Reviewer", label: "Reviewer" }, { value: "LimitedDetails", label: "Limited Details" }, { value: "AvailabilityOnly", label: "Availability Only" }, + { value: "None", label: "None" }, ]} multiple={false} formControl={formHook} diff --git a/src/components/CippComponents/CippContactPermissionsDialog.jsx b/src/components/CippComponents/CippContactPermissionsDialog.jsx index f3a8b17c9b35..ee554d6f2909 100644 --- a/src/components/CippComponents/CippContactPermissionsDialog.jsx +++ b/src/components/CippComponents/CippContactPermissionsDialog.jsx @@ -58,6 +58,7 @@ const CippContactPermissionsDialog = ({ formHook, combinedOptions, isUserGroupLo { value: "Reviewer", label: "Reviewer" }, { value: "LimitedDetails", label: "Limited Details" }, { value: "AvailabilityOnly", label: "Availability Only" }, + { value: "None", label: "None" }, ]} multiple={false} formControl={formHook} From 0f32a7bdff506cf3fc1ebba2e68c78cb977c2dcc Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Mon, 22 Sep 2025 12:53:26 +0200 Subject: [PATCH 023/112] catalog stuff --- .../CippComponents/CippPolicyImportDrawer.jsx | 16 +++++++++++++++- .../list-standards/classic-standards/index.js | 11 ++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/components/CippComponents/CippPolicyImportDrawer.jsx b/src/components/CippComponents/CippPolicyImportDrawer.jsx index 9ba29343b7da..bbe5b3a03dc4 100644 --- a/src/components/CippComponents/CippPolicyImportDrawer.jsx +++ b/src/components/CippComponents/CippPolicyImportDrawer.jsx @@ -47,6 +47,8 @@ export const CippPolicyImportDrawer = ({ url: mode === "ConditionalAccess" ? `/api/ListCATemplates?TenantFilter=${tenantFilter?.value || ""}` + : mode === "Standards" + ? `/api/listStandardTemplates?TenantFilter=${tenantFilter?.value || ""}` : `/api/ListIntunePolicy?type=ESP&TenantFilter=${tenantFilter?.value || ""}`, queryKey: `TenantPolicies-${mode}-${tenantFilter?.value || "none"}`, }); @@ -72,6 +74,8 @@ export const CippPolicyImportDrawer = ({ relatedQueryKeys: mode === "ConditionalAccess" ? ["ListCATemplates-table"] + : mode === "Standards" + ? ["listStandardTemplates"] : ["ListIntuneTemplates-table", "ListIntuneTemplates-autcomplete"], }); @@ -121,6 +125,16 @@ export const CippPolicyImportDrawer = ({ url: "/api/AddCATemplate", data: caTemplateData, }); + } else if (mode === "Standards") { + // For Standards templates, clone the template + importPolicy.mutate({ + url: "/api/AddStandardTemplate", + data: { + tenantFilter: tenantFilter?.value, + templateId: policy.GUID, + clone: true, + }, + }); } else { // For Intune policies, use existing format importPolicy.mutate({ @@ -486,7 +500,7 @@ export const CippPolicyImportDrawer = ({ ) : ( )} 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 aa5f940a561a..ed718b46caab 100644 --- a/src/pages/tenant/standards/list-standards/classic-standards/index.js +++ b/src/pages/tenant/standards/list-standards/classic-standards/index.js @@ -10,6 +10,8 @@ import { CippApiResults } from "../../../../../components/CippComponents/CippApi import { EyeIcon } from "@heroicons/react/24/outline"; import tabOptions from "../tabOptions.json"; import { useSettings } from "/src/hooks/use-settings.js"; +import { CippPolicyImportDrawer } from "../../../../../components/CippComponents/CippPolicyImportDrawer.jsx"; +import { PermissionButton } from "/src/utils/permissions.js"; const Page = () => { const oldStandards = ApiGetCall({ url: "/api/ListStandards", queryKey: "ListStandards-legacy" }); @@ -22,6 +24,7 @@ const Page = () => { const currentTenant = useSettings().currentTenant; const pageTitle = "Templates"; + const cardButtonPermissions = ["Tenant.Standards.ReadWrite"]; const actions = [ { label: "View Tenant Report", @@ -183,9 +186,15 @@ const Page = () => { - + } actions={actions} From c7c6343b9be7029a6d50eafb0eb1a03f4eb1546d Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Mon, 22 Sep 2025 15:18:10 +0200 Subject: [PATCH 024/112] added check page --- src/layouts/config.js | 5 ++ .../incidents/list-check-alerts/index.js | 61 +++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 src/pages/security/incidents/list-check-alerts/index.js diff --git a/src/layouts/config.js b/src/layouts/config.js index 879244bf207f..db0508ce56f3 100644 --- a/src/layouts/config.js +++ b/src/layouts/config.js @@ -295,6 +295,11 @@ export const nativeMenuItems = [ path: "/security/incidents/list-mdo-alerts", permissions: ["Security.Alert.*"], }, + { + title: "Check Alerts", + path: "/security/incidents/list-check-alerts", + permissions: ["Security.Alert.*"], + }, ], }, { diff --git a/src/pages/security/incidents/list-check-alerts/index.js b/src/pages/security/incidents/list-check-alerts/index.js new file mode 100644 index 000000000000..6d1401518d0f --- /dev/null +++ b/src/pages/security/incidents/list-check-alerts/index.js @@ -0,0 +1,61 @@ +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { Alert, Link } from "@mui/material"; + +const Page = () => { + const pageTitle = "Check Alerts"; + + // Explainer component + const explainer = ( + + This page collects the alerts from Check by Cyberdrain, a browser plugin that blocks AiTM + (Adversary-in-the-Middle) attacks. Check provides real-time protection against phishing and + credential theft attempts. Learn more at{" "} + + docs.check.tech + {" "} + or install the plugin now: + + Microsoft Edge + {" "} + | + + Chrome + + + ); + + const columns = [ + "tenantFilter", + "type", + "url", + "reason", + "score", + "threshold", + "potentialUserName", + "potentialUserDisplayName", + "reportedByIP", + "timestamp", + ]; + + return ( + + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; From 6c61aa8fe3c8358c630ece8a403fa27f0adeb078 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Mon, 22 Sep 2025 23:59:09 +0200 Subject: [PATCH 025/112] fix autocomplete errors, prettifications --- .../CippBackupScheduleDrawer.jsx | 9 +++++++-- .../CippRestoreBackupDrawer.jsx | 4 ++-- .../manage-drift/configuration-backup.js | 20 +++++++++++++++++-- 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/src/components/CippComponents/CippBackupScheduleDrawer.jsx b/src/components/CippComponents/CippBackupScheduleDrawer.jsx index e73a795715d4..d1fc1709737c 100644 --- a/src/components/CippComponents/CippBackupScheduleDrawer.jsx +++ b/src/components/CippComponents/CippBackupScheduleDrawer.jsx @@ -15,6 +15,7 @@ export const CippBackupScheduleDrawer = ({ buttonText = "Add Backup Schedule", requiredPermissions = [], PermissionButton = Button, + onSuccess, }) => { const [drawerVisible, setDrawerVisible] = useState(false); const userSettingsDefaults = useSettings(); @@ -38,7 +39,7 @@ export const CippBackupScheduleDrawer = ({ const createBackup = ApiPostCall({ urlFromData: true, - relatedQueryKeys: ["BackupList", "BackupTasks"], + relatedQueryKeys: [`BackupTasks-${userSettingsDefaults.currentTenant}`], }); const { isValid, isDirty } = useFormState({ control: formControl.control }); @@ -58,8 +59,12 @@ export const CippBackupScheduleDrawer = ({ CippWebhookAlerts: true, CippScriptedAlerts: true, }); + // Call onSuccess callback if provided + if (onSuccess) { + onSuccess(); + } } - }, [createBackup.isSuccess]); + }, [createBackup.isSuccess, onSuccess]); const handleSubmit = () => { formControl.trigger(); diff --git a/src/components/CippComponents/CippRestoreBackupDrawer.jsx b/src/components/CippComponents/CippRestoreBackupDrawer.jsx index 56a0600e6d29..275bf4e31abc 100644 --- a/src/components/CippComponents/CippRestoreBackupDrawer.jsx +++ b/src/components/CippComponents/CippRestoreBackupDrawer.jsx @@ -46,7 +46,7 @@ export const CippRestoreBackupDrawer = ({ const restoreBackup = ApiPostCall({ urlFromData: true, - relatedQueryKeys: ["BackupList", "BackupTasks"], + relatedQueryKeys: [`BackupTasks-${tenantFilter}`], }); const { isValid, isDirty } = useFormState({ control: formControl.control }); @@ -195,7 +195,7 @@ export const CippRestoreBackupDrawer = ({ multiple={false} api={{ url: "/api/ExecListBackup", - queryKey: `BackupList-${tenantFilter}`, + 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; diff --git a/src/pages/tenant/standards/manage-drift/configuration-backup.js b/src/pages/tenant/standards/manage-drift/configuration-backup.js index 7439404b57b4..26364fb41c82 100644 --- a/src/pages/tenant/standards/manage-drift/configuration-backup.js +++ b/src/pages/tenant/standards/manage-drift/configuration-backup.js @@ -203,7 +203,16 @@ const Page = () => { Current Configuration {!hasExistingConfig ? ( - + { + // Refresh both queries when a backup schedule is added + setTimeout(() => { + backupList.refetch(); + existingBackupConfig.refetch(); + }, 2000); + }} + /> ) : ( + )} + + ))} - ); - }); - return sections; + + ); })} + + {/* Description offcanvas */} + setDescriptionOffcanvasVisible(false)} + title="Function Description" + > + + + {selectedDescription.name} + + + {selectedDescription.description} + + + ); }; diff --git a/src/components/CippSettings/CippRoleAddEdit.jsx b/src/components/CippSettings/CippRoleAddEdit.jsx index abc605f34df7..5d992969d2bc 100644 --- a/src/components/CippSettings/CippRoleAddEdit.jsx +++ b/src/components/CippSettings/CippRoleAddEdit.jsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import React, { useEffect, useState } from "react"; import { Box, @@ -349,13 +349,13 @@ export const CippRoleAddEdit = ({ selectedRole }) => { const ApiPermissionRow = ({ obj, cat, readOnly }) => { const [offcanvasVisible, setOffcanvasVisible] = useState(false); + const [descriptionOffcanvasVisible, setDescriptionOffcanvasVisible] = useState(false); + const [selectedDescription, setSelectedDescription] = useState({ name: '', description: '' }); - var items = []; - for (var key in apiPermissions[cat][obj]) - for (var key2 in apiPermissions[cat][obj][key]) { - items.push({ heading: "", content: apiPermissions[cat][obj][key][key2] }); - } - var group = [{ items: items }]; + const handleDescriptionClick = (name, description) => { + setSelectedDescription({ name, description }); + setDescriptionOffcanvasVisible(true); + }; return ( { disabled={readOnly} /> + {/* Main offcanvas */} { - setOffcanvasVisible(false); - }} + onClose={() => setOffcanvasVisible(false)} + title={`${cat}.${obj} Endpoints`} > - - {`${cat}.${obj}`} - - Listed below are the available API endpoints based on permission level, ReadWrite - level includes endpoints under Read. + Listed below are the available API endpoints based on permission level. + ReadWrite level includes endpoints under Read. - {[apiPermissions[cat][obj]].map((permissions, key) => { - var sections = Object.keys(permissions).map((type) => { - var items = []; - for (var api in permissions[type]) { - items.push({ heading: "", content: permissions[type][api] }); - } - return ( - - {type} - - {items.map((item, idx) => ( - - {item.content} - - ))} - + {Object.keys(apiPermissions[cat][obj]).map((type, typeIndex) => { + var items = []; + for (var api in apiPermissions[cat][obj][type]) { + const apiFunction = apiPermissions[cat][obj][type][api]; + items.push({ + name: apiFunction.Name, + description: apiFunction.Description?.[0]?.Text || null + }); + } + return ( + + {type} + + {items.map((item, idx) => ( + + + {item.name} + + {item.description && ( + + )} + + ))} - ); - }); - return sections; + + ); })} + + {/* Description offcanvas */} + setDescriptionOffcanvasVisible(false)} + title="Function Description" + > + + + {selectedDescription.name} + + + {selectedDescription.description} + + + ); }; @@ -534,15 +561,15 @@ export const CippRoleAddEdit = ({ selectedRole }) => { .sort() .forEach((obj) => { Object.keys(apiPermissions[cat][obj]).forEach((type) => { - Object.keys(apiPermissions[cat][obj][type]).forEach( - (apiKey) => { - allEndpoints.push({ - label: apiPermissions[cat][obj][type][apiKey], - value: apiPermissions[cat][obj][type][apiKey], - category: cat, - }); - } - ); + Object.keys(apiPermissions[cat][obj][type]).forEach((apiKey) => { + const apiFunction = apiPermissions[cat][obj][type][apiKey]; + const descriptionText = apiFunction.Description?.[0]?.Text; + allEndpoints.push({ + label: descriptionText ? `${apiFunction.Name} - ${descriptionText}` : apiFunction.Name, + value: apiFunction.Name, + category: cat, + }); + }); }); }); }); @@ -646,7 +673,7 @@ export const CippRoleAddEdit = ({ selectedRole }) => {
Allowed Tenants
    {selectedTenant.map((tenant, idx) => ( -
  • {tenant?.label}
  • +
  • {tenant?.label}
  • ))}
@@ -656,7 +683,7 @@ export const CippRoleAddEdit = ({ selectedRole }) => {
Blocked Tenants
    {blockedTenants.map((tenant, idx) => ( -
  • {tenant?.label}
  • +
  • {tenant?.label}
  • ))}
@@ -666,7 +693,7 @@ export const CippRoleAddEdit = ({ selectedRole }) => {
Blocked Endpoints
    {blockedEndpoints.map((endpoint, idx) => ( -
  • +
  • {endpoint?.label || endpoint?.value || endpoint}
  • ))} @@ -681,13 +708,13 @@ export const CippRoleAddEdit = ({ selectedRole }) => { Object.keys(selectedPermissions) ?.sort() .map((cat, idx) => ( - <> + {selectedPermissions?.[cat] && typeof selectedPermissions[cat] === "string" && !selectedPermissions[cat]?.includes("None") && ( -
  • {selectedPermissions[cat]}
  • +
  • {selectedPermissions[cat]}
  • )} - +
    ))}
From 488f61ed0483c7883d32decf72f74e29e6f888c2 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sat, 27 Sep 2025 12:53:46 +0200 Subject: [PATCH 036/112] string percentage --- src/utils/get-cipp-formatting.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/utils/get-cipp-formatting.js b/src/utils/get-cipp-formatting.js index 26b388d7fc8f..e2df26a72ffd 100644 --- a/src/utils/get-cipp-formatting.js +++ b/src/utils/get-cipp-formatting.js @@ -269,10 +269,6 @@ export const getCippFormatting = (data, cellName, type, canReceive, flatten = tr return isText ? `${data}%` : ; } - if (cellName === "DMARCPercentagePass") { - return isText ? `${data}%` : ; - } - if (cellName === "ScoreExplanation") { return isText ? data : ; } From 0cb5fb027195697f813ab98ca677955ef219aeec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Sun, 28 Sep 2025 12:37:33 +0200 Subject: [PATCH 037/112] Show subgroups and type in the category for blocked endpoints --- cspell.json | 1 + .../CippSettings/CippRoleAddEdit.jsx | 53 ++++++++++--------- 2 files changed, 30 insertions(+), 24 deletions(-) diff --git a/cspell.json b/cspell.json index fe7d6143946f..b51f8a518d31 100644 --- a/cspell.json +++ b/cspell.json @@ -30,6 +30,7 @@ "Reshare", "Rewst", "Sherweb", + "superadmin", "Syncro", "TERRL", "unconfigured", diff --git a/src/components/CippSettings/CippRoleAddEdit.jsx b/src/components/CippSettings/CippRoleAddEdit.jsx index 84bb94d1802e..002d847426be 100644 --- a/src/components/CippSettings/CippRoleAddEdit.jsx +++ b/src/components/CippSettings/CippRoleAddEdit.jsx @@ -350,7 +350,7 @@ export const CippRoleAddEdit = ({ selectedRole }) => { const ApiPermissionRow = ({ obj, cat, readOnly }) => { const [offcanvasVisible, setOffcanvasVisible] = useState(false); const [descriptionOffcanvasVisible, setDescriptionOffcanvasVisible] = useState(false); - const [selectedDescription, setSelectedDescription] = useState({ name: '', description: '' }); + const [selectedDescription, setSelectedDescription] = useState({ name: "", description: "" }); const handleDescriptionClick = (name, description) => { setSelectedDescription({ name, description }); @@ -400,16 +400,16 @@ export const CippRoleAddEdit = ({ selectedRole }) => { > - Listed below are the available API endpoints based on permission level. - ReadWrite level includes endpoints under Read. + Listed below are the available API endpoints based on permission level. ReadWrite + level includes endpoints under Read. {Object.keys(apiPermissions[cat][obj]).map((type, typeIndex) => { var items = []; for (var api in apiPermissions[cat][obj][type]) { const apiFunction = apiPermissions[cat][obj][type][api]; - items.push({ - name: apiFunction.Name, - description: apiFunction.Description?.[0]?.Text || null + items.push({ + name: apiFunction.Name, + description: apiFunction.Description?.[0]?.Text || null, }); } return ( @@ -418,14 +418,14 @@ export const CippRoleAddEdit = ({ selectedRole }) => { {items.map((item, idx) => ( - + {item.name} {item.description && ( - + + + } + > + + {/* Display Name */} + + + + + + + {/* Username and Domain */} + + + + + + + + + + + + ); +}; diff --git a/src/components/CippComponents/CippAddRoomDrawer.jsx b/src/components/CippComponents/CippAddRoomDrawer.jsx new file mode 100644 index 000000000000..9bf3767cdf76 --- /dev/null +++ b/src/components/CippComponents/CippAddRoomDrawer.jsx @@ -0,0 +1,171 @@ +import React, { useState, useEffect } from "react"; +import { Button, Divider } from "@mui/material"; +import { Grid } from "@mui/system"; +import { useForm, useFormState } from "react-hook-form"; +import { AddHomeWork } from "@mui/icons-material"; +import { CippOffCanvas } from "./CippOffCanvas"; +import CippFormComponent from "./CippFormComponent"; +import { CippFormDomainSelector } from "./CippFormDomainSelector"; +import { CippApiResults } from "./CippApiResults"; +import { useSettings } from "../../hooks/use-settings"; +import { ApiPostCall } from "../../api/ApiCall"; + +export const CippAddRoomDrawer = ({ + buttonText = "Add Room Mailbox", + requiredPermissions = [], + PermissionButton = Button, +}) => { + const [drawerVisible, setDrawerVisible] = useState(false); + const tenantDomain = useSettings().currentTenant; + + const formControl = useForm({ + mode: "onChange", + defaultValues: { + displayName: "", + username: "", + domain: null, + resourceCapacity: "", + }, + }); + + const { isValid } = useFormState({ control: formControl.control }); + + const addRoom = ApiPostCall({ + urlFromData: true, + relatedQueryKeys: [`RoomMailbox-${tenantDomain}`], + }); + + // Reset form fields on successful creation + useEffect(() => { + if (addRoom.isSuccess) { + formControl.reset({ + displayName: "", + username: "", + domain: null, + resourceCapacity: "", + }); + } + }, [addRoom.isSuccess, formControl]); + + const handleSubmit = () => { + formControl.trigger(); + // Check if the form is valid before proceeding + if (!isValid) { + return; + } + + const formData = formControl.getValues(); + const shippedValues = { + tenantID: tenantDomain, + domain: formData.domain?.value, + displayName: formData.displayName.trim(), + username: formData.username.trim(), + userPrincipalName: formData.username.trim() + "@" + (formData.domain?.value || "").trim(), + }; + + if (formData.resourceCapacity && formData.resourceCapacity.trim() !== "") { + shippedValues.resourceCapacity = formData.resourceCapacity.trim(); + } + + addRoom.mutate({ + url: "/api/AddRoomMailbox", + data: shippedValues, + relatedQueryKeys: [`RoomMailbox-${tenantDomain}`], + }); + }; + + const handleCloseDrawer = () => { + setDrawerVisible(false); + formControl.reset({ + displayName: "", + username: "", + domain: null, + resourceCapacity: "", + }); + }; + + return ( + <> + setDrawerVisible(true)} + startIcon={} + > + {buttonText} + + + + + + } + > + + {/* Display Name */} + + + + + + + {/* Username and Domain */} + + + + + + + + + + {/* Resource Capacity (Optional) */} + + + + + + + + + ); +}; diff --git a/src/components/CippComponents/CippAddRoomListDrawer.jsx b/src/components/CippComponents/CippAddRoomListDrawer.jsx new file mode 100644 index 000000000000..6ced8947993b --- /dev/null +++ b/src/components/CippComponents/CippAddRoomListDrawer.jsx @@ -0,0 +1,159 @@ +import React, { useState, useEffect } from "react"; +import { Button, InputAdornment, Divider } from "@mui/material"; +import { Grid } from "@mui/system"; +import { useForm, useFormState } from "react-hook-form"; +import { ListAlt } from "@mui/icons-material"; +import { CippOffCanvas } from "./CippOffCanvas"; +import CippFormComponent from "./CippFormComponent"; +import { CippFormDomainSelector } from "./CippFormDomainSelector"; +import { CippApiResults } from "./CippApiResults"; +import { useSettings } from "../../hooks/use-settings"; +import { ApiPostCall } from "../../api/ApiCall"; + +export const CippAddRoomListDrawer = ({ + buttonText = "Add Room List", + requiredPermissions = [], + PermissionButton = Button, +}) => { + const [drawerVisible, setDrawerVisible] = useState(false); + const userSettingsDefaults = useSettings(); + const tenantDomain = userSettingsDefaults.currentTenant; + + const formControl = useForm({ + mode: "onChange", + defaultValues: { + displayName: "", + username: "", + primDomain: null, + }, + }); + + const { isValid } = useFormState({ control: formControl.control }); + + const addRoomList = ApiPostCall({ + urlFromData: true, + relatedQueryKeys: [`RoomLists-${tenantDomain}`], + }); + + // Reset form fields on successful creation + useEffect(() => { + if (addRoomList.isSuccess) { + formControl.reset({ + displayName: "", + username: "", + primDomain: null, + }); + } + }, [addRoomList.isSuccess, formControl]); + + const handleSubmit = () => { + formControl.trigger(); + // Check if the form is valid before proceeding + if (!isValid) { + return; + } + + const formData = formControl.getValues(); + const shippedValues = { + tenantFilter: tenantDomain, + displayName: formData.displayName?.trim(), + username: formData.username?.trim(), + primDomain: formData.primDomain, + }; + + addRoomList.mutate({ + url: "/api/AddRoomList", + data: shippedValues, + relatedQueryKeys: [`RoomLists-${tenantDomain}`], + }); + }; + + const handleCloseDrawer = () => { + setDrawerVisible(false); + formControl.reset({ + displayName: "", + username: "", + primDomain: null, + }); + }; + + return ( + <> + setDrawerVisible(true)} + startIcon={} + > + {buttonText} + + + + + + } + > + + + + + + + @, + }} + /> + + + + + + + + + + + ); +}; diff --git a/src/pages/email/resources/management/equipment/add.jsx b/src/pages/email/resources/management/equipment/add.jsx deleted file mode 100644 index a186a19e0ba5..000000000000 --- a/src/pages/email/resources/management/equipment/add.jsx +++ /dev/null @@ -1,83 +0,0 @@ -import React from "react"; -import { Divider } from "@mui/material"; -import { Grid } from "@mui/system"; -import { useForm } from "react-hook-form"; -import { Layout as DashboardLayout } from "/src/layouts/index.js"; -import CippFormPage from "/src/components/CippFormPages/CippFormPage"; -import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; -import { CippFormDomainSelector } from "/src/components/CippComponents/CippFormDomainSelector"; -import { useSettings } from "/src/hooks/use-settings"; - -const AddEquipmentMailbox = () => { - const tenantDomain = useSettings().currentTenant; - const formControl = useForm({ - mode: "onChange", - defaultValues: { - displayName: "", - username: "", - domain: null, - location: "", - department: "", - company: "", - }, - }); - - return ( - { - const shippedValues = { - tenantID: tenantDomain, - domain: values.domain?.value, - displayName: values.displayName.trim(), - username: values.username.trim(), - userPrincipalName: values.username.trim() + "@" + (values.domain?.value || "").trim(), - }; - - return shippedValues; - }} - > - - {/* Display Name */} - - - - - - - {/* Username and Domain */} - - - - - - - - - ); -}; - -AddEquipmentMailbox.getLayout = (page) => {page}; - -export default AddEquipmentMailbox; diff --git a/src/pages/email/resources/management/equipment/index.js b/src/pages/email/resources/management/equipment/index.js index 9a3f97e29cc5..f8a19bef011b 100644 --- a/src/pages/email/resources/management/equipment/index.js +++ b/src/pages/email/resources/management/equipment/index.js @@ -1,9 +1,8 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; -import { Button } from "@mui/material"; -import Link from "next/link"; -import { AddBusiness, Edit, Block, LockOpen, Key } from "@mui/icons-material"; +import { Edit, Block, LockOpen, Key } from "@mui/icons-material"; import { TrashIcon } from "@heroicons/react/24/outline"; +import { CippAddEquipmentDrawer } from "../../../../../components/CippComponents/CippAddEquipmentDrawer"; const Page = () => { const pageTitle = "Equipment"; @@ -54,26 +53,20 @@ const Page = () => { }, ]; + const simpleColumns = [ + "DisplayName", + "UserPrincipalName", + "HiddenFromAddressListsEnabled", + "PrimarySmtpAddress", + ]; + return ( } - > - Add Equipment - - } + simpleColumns={simpleColumns} + cardButton={} /> ); }; diff --git a/src/pages/email/resources/management/list-rooms/add.jsx b/src/pages/email/resources/management/list-rooms/add.jsx deleted file mode 100644 index b4ef65fd7317..000000000000 --- a/src/pages/email/resources/management/list-rooms/add.jsx +++ /dev/null @@ -1,97 +0,0 @@ -import React from "react"; -import { Divider } from "@mui/material"; -import { Grid } from "@mui/system"; -import { useForm } from "react-hook-form"; -import { Layout as DashboardLayout } from "/src/layouts/index.js"; -import CippFormPage from "/src/components/CippFormPages/CippFormPage"; -import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; -import { CippFormDomainSelector } from "/src/components/CippComponents/CippFormDomainSelector"; -import { useSettings } from "/src/hooks/use-settings"; - -const AddRoomMailbox = () => { - const tenantDomain = useSettings().currentTenant; - const formControl = useForm({ - mode: "onChange", - defaultValues: { - displayName: "", - username: "", - domain: null, - resourceCapacity: "", - }, - }); - - return ( - { - const shippedValues = { - tenantID: tenantDomain, - domain: values.domain?.value, - displayName: values.displayName.trim(), - username: values.username.trim(), - userPrincipalName: values.username.trim() + "@" + (values.domain?.value || "").trim(), - }; - - if (values.resourceCapacity && values.resourceCapacity.trim() !== "") { - shippedValues.resourceCapacity = values.resourceCapacity.trim(); - } - - return shippedValues; - }} - > - - {/* Display Name */} - - - - - - - {/* Username and Domain */} - - - - - - - - - - {/* Resource Capacity (Optional) */} - - - - - - ); -}; - -AddRoomMailbox.getLayout = (page) => {page}; - -export default AddRoomMailbox; diff --git a/src/pages/email/resources/management/list-rooms/index.js b/src/pages/email/resources/management/list-rooms/index.js index 68c79ba360c2..acac08a8b1b9 100644 --- a/src/pages/email/resources/management/list-rooms/index.js +++ b/src/pages/email/resources/management/list-rooms/index.js @@ -1,9 +1,8 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; -import { Button } from "@mui/material"; -import Link from "next/link"; -import { AddHomeWork, Edit, Block, LockOpen, Key } from "@mui/icons-material"; +import { Edit, Block, LockOpen, Key } from "@mui/icons-material"; import { TrashIcon } from "@heroicons/react/24/outline"; +import { CippAddRoomDrawer } from "../../../../../components/CippComponents/CippAddRoomDrawer"; const Page = () => { const pageTitle = "Rooms"; @@ -70,15 +69,7 @@ const Page = () => { "countryOrRegion", "hiddenFromAddressListsEnabled", ]} - cardButton={ - - } + cardButton={} /> ); }; diff --git a/src/pages/email/resources/management/room-lists/add.jsx b/src/pages/email/resources/management/room-lists/add.jsx deleted file mode 100644 index 606259d728e0..000000000000 --- a/src/pages/email/resources/management/room-lists/add.jsx +++ /dev/null @@ -1,50 +0,0 @@ -import { Box } from "@mui/material"; -import CippFormPage from "../../../../../components/CippFormPages/CippFormPage"; -import { Layout as DashboardLayout } from "/src/layouts/index.js"; -import { useForm } from "react-hook-form"; -import { useSettings } from "../../../../../hooks/use-settings"; -import CippAddRoomListForm from "../../../../../components/CippFormPages/CippAddRoomListForm"; - -const Page = () => { - const userSettingsDefaults = useSettings(); - const tenantDomain = userSettingsDefaults.currentTenant; - - const formControl = useForm({ - mode: "onChange", - defaultValues: { - displayName: "", - username: "", - primDomain: null, - }, - }); - - return ( - <> - { - return { - tenantFilter: tenantDomain, - displayName: values.displayName?.trim(), - username: values.username?.trim(), - primDomain: values.primDomain, - }; - }} - > - - - - - - ); -}; - -Page.getLayout = (page) => {page}; - -export default Page; \ No newline at end of file diff --git a/src/pages/email/resources/management/room-lists/index.js b/src/pages/email/resources/management/room-lists/index.js index b29198a98e9a..c5a8f0848986 100644 --- a/src/pages/email/resources/management/room-lists/index.js +++ b/src/pages/email/resources/management/room-lists/index.js @@ -1,9 +1,8 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; -import { Visibility, ListAlt, Edit } from "@mui/icons-material"; +import { Edit } from "@mui/icons-material"; import { TrashIcon } from "@heroicons/react/24/outline"; -import { Button } from "@mui/material"; -import Link from "next/link"; +import { CippAddRoomListDrawer } from "../../../../../components/CippComponents/CippAddRoomListDrawer"; const Page = () => { const pageTitle = "Room Lists"; @@ -45,13 +44,7 @@ const Page = () => { actions: actions, }; - const simpleColumns = [ - "DisplayName", - "PrimarySmtpAddress", - "Identity", - "Phone", - "Notes", - ]; + const simpleColumns = ["DisplayName", "PrimarySmtpAddress", "Identity", "Phone", "Notes"]; return ( { simpleColumns={simpleColumns} cardButton={ <> - + } /> From 3515330ca53756a95f56f3427cc2b36a87f2ab79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Sun, 28 Sep 2025 21:28:15 +0200 Subject: [PATCH 042/112] fix: adjust grid sizes for display name and username fields in equipment and room drawers --- src/components/CippComponents/CippAddEquipmentDrawer.jsx | 2 +- src/components/CippComponents/CippAddRoomDrawer.jsx | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/CippComponents/CippAddEquipmentDrawer.jsx b/src/components/CippComponents/CippAddEquipmentDrawer.jsx index 8c34c1994e81..3d9a397765c6 100644 --- a/src/components/CippComponents/CippAddEquipmentDrawer.jsx +++ b/src/components/CippComponents/CippAddEquipmentDrawer.jsx @@ -112,7 +112,7 @@ export const CippAddEquipmentDrawer = ({ > {/* Display Name */} - + {/* Display Name */} - + {/* Username and Domain */} - + - + Date: Sun, 28 Sep 2025 21:33:22 +0200 Subject: [PATCH 043/112] Feat: Enhance CippAutoComplete and CippFormDomainSelector for improved preselection logic --- .../CippComponents/CippAutocomplete.jsx | 27 +++++++--- .../CippComponents/CippFormDomainSelector.jsx | 51 +++++++++++++------ 2 files changed, 57 insertions(+), 21 deletions(-) diff --git a/src/components/CippComponents/CippAutocomplete.jsx b/src/components/CippComponents/CippAutocomplete.jsx index 6ac86f53d4ba..2e0df7f25c1f 100644 --- a/src/components/CippComponents/CippAutocomplete.jsx +++ b/src/components/CippComponents/CippAutocomplete.jsx @@ -208,9 +208,9 @@ export const CippAutoComplete = (props) => { return finalOptions; }, [api, usedOptions, options, removeOptions, sortOptions]); - // Dedicated effect for handling preselected value - only runs once + // Dedicated effect for handling preselected value or auto-select first item - only runs once useEffect(() => { - if (preselectedValue && memoizedOptions.length > 0 && !hasPreselectedRef.current) { + if (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); @@ -223,9 +223,16 @@ export const CippAutoComplete = (props) => { : !value; if (shouldPreselect) { - const preselectedOption = memoizedOptions.find( - (option) => option.value === preselectedValue - ); + let preselectedOption; + + // Handle explicit preselected value + if (preselectedValue) { + preselectedOption = memoizedOptions.find((option) => option.value === preselectedValue); + } + // Handle auto-select first item from API + else if (api?.autoSelectFirstItem && memoizedOptions.length > 0) { + preselectedOption = memoizedOptions[0]; + } if (preselectedOption) { const newValue = multiple ? [preselectedOption] : preselectedOption; @@ -237,7 +244,15 @@ export const CippAutoComplete = (props) => { } } } - }, [preselectedValue, defaultValue, value, memoizedOptions, multiple, onChange]); + }, [ + preselectedValue, + defaultValue, + value, + memoizedOptions, + multiple, + onChange, + api?.autoSelectFirstItem, + ]); // Create a stable key that only changes when necessary inputs change const stableKey = useMemo(() => { diff --git a/src/components/CippComponents/CippFormDomainSelector.jsx b/src/components/CippComponents/CippFormDomainSelector.jsx index 9bf5f65639d0..8d9cbea7dca2 100644 --- a/src/components/CippComponents/CippFormDomainSelector.jsx +++ b/src/components/CippComponents/CippFormDomainSelector.jsx @@ -1,6 +1,7 @@ import { CippFormComponent } from "./CippFormComponent"; import { useWatch } from "react-hook-form"; import { useSettings } from "../../hooks/use-settings"; +import { useMemo } from "react"; export const CippFormDomainSelector = ({ formControl, @@ -9,10 +10,44 @@ export const CippFormDomainSelector = ({ allTenants = false, type = "multiple", multiple = false, + preselectDefaultDomain = true, ...other }) => { const currentTenant = useWatch({ control: formControl.control, name: "tenantFilter" }); const selectedTenant = useSettings().currentTenant; + + const apiConfig = useMemo( + () => ({ + autoSelectFirstItem: preselectDefaultDomain && !multiple, + tenantFilter: currentTenant ? currentTenant.value : selectedTenant, + queryKey: `listDomains-${currentTenant?.value ? currentTenant.value : selectedTenant}`, + url: "/api/ListGraphRequest", + dataKey: "Results", + labelField: (option) => `${option.id}`, + valueField: "id", + addedField: { + isDefault: "isDefault", + isInitial: "isInitial", + isVerified: "isVerified", + }, + data: { + Endpoint: "domains", + manualPagination: true, + $count: true, + $top: 99, + }, + dataFilter: (domains) => { + // Always sort domains so that the default domain appears first + return domains.sort((a, b) => { + if (a.addedFields?.isDefault === true) return -1; + if (b.addedFields?.isDefault === true) return 1; + return 0; + }); + }, + }), + [currentTenant, selectedTenant, preselectDefaultDomain, multiple] + ); + return ( `${option.id}`, - valueField: "id", - data: { - Endpoint: "domains", - manualPagination: true, - $count: true, - $top: 99, - }, - }} + api={apiConfig} {...other} /> ); From 1c3bd7e9d3d119d499255db8bc40c9ca37636b94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Sun, 28 Sep 2025 22:12:50 +0200 Subject: [PATCH 044/112] feat: add requiredPermissions prop to room, roomlist and equipment page buttons --- src/pages/email/resources/management/equipment/index.js | 3 ++- src/pages/email/resources/management/list-rooms/index.js | 3 ++- src/pages/email/resources/management/room-lists/index.js | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/pages/email/resources/management/equipment/index.js b/src/pages/email/resources/management/equipment/index.js index f8a19bef011b..abaae1ebaec3 100644 --- a/src/pages/email/resources/management/equipment/index.js +++ b/src/pages/email/resources/management/equipment/index.js @@ -6,6 +6,7 @@ import { CippAddEquipmentDrawer } from "../../../../../components/CippComponents const Page = () => { const pageTitle = "Equipment"; + const cardButtonPermissions = ["Exchange.Equipment.ReadWrite"]; const actions = [ { @@ -66,7 +67,7 @@ const Page = () => { apiUrl="/api/ListEquipment" actions={actions} simpleColumns={simpleColumns} - cardButton={} + cardButton={} /> ); }; diff --git a/src/pages/email/resources/management/list-rooms/index.js b/src/pages/email/resources/management/list-rooms/index.js index acac08a8b1b9..256dfaf3f3f7 100644 --- a/src/pages/email/resources/management/list-rooms/index.js +++ b/src/pages/email/resources/management/list-rooms/index.js @@ -6,6 +6,7 @@ import { CippAddRoomDrawer } from "../../../../../components/CippComponents/Cipp const Page = () => { const pageTitle = "Rooms"; + const cardButtonPermissions = ["Exchange.Room.ReadWrite"]; const actions = [ { @@ -69,7 +70,7 @@ const Page = () => { "countryOrRegion", "hiddenFromAddressListsEnabled", ]} - cardButton={} + cardButton={} /> ); }; diff --git a/src/pages/email/resources/management/room-lists/index.js b/src/pages/email/resources/management/room-lists/index.js index c5a8f0848986..1ee0cd11e346 100644 --- a/src/pages/email/resources/management/room-lists/index.js +++ b/src/pages/email/resources/management/room-lists/index.js @@ -7,6 +7,7 @@ import { CippAddRoomListDrawer } from "../../../../../components/CippComponents/ const Page = () => { const pageTitle = "Room Lists"; const apiUrl = "/api/ListRoomLists"; + const cardButtonPermissions = ["Exchange.Room.ReadWrite"]; const actions = [ { @@ -56,7 +57,7 @@ const Page = () => { simpleColumns={simpleColumns} cardButton={ <> - + } /> From b105441abf3ed24936473a68ecd4d63c8de7b877 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Sun, 28 Sep 2025 22:04:19 +0200 Subject: [PATCH 045/112] Feat: Add CippAddContactDrawer component for adding new contacts --- .../CippComponents/CippAddContactDrawer.jsx | 321 ++++++++++++++++++ .../email/administration/contacts/index.js | 86 ++--- 2 files changed, 364 insertions(+), 43 deletions(-) create mode 100644 src/components/CippComponents/CippAddContactDrawer.jsx diff --git a/src/components/CippComponents/CippAddContactDrawer.jsx b/src/components/CippComponents/CippAddContactDrawer.jsx new file mode 100644 index 000000000000..f46b5b54f80e --- /dev/null +++ b/src/components/CippComponents/CippAddContactDrawer.jsx @@ -0,0 +1,321 @@ +import React, { useState, useEffect } from "react"; +import { Button, Divider } from "@mui/material"; +import { Grid } from "@mui/system"; +import { useForm, useFormState } from "react-hook-form"; +import { PersonAdd } from "@mui/icons-material"; +import { CippOffCanvas } from "./CippOffCanvas"; +import CippFormComponent from "./CippFormComponent"; +import { CippApiResults } from "./CippApiResults"; +import { useSettings } from "../../hooks/use-settings"; +import { ApiPostCall } from "../../api/ApiCall"; + +export const CippAddContactDrawer = ({ + buttonText = "Add Contact", + requiredPermissions = [], + PermissionButton = Button, +}) => { + const [drawerVisible, setDrawerVisible] = useState(false); + const tenantDomain = useSettings().currentTenant; + + const formControl = useForm({ + mode: "onChange", + defaultValues: { + displayName: "", + firstName: "", + lastName: "", + email: "", + hidefromGAL: false, + streetAddress: "", + postalCode: "", + city: "", + state: "", + country: "", + companyName: "", + mobilePhone: "", + businessPhone: "", + jobTitle: "", + website: "", + mailTip: "", + }, + }); + + const { isValid } = useFormState({ control: formControl.control }); + + const addContact = ApiPostCall({ + urlFromData: true, + relatedQueryKeys: [`Contacts-${tenantDomain}`], + }); + + // Reset form fields on successful creation + useEffect(() => { + if (addContact.isSuccess) { + formControl.reset({ + displayName: "", + firstName: "", + lastName: "", + email: "", + hidefromGAL: false, + streetAddress: "", + postalCode: "", + city: "", + state: "", + country: "", + companyName: "", + mobilePhone: "", + businessPhone: "", + jobTitle: "", + website: "", + mailTip: "", + }); + } + }, [addContact.isSuccess, formControl]); + + const handleSubmit = () => { + formControl.trigger(); + // Check if the form is valid before proceeding + if (!isValid) { + return; + } + + const formData = formControl.getValues(); + const shippedValues = { + tenantID: tenantDomain, + DisplayName: formData.displayName, + hidefromGAL: formData.hidefromGAL, + email: formData.email, + FirstName: formData.firstName, + LastName: formData.lastName, + Title: formData.jobTitle, + StreetAddress: formData.streetAddress, + PostalCode: formData.postalCode, + City: formData.city, + State: formData.state, + CountryOrRegion: formData.country?.value || formData.country, + Company: formData.companyName, + mobilePhone: formData.mobilePhone, + phone: formData.businessPhone, + website: formData.website, + mailTip: formData.mailTip, + }; + + addContact.mutate({ + url: "/api/AddContact", + data: shippedValues, + relatedQueryKeys: [`Contacts-${tenantDomain}`], + }); + }; + + const handleCloseDrawer = () => { + setDrawerVisible(false); + formControl.reset({ + displayName: "", + firstName: "", + lastName: "", + email: "", + hidefromGAL: false, + streetAddress: "", + postalCode: "", + city: "", + state: "", + country: "", + companyName: "", + mobilePhone: "", + businessPhone: "", + jobTitle: "", + website: "", + mailTip: "", + }); + }; + + return ( + <> + setDrawerVisible(true)} + startIcon={} + > + {buttonText} + + + + + + } + > + + {/* Display Name */} + + + + + {/* First Name and Last Name */} + + + + + + + + + + {/* Email */} + + + + + {/* Hide from GAL */} + + + + + + + {/* Additional Contact Information */} + + + + + + + + {/* Phone Numbers */} + + + + + + + + {/* Address Information */} + + + + + + + + + + + + + + {/* Website and Mail Tip */} + + + + {/* Website and Mail Tip */} + + + + + + + + + ); +}; diff --git a/src/pages/email/administration/contacts/index.js b/src/pages/email/administration/contacts/index.js index 48e5f2a65453..d2e5c58207e3 100644 --- a/src/pages/email/administration/contacts/index.js +++ b/src/pages/email/administration/contacts/index.js @@ -1,63 +1,63 @@ import { useMemo } from "react"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; -import { Edit, PersonAdd } from "@mui/icons-material"; +import { Edit, RocketLaunch } from "@mui/icons-material"; import { Button } from "@mui/material"; import Link from "next/link"; import TrashIcon from "@heroicons/react/24/outline/TrashIcon"; +import { CippAddContactDrawer } from "../../../../components/CippComponents/CippAddContactDrawer"; const Page = () => { const pageTitle = "Contacts"; - const actions = useMemo(() => [ - { - label: "Edit Contact", - link: "/email/administration/contacts/edit?id=[Guid]", - multiPost: false, - postEntireRow: true, - icon: , - color: "warning", - condition: (row) => !row.IsDirSynced, - }, - { - label: "Remove Contact", - type: "POST", - url: "/api/RemoveContact", - data: { - GUID: "Guid", - mail: "WindowsEmailAddress", + const cardButtonPermissions = ["Exchange.Contact.ReadWrite"]; + const actions = useMemo( + () => [ + { + label: "Edit Contact", + link: "/email/administration/contacts/edit?id=[Guid]", + multiPost: false, + postEntireRow: true, + icon: , + color: "warning", + condition: (row) => !row.IsDirSynced, }, - confirmText: - "Are you sure you want to delete this contact? Remember this will not work if the contact is AD Synced.", - color: "danger", - icon: , - condition: (row) => !row.IsDirSynced, - }, - ], []); - - const simpleColumns = useMemo(() => [ - "DisplayName", - "WindowsEmailAddress", - "Company", - "IsDirSynced" - ], []); - - const cardButton = useMemo(() => ( - - ), []); + { + label: "Remove Contact", + type: "POST", + url: "/api/RemoveContact", + data: { + GUID: "Guid", + mail: "WindowsEmailAddress", + }, + confirmText: + "Are you sure you want to delete this contact? Remember this will not work if the contact is AD Synced.", + color: "danger", + icon: , + condition: (row) => !row.IsDirSynced, + }, + ], + [] + ); + const simpleColumns = ["DisplayName", "WindowsEmailAddress", "Company", "IsDirSynced"]; return ( + + + + } /> ); }; From ac5a59b59c56b5d9dcff13da5c8034fb5d554abf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Sun, 28 Sep 2025 22:08:57 +0200 Subject: [PATCH 046/112] Feat: Change CippDeployContactTemplate to drawer for deploying contact templates --- .../CippDeployContactTemplateDrawer.jsx | 141 ++++++++++++++++++ .../contacts-template/deploy.jsx | 65 -------- .../contacts-template/index.jsx | 22 ++- .../email/administration/contacts/add.jsx | 131 ---------------- .../email/administration/contacts/index.js | 11 +- 5 files changed, 153 insertions(+), 217 deletions(-) create mode 100644 src/components/CippComponents/CippDeployContactTemplateDrawer.jsx delete mode 100644 src/pages/email/administration/contacts-template/deploy.jsx delete mode 100644 src/pages/email/administration/contacts/add.jsx diff --git a/src/components/CippComponents/CippDeployContactTemplateDrawer.jsx b/src/components/CippComponents/CippDeployContactTemplateDrawer.jsx new file mode 100644 index 000000000000..4ec2b0d533e1 --- /dev/null +++ b/src/components/CippComponents/CippDeployContactTemplateDrawer.jsx @@ -0,0 +1,141 @@ +import React, { useState, useEffect } from "react"; +import { Button, Divider } from "@mui/material"; +import { Grid } from "@mui/system"; +import { useForm, useFormState } from "react-hook-form"; +import { RocketLaunch } from "@mui/icons-material"; +import { CippOffCanvas } from "./CippOffCanvas"; +import CippFormComponent from "./CippFormComponent"; +import { CippFormTenantSelector } from "./CippFormTenantSelector"; +import { CippApiResults } from "./CippApiResults"; +import { ApiPostCall } from "../../api/ApiCall"; + +export const CippDeployContactTemplateDrawer = ({ + buttonText = "Deploy Contact Template", + requiredPermissions = [], + PermissionButton = Button, +}) => { + const [drawerVisible, setDrawerVisible] = useState(false); + + const formControl = useForm({ + mode: "onChange", + defaultValues: { + selectedTenants: [], + TemplateList: [], + }, + }); + + const { isValid } = useFormState({ control: formControl.control }); + + const deployTemplate = ApiPostCall({ + urlFromData: true, + relatedQueryKeys: ["DeployContactTemplates"], + }); + + // Reset form fields on successful creation + useEffect(() => { + if (deployTemplate.isSuccess) { + formControl.reset({ + selectedTenants: [], + TemplateList: [], + }); + } + }, [deployTemplate.isSuccess, formControl]); + + const handleSubmit = () => { + formControl.trigger(); + // Check if the form is valid before proceeding + if (!isValid) { + return; + } + + const formData = formControl.getValues(); + + deployTemplate.mutate({ + url: "/api/DeployContactTemplates", + data: formData, + relatedQueryKeys: ["DeployContactTemplates"], + }); + }; + + const handleCloseDrawer = () => { + setDrawerVisible(false); + formControl.reset({ + selectedTenants: [], + TemplateList: [], + }); + }; + + return ( + <> + setDrawerVisible(true)} + startIcon={} + > + {buttonText} + + + + + + } + > + + + + + + + + {/* TemplateList */} + + option, + url: "/api/ListContactTemplates", + }} + placeholder="Select a template or enter PowerShell JSON manually" + /> + + + + + + + ); +}; diff --git a/src/pages/email/administration/contacts-template/deploy.jsx b/src/pages/email/administration/contacts-template/deploy.jsx deleted file mode 100644 index 6766b209d6ba..000000000000 --- a/src/pages/email/administration/contacts-template/deploy.jsx +++ /dev/null @@ -1,65 +0,0 @@ -import { Divider } from "@mui/material"; -import { Grid } from "@mui/system"; -import { useForm } from "react-hook-form"; -import { Layout as DashboardLayout } from "/src/layouts/index.js"; -import CippFormPage from "/src/components/CippFormPages/CippFormPage"; -import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; -import { CippFormTenantSelector } from "/src/components/CippComponents/CippFormTenantSelector"; - -const Page = () => { - const formControl = useForm({ - mode: "onChange", - defaultValues: { - selectedTenants: [], - TemplateList: [], - }, - }); - - return ( - - - - - - - - - {/* TemplateList */} - - option, - url: "/api/ListContactTemplates", - }} - placeholder="Select a template or enter PowerShell JSON manually" - /> - - - - ); -}; - -Page.getLayout = (page) => {page}; - -export default Page; diff --git a/src/pages/email/administration/contacts-template/index.jsx b/src/pages/email/administration/contacts-template/index.jsx index 6257f6aa9a5c..718d0c2f8ae5 100644 --- a/src/pages/email/administration/contacts-template/index.jsx +++ b/src/pages/email/administration/contacts-template/index.jsx @@ -6,9 +6,11 @@ import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx" import { TrashIcon } from "@heroicons/react/24/outline"; import { GitHub, Edit } from "@mui/icons-material"; import { ApiGetCall } from "/src/api/ApiCall"; +import { CippDeployContactTemplateDrawer } from "../../../../components/CippComponents/CippDeployContactTemplateDrawer"; const Page = () => { const pageTitle = "Contact Templates"; + const cardButtonPermissions = ["Exchange.Contact.ReadWrite"]; const integrations = ApiGetCall({ url: "/api/ListExtensionsConfig", queryKey: "Integrations", @@ -72,12 +74,12 @@ const Page = () => { color: "danger", }, { - label: "Edit Contact Template", - link: "/email/administration/contacts-template/edit?id=[GUID]", - icon: , - color: "success", - target: "_self", - }, + label: "Edit Contact Template", + link: "/email/administration/contacts-template/edit?id=[GUID]", + icon: , + color: "success", + target: "_self", + }, ]; const simpleColumns = ["name", "contactTemplateName", "GUID"]; @@ -90,13 +92,7 @@ const Page = () => { simpleColumns={simpleColumns} cardButton={ <> - + + + } /> From 44442ecda675ba5d34d8b9085f215d0b1879c9db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Sun, 28 Sep 2025 22:16:22 +0200 Subject: [PATCH 047/112] Refactor: Remove unused imports from contacts page --- src/pages/email/administration/contacts/index.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/pages/email/administration/contacts/index.js b/src/pages/email/administration/contacts/index.js index d17fd91826ff..b61034ace42c 100644 --- a/src/pages/email/administration/contacts/index.js +++ b/src/pages/email/administration/contacts/index.js @@ -1,9 +1,7 @@ import { useMemo } from "react"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; -import { Edit, RocketLaunch } from "@mui/icons-material"; -import { Button } from "@mui/material"; -import Link from "next/link"; +import { Edit } from "@mui/icons-material"; import TrashIcon from "@heroicons/react/24/outline/TrashIcon"; import { CippAddContactDrawer } from "../../../../components/CippComponents/CippAddContactDrawer"; import { CippDeployContactTemplateDrawer } from "../../../../components/CippComponents/CippDeployContactTemplateDrawer"; From 5c09b75f94c02e974d42f2019eb22ae7cedb5138 Mon Sep 17 00:00:00 2001 From: Peter Vive <95594418+PeterVive@users.noreply.github.com> Date: Mon, 29 Sep 2025 20:16:02 +0200 Subject: [PATCH 048/112] Added "Clear Capabilities Cache" action button on tenants page. --- src/pages/tenant/administration/tenants/index.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/pages/tenant/administration/tenants/index.js b/src/pages/tenant/administration/tenants/index.js index 4d386c1c5eca..c21587c72044 100644 --- a/src/pages/tenant/administration/tenants/index.js +++ b/src/pages/tenant/administration/tenants/index.js @@ -1,7 +1,7 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { TabbedLayout } from "/src/layouts/TabbedLayout"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; -import { Edit } from "@mui/icons-material"; +import { DeleteOutline, Edit } from "@mui/icons-material"; import tabOptions from "./tabOptions"; const Page = () => { @@ -34,6 +34,15 @@ const Page = () => { link: "/tenant/standards/manage-drift/configuration-backup?tenantFilter=[defaultDomainName]", icon: , }, + { + label: "Delete Capabilities Cache", + type: "GET", + url: "/api/RemoveTenantCapabilitiesCache", + data: { defaultDomainName: "defaultDomainName" }, + confirmText: "Are you sure you want to delete the capabilities cache for this tenant?", + color: "info", + icon: , + }, ]; return ( From 942655bf8baf88b0634e5f8a432b0819899f3eda Mon Sep 17 00:00:00 2001 From: John Duprey Date: Mon, 29 Sep 2025 14:27:09 -0400 Subject: [PATCH 049/112] disable change status button for defender controls --- src/pages/tenant/administration/securescore/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/tenant/administration/securescore/index.js b/src/pages/tenant/administration/securescore/index.js index f6169e00005b..2fc2f04346e2 100644 --- a/src/pages/tenant/administration/securescore/index.js +++ b/src/pages/tenant/administration/securescore/index.js @@ -128,6 +128,7 @@ const Page = () => { createDialog.handleOpen(); }} variant="contained" + disabled={secureScoreControl.controlName.startsWith("scid_")} > Change Status From 65a7d6aa56964da4752f74fd8a91510b41fda391 Mon Sep 17 00:00:00 2001 From: Peter Vive <95594418+PeterVive@users.noreply.github.com> Date: Mon, 29 Sep 2025 21:43:06 +0200 Subject: [PATCH 050/112] Update Azurite launch task to ensure en-US locale. This ensure correct timestamp formatting in tables during dev - regardless of local formatting. --- .vscode/tasks.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 1bcdb9744954..91a6a5c671f2 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -10,6 +10,12 @@ "type": "shell", "command": "azurite --location ../", "isBackground": true, + "options": { + "env": { + "LC_ALL": "en-US.UTF-8", + "LANG": "en-US" + } + }, "problemMatcher": { "pattern": [ { From 48a83587ae8a79466d0cd5a9657e749d2191ee0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Mon, 29 Sep 2025 20:18:02 +0200 Subject: [PATCH 051/112] Feat: Update confirmation messages to include device name --- src/pages/endpoint/MEM/devices/index.js | 41 ++++++++++++++----------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/src/pages/endpoint/MEM/devices/index.js b/src/pages/endpoint/MEM/devices/index.js index 14b4c6feae12..99f1090815b4 100644 --- a/src/pages/endpoint/MEM/devices/index.js +++ b/src/pages/endpoint/MEM/devices/index.js @@ -18,6 +18,7 @@ import { Recycling, ManageAccounts, } from "@mui/icons-material"; +import { validate } from "numeral"; const Page = () => { const pageTitle = "Devices"; @@ -68,7 +69,7 @@ const Page = () => { }, }, ], - confirmText: "Select the User to set as the primary user for this device", + confirmText: "Select the User to set as the primary user for [deviceName]", }, { label: "Rename Device", @@ -98,7 +99,7 @@ const Page = () => { GUID: "id", Action: "syncDevice", }, - confirmText: "Are you sure you want to sync this device?", + confirmText: "Are you sure you want to sync [deviceName]?", }, { label: "Reboot Device", @@ -109,7 +110,7 @@ const Page = () => { GUID: "id", Action: "rebootNow", }, - confirmText: "Are you sure you want to reboot this device?", + confirmText: "Are you sure you want to reboot [deviceName]?", }, { label: "Locate Device", @@ -120,7 +121,7 @@ const Page = () => { GUID: "id", Action: "locateDevice", }, - confirmText: "Are you sure you want to locate this device?", + confirmText: "Are you sure you want to locate [deviceName]?", }, { label: "Retrieve LAPs password", @@ -141,7 +142,7 @@ const Page = () => { GUID: "id", Action: "RotateLocalAdminPassword", }, - confirmText: "Are you sure you want to rotate the password for this device?", + confirmText: "Are you sure you want to rotate the password for [deviceName]?", }, { label: "Retrieve BitLocker Keys", @@ -163,7 +164,7 @@ const Page = () => { Action: "WindowsDefenderScan", quickScan: false, }, - confirmText: "Are you sure you want to perform a full scan on this device?", + confirmText: "Are you sure you want to perform a full scan on [deviceName]?", }, { label: "Windows Defender Quick Scan", @@ -175,7 +176,7 @@ const Page = () => { Action: "WindowsDefenderScan", quickScan: true, }, - confirmText: "Are you sure you want to perform a quick scan on this device?", + confirmText: "Are you sure you want to perform a quick scan on [deviceName]?", }, { label: "Update Windows Defender", @@ -187,7 +188,7 @@ const Page = () => { Action: "windowsDefenderUpdateSignatures", }, confirmText: - "Are you sure you want to update the Windows Defender signatures for this device?", + "Are you sure you want to update the Windows Defender signatures for [deviceName]?", }, { label: "Generate logs and ship to MEM", @@ -196,8 +197,9 @@ const Page = () => { url: "/api/ExecDeviceAction", data: { GUID: "id", - Action: "CreateDeviceLogCollectionRequest", + Action: "createDeviceLogCollectionRequest", }, + condition: (row) => row.operatingSystem === "Windows", confirmText: "Are you sure you want to generate logs and ship these to MEM?", }, { @@ -210,7 +212,8 @@ const Page = () => { Action: "cleanWindowsDevice", keepUserData: false, }, - confirmText: "Are you sure you want to Fresh Start this device?", + condition: (row) => row.operatingSystem === "Windows", + confirmText: "Are you sure you want to Fresh Start [deviceName]?", }, { label: "Fresh Start (Do not remove user data)", @@ -222,7 +225,8 @@ const Page = () => { Action: "cleanWindowsDevice", keepUserData: true, }, - confirmText: "Are you sure you want to Fresh Start this device?", + condition: (row) => row.operatingSystem === "Windows", + confirmText: "Are you sure you want to Fresh Start [deviceName]?", }, { label: "Wipe Device, keep enrollment data", @@ -235,7 +239,7 @@ const Page = () => { keepUserData: false, keepEnrollmentData: true, }, - confirmText: "Are you sure you want to wipe this device, and retain enrollment data?", + confirmText: "Are you sure you want to wipe [deviceName], and retain enrollment data?", }, { label: "Wipe Device, remove enrollment data", @@ -248,7 +252,7 @@ const Page = () => { keepUserData: false, keepEnrollmentData: false, }, - confirmText: "Are you sure you want to wipe this device, and remove enrollment data?", + confirmText: "Are you sure you want to wipe [deviceName], and remove enrollment data?", }, { label: "Wipe Device, keep enrollment data, and continue at powerloss", @@ -263,7 +267,7 @@ const Page = () => { useProtectedWipe: true, }, confirmText: - "Are you sure you want to wipe this device? This will retain enrollment data. Continuing at powerloss may cause boot issues if wipe is interrupted.", + "Are you sure you want to wipe [deviceName]? This will retain enrollment data. Continuing at powerloss may cause boot issues if wipe is interrupted.", }, { label: "Wipe Device, remove enrollment data, and continue at powerloss", @@ -278,7 +282,7 @@ const Page = () => { useProtectedWipe: true, }, confirmText: - "Are you sure you want to wipe this device? This will also remove enrollment data. Continuing at powerloss may cause boot issues if wipe is interrupted.", + "Are you sure you want to wipe [deviceName]? This will also remove enrollment data. Continuing at powerloss may cause boot issues if wipe is interrupted.", }, { label: "Autopilot Reset", @@ -291,7 +295,8 @@ const Page = () => { keepUserData: "false", keepEnrollmentData: "true", }, - confirmText: "Are you sure you want to Autopilot Reset this device?", + condition: (row) => row.operatingSystem === "Windows", + confirmText: "Are you sure you want to Autopilot Reset [deviceName]?", }, { label: "Delete device", @@ -302,7 +307,7 @@ const Page = () => { GUID: "id", Action: "delete", }, - confirmText: "Are you sure you want to retire this device?", + confirmText: "Are you sure you want to retire [deviceName]?", }, { label: "Retire device", @@ -313,7 +318,7 @@ const Page = () => { GUID: "id", Action: "retire", }, - confirmText: "Are you sure you want to retire this device?", + confirmText: "Are you sure you want to retire [deviceName]?", }, ]; From 448567acbb61185bdfd0ac7378f255fae21ee5a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Mon, 29 Sep 2025 22:34:49 +0200 Subject: [PATCH 052/112] Feat: Add conditions for Windows OS in confirmation prompts and update messages to include device name --- src/pages/endpoint/MEM/devices/index.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/pages/endpoint/MEM/devices/index.js b/src/pages/endpoint/MEM/devices/index.js index 99f1090815b4..5c04d976a859 100644 --- a/src/pages/endpoint/MEM/devices/index.js +++ b/src/pages/endpoint/MEM/devices/index.js @@ -18,7 +18,6 @@ import { Recycling, ManageAccounts, } from "@mui/icons-material"; -import { validate } from "numeral"; const Page = () => { const pageTitle = "Devices"; @@ -131,6 +130,7 @@ const Page = () => { data: { GUID: "azureADDeviceId", }, + condition: (row) => row.operatingSystem === "Windows", confirmText: "Are you sure you want to retrieve the local admin password?", }, { @@ -142,6 +142,7 @@ const Page = () => { GUID: "id", Action: "RotateLocalAdminPassword", }, + condition: (row) => row.operatingSystem === "Windows", confirmText: "Are you sure you want to rotate the password for [deviceName]?", }, { @@ -152,6 +153,7 @@ const Page = () => { data: { GUID: "azureADDeviceId", }, + condition: (row) => row.operatingSystem === "Windows", confirmText: "Are you sure you want to retrieve the BitLocker keys?", }, { @@ -164,6 +166,7 @@ const Page = () => { Action: "WindowsDefenderScan", quickScan: false, }, + condition: (row) => row.operatingSystem === "Windows", confirmText: "Are you sure you want to perform a full scan on [deviceName]?", }, { @@ -176,6 +179,7 @@ const Page = () => { Action: "WindowsDefenderScan", quickScan: true, }, + condition: (row) => row.operatingSystem === "Windows", confirmText: "Are you sure you want to perform a quick scan on [deviceName]?", }, { @@ -187,6 +191,7 @@ const Page = () => { GUID: "id", Action: "windowsDefenderUpdateSignatures", }, + condition: (row) => row.operatingSystem === "Windows", confirmText: "Are you sure you want to update the Windows Defender signatures for [deviceName]?", }, @@ -200,7 +205,8 @@ const Page = () => { Action: "createDeviceLogCollectionRequest", }, condition: (row) => row.operatingSystem === "Windows", - confirmText: "Are you sure you want to generate logs and ship these to MEM?", + confirmText: + "Are you sure you want to generate logs for device [deviceName] and ship these to MEM?", }, { label: "Fresh Start (Remove user data)", @@ -239,6 +245,7 @@ const Page = () => { keepUserData: false, keepEnrollmentData: true, }, + condition: (row) => row.operatingSystem === "Windows", confirmText: "Are you sure you want to wipe [deviceName], and retain enrollment data?", }, { @@ -252,6 +259,7 @@ const Page = () => { keepUserData: false, keepEnrollmentData: false, }, + condition: (row) => row.operatingSystem === "Windows", confirmText: "Are you sure you want to wipe [deviceName], and remove enrollment data?", }, { @@ -266,6 +274,7 @@ const Page = () => { keepUserData: false, useProtectedWipe: true, }, + condition: (row) => row.operatingSystem === "Windows", confirmText: "Are you sure you want to wipe [deviceName]? This will retain enrollment data. Continuing at powerloss may cause boot issues if wipe is interrupted.", }, @@ -281,6 +290,7 @@ const Page = () => { keepUserData: false, useProtectedWipe: true, }, + condition: (row) => row.operatingSystem === "Windows", confirmText: "Are you sure you want to wipe [deviceName]? This will also remove enrollment data. Continuing at powerloss may cause boot issues if wipe is interrupted.", }, From fe121f8c17f8f9124aafaa714e2060f8885b6167 Mon Sep 17 00:00:00 2001 From: Peter Vive <95594418+PeterVive@users.noreply.github.com> Date: Mon, 29 Sep 2025 23:07:55 +0200 Subject: [PATCH 053/112] Add UPN column back to CA Vacation Mode list. Fixes #4647 --- src/pages/tenant/conditional/deploy-vacation/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/tenant/conditional/deploy-vacation/index.js b/src/pages/tenant/conditional/deploy-vacation/index.js index a4a95667cfee..6d9bb2a1b88c 100644 --- a/src/pages/tenant/conditional/deploy-vacation/index.js +++ b/src/pages/tenant/conditional/deploy-vacation/index.js @@ -41,6 +41,7 @@ const Page = () => { actions={actions} simpleColumns={[ "Tenant", + "Parameters.Users.addedFields.userPrincipalName", "Name", "TaskState", "ScheduledTime", From 67ff1f7ad2f45282db51d2d3838646593f6d2ffc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Mon, 29 Sep 2025 23:40:14 +0200 Subject: [PATCH 054/112] Refactor: Remove redundant conditions for Windows OS in scan actions and update confirmation text for delete action --- src/pages/endpoint/MEM/devices/index.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/pages/endpoint/MEM/devices/index.js b/src/pages/endpoint/MEM/devices/index.js index 5c04d976a859..8c2cf07cbcfa 100644 --- a/src/pages/endpoint/MEM/devices/index.js +++ b/src/pages/endpoint/MEM/devices/index.js @@ -166,7 +166,6 @@ const Page = () => { Action: "WindowsDefenderScan", quickScan: false, }, - condition: (row) => row.operatingSystem === "Windows", confirmText: "Are you sure you want to perform a full scan on [deviceName]?", }, { @@ -179,7 +178,6 @@ const Page = () => { Action: "WindowsDefenderScan", quickScan: true, }, - condition: (row) => row.operatingSystem === "Windows", confirmText: "Are you sure you want to perform a quick scan on [deviceName]?", }, { @@ -191,7 +189,6 @@ const Page = () => { GUID: "id", Action: "windowsDefenderUpdateSignatures", }, - condition: (row) => row.operatingSystem === "Windows", confirmText: "Are you sure you want to update the Windows Defender signatures for [deviceName]?", }, @@ -317,7 +314,7 @@ const Page = () => { GUID: "id", Action: "delete", }, - confirmText: "Are you sure you want to retire [deviceName]?", + confirmText: "Are you sure you want to delete [deviceName]?", }, { label: "Retire device", From cda1ee548bcc82b6d6200d654462f09350029d9c Mon Sep 17 00:00:00 2001 From: Zac Richards <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 30 Sep 2025 14:12:31 +0800 Subject: [PATCH 055/112] Add internal reference field to GDAP invites Introduces a 'Reference' field to the invite creation form and displays it in the invites table. Also adds an action to update the internal reference for existing invites, improving tracking and context for each invite. --- .../tenant/gdap-management/invites/add.js | 12 ++++++++- .../tenant/gdap-management/invites/index.js | 26 +++++++++++++++++-- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/src/pages/tenant/gdap-management/invites/add.js b/src/pages/tenant/gdap-management/invites/add.js index f7b09d1d0a59..22a45e334d1d 100644 --- a/src/pages/tenant/gdap-management/invites/add.js +++ b/src/pages/tenant/gdap-management/invites/add.js @@ -67,6 +67,7 @@ const Page = () => { if (!formControl.formState.isValid) return; const eachInvite = Array.from({ length: values.inviteCount }, (_, i) => ({ roleMappings: values.roleMappings.value, + Reference: values.Reference, })); addInvites.mutate({ @@ -180,7 +181,7 @@ const Page = () => { }} /> - + { required={true} /> + + + {selectedTemplate?.value && ( diff --git a/src/pages/tenant/gdap-management/invites/index.js b/src/pages/tenant/gdap-management/invites/index.js index 99dd16053bf1..ce1384c92fb5 100644 --- a/src/pages/tenant/gdap-management/invites/index.js +++ b/src/pages/tenant/gdap-management/invites/index.js @@ -5,13 +5,34 @@ import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx" import { Button } from "@mui/material"; import { Add } from "@mui/icons-material"; import Link from "next/link"; -import { TrashIcon } from "@heroicons/react/24/outline"; +import { TrashIcon, PencilIcon } from "@heroicons/react/24/outline"; const pageTitle = "GDAP Invites"; -const simpleColumns = ["Timestamp", "RowKey", "InviteUrl", "OnboardingUrl", "RoleMappings"]; +const simpleColumns = ["Timestamp", "RowKey", "Reference", "Technician", "InviteUrl", "OnboardingUrl", "RoleMappings"]; const apiUrl = "/api/ListGDAPInvite"; const actions = [ + { + label: "Update Internal Reference", + url: "/api/ExecGDAPInvite", + type: "POST", + icon: , + confirmText: "Are you sure you want to update the internal reference for this invite?", + data: { + Action: "Update", + InviteId: "RowKey", + }, + fields: [ + { + label: "Internal Reference", + name: "Reference", + type: "textField", + required: false, + helperText: "Enter an internal reference/note for this GDAP invite (e.g., client name, ticket number).", + }, + ], + relatedQueryKeys: ["ListGDAPInvite"], + }, { label: "Delete Invite", url: "/api/ExecGDAPInvite", @@ -23,6 +44,7 @@ const actions = [ Action: "Delete", InviteId: "RowKey", }, + relatedQueryKeys: ["ListGDAPInvite"], }, ]; From 22032e721d7487d32b985962570634b19a58532e Mon Sep 17 00:00:00 2001 From: Zac Richards <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 30 Sep 2025 23:35:28 +0800 Subject: [PATCH 056/112] Add new transport rule creation page Introduces a new page for creating email transport rules with a comprehensive form supporting conditions, actions, and exceptions. Also adds a button to the transport rules list for easy navigation to the new rule creation page. --- src/pages/email/transport/list-rules/index.js | 7 + src/pages/email/transport/new-rules/add.jsx | 811 ++++++++++++++++++ 2 files changed, 818 insertions(+) create mode 100644 src/pages/email/transport/new-rules/add.jsx diff --git a/src/pages/email/transport/list-rules/index.js b/src/pages/email/transport/list-rules/index.js index 5faec982d216..db1fcf5a507e 100644 --- a/src/pages/email/transport/list-rules/index.js +++ b/src/pages/email/transport/list-rules/index.js @@ -104,6 +104,13 @@ const Page = () => { > Deploy Template + } /> diff --git a/src/pages/email/transport/new-rules/add.jsx b/src/pages/email/transport/new-rules/add.jsx new file mode 100644 index 000000000000..76083d0ca3d7 --- /dev/null +++ b/src/pages/email/transport/new-rules/add.jsx @@ -0,0 +1,811 @@ +import React, { useState, useEffect, cloneElement } from "react"; +import { Divider, Typography, Alert, Box } from "@mui/material"; +import { Grid } from "@mui/system"; +import { useForm, useWatch } from "react-hook-form"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import CippFormPage from "/src/components/CippFormPages/CippFormPage"; +import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; +import { CippFormDomainSelector } from "/src/components/CippComponents/CippFormDomainSelector"; +import { useSettings } from "/src/hooks/use-settings"; + +const AddTransportRule = () => { + const currentTenant = useSettings().currentTenant; + const formControl = useForm({ + mode: "onChange", + defaultValues: { + Enabled: true, + Mode: { value: "Enforce", label: "Enforce" }, + StopRuleProcessing: false, + SenderAddressLocation: { value: "Header", label: "Header" }, + applyToAllMessages: false, + tenantFilter: currentTenant + }, + }); + + const [selectedConditions, setSelectedConditions] = useState([]); + const [selectedActions, setSelectedActions] = useState([]); + const [selectedExceptions, setSelectedExceptions] = useState([]); + + const conditionTypeWatch = useWatch({ control: formControl.control, name: "conditionType" }); + const actionTypeWatch = useWatch({ control: formControl.control, name: "actionType" }); + const exceptionTypeWatch = useWatch({ control: formControl.control, name: "exceptionType" }); + const applyToAllMessagesWatch = useWatch({ control: formControl.control, name: "applyToAllMessages" }); + + // Update selected conditions when conditionType changes + useEffect(() => { + setSelectedConditions(conditionTypeWatch || []); + }, [conditionTypeWatch]); + + useEffect(() => { + setSelectedActions(actionTypeWatch || []); + }, [actionTypeWatch]); + + useEffect(() => { + setSelectedExceptions(exceptionTypeWatch || []); + }, [exceptionTypeWatch]); + + // Handle "Apply to all messages" logic + useEffect(() => { + if (applyToAllMessagesWatch) { + // Clear conditions when "apply to all" is enabled + formControl.setValue("conditionType", []); + setSelectedConditions([]); + } + }, [applyToAllMessagesWatch, formControl]); + + // Disable "Apply to all messages" when conditions are selected + useEffect(() => { + if (conditionTypeWatch && conditionTypeWatch.length > 0) { + formControl.setValue("applyToAllMessages", false); + } + }, [conditionTypeWatch, formControl]); + + // Condition options + const conditionOptions = [ + { value: "From", label: "The sender is..." }, + { value: "FromScope", label: "The sender is located..." }, + { value: "SentTo", label: "The recipient is..." }, + { value: "SentToScope", label: "The recipient is located..." }, + { value: "SubjectContainsWords", label: "Subject contains words..." }, + { value: "SubjectMatchesPatterns", label: "Subject matches patterns..." }, + { value: "SubjectOrBodyContainsWords", label: "Subject or body contains words..." }, + { value: "SubjectOrBodyMatchesPatterns", label: "Subject or body matches patterns..." }, + { value: "FromAddressContainsWords", label: "Sender address contains words..." }, + { value: "FromAddressMatchesPatterns", label: "Sender address matches patterns..." }, + { value: "AttachmentContainsWords", label: "Attachment content contains words..." }, + { value: "AttachmentMatchesPatterns", label: "Attachment content matches patterns..." }, + { value: "AttachmentExtensionMatchesWords", label: "Attachment extension is..." }, + { value: "AttachmentSizeOver", label: "Attachment size is greater than..." }, + { value: "MessageSizeOver", label: "Message size is greater than..." }, + { value: "SCLOver", label: "SCL is greater than or equal to..." }, + { value: "WithImportance", label: "Message importance is..." }, + { value: "MessageTypeMatches", label: "Message type is..." }, + { value: "SenderDomainIs", label: "Sender domain is..." }, + { value: "RecipientDomainIs", label: "Recipient domain is..." }, + { value: "HeaderContainsWords", label: "Message header contains words..." }, + { value: "HeaderMatchesPatterns", label: "Message header matches patterns..." }, + ]; + + // Action options + const actionOptions = [ + { value: "DeleteMessage", label: "Delete the message without notifying anyone" }, + { value: "Quarantine", label: "Quarantine the message" }, + { value: "RedirectMessageTo", label: "Redirect the message to..." }, + { value: "BlindCopyTo", label: "Add recipients to the Bcc box..." }, + { value: "CopyTo", label: "Add recipients to the Cc box..." }, + { value: "ModerateMessageByUser", label: "Forward the message for approval to..." }, + { value: "ModerateMessageByManager", label: "Forward the message for approval to the sender's manager" }, + { value: "RejectMessage", label: "Reject the message with explanation..." }, + { value: "PrependSubject", label: "Prepend the subject with..." }, + { value: "SetSCL", label: "Set spam confidence level (SCL) to..." }, + { value: "SetHeader", label: "Set message header..." }, + { value: "RemoveHeader", label: "Remove message header..." }, + { value: "ApplyClassification", label: "Apply message classification..." }, + { value: "ApplyHtmlDisclaimer", label: "Apply HTML disclaimer..." }, + { value: "GenerateIncidentReport", label: "Generate incident report and send to..." }, + { value: "GenerateNotification", label: "Notify the sender with a message..." }, + { value: "ApplyOME", label: "Apply Office 365 Message Encryption" }, + ]; + + const renderConditionField = (condition) => { + const conditionValue = condition.value || condition; + const conditionLabel = condition.label || condition; + + switch (conditionValue) { + case "From": + case "SentTo": + return ( + + `${option.displayName} (${option.userPrincipalName})`, + valueField: "userPrincipalName", + dataKey: "Results", + }} + /> + + ); + + case "FromScope": + case "SentToScope": + return ( + + + + ); + + case "WithImportance": + return ( + + + + ); + + case "MessageTypeMatches": + return ( + + + + ); + + case "SCLOver": + return ( + + ({ + value: i.toString(), + label: i.toString(), + }))} + /> + + ); + + case "AttachmentSizeOver": + case "MessageSizeOver": + return ( + + + + ); + + case "SenderDomainIs": + case "RecipientDomainIs": + return ( + + + + ); + + case "HeaderContainsWords": + case "HeaderMatchesPatterns": + return ( + + + + + + + + + + + ); + + default: + return ( + + + + ); + } + }; + + const renderActionField = (action) => { + const actionValue = action.value || action; + const actionLabel = action.label || action; + + switch (actionValue) { + case "DeleteMessage": + case "Quarantine": + case "ModerateMessageByManager": + case "ApplyOME": + return ( + + + + ); + + case "RedirectMessageTo": + case "BlindCopyTo": + case "CopyTo": + case "ModerateMessageByUser": + case "GenerateIncidentReport": + return ( + + `${option.displayName} (${option.userPrincipalName})`, + valueField: "userPrincipalName", + dataKey: "Results", + }} + /> + + ); + + case "SetSCL": + return ( + + ({ + value: i.toString(), + label: i.toString(), + })), + ]} + /> + + ); + + case "RejectMessage": + return ( + + + + + + + + + + + ); + + case "SetHeader": + return ( + + + + + + + + + + + ); + + case "RemoveHeader": + return ( + + + + ); + + case "ApplyHtmlDisclaimer": + return ( + + + + + + + + + + + + + + ); + + case "PrependSubject": + case "ApplyClassification": + case "GenerateNotification": + return ( + + + + ); + + default: + return ( + + + + ); + } + }; + + const renderExceptionField = (exception) => { + const exceptionValue = exception.value || exception; + const baseCondition = exceptionValue.replace("ExceptIf", ""); + const exceptionLabel = exception.label || exception; + + // Reuse the condition rendering logic + const mockCondition = { value: baseCondition, label: exceptionLabel }; + const field = renderConditionField(mockCondition); + + // Update the field's name to include ExceptIf prefix + if (field) { + return cloneElement(field, { + key: exceptionValue, + children: React.Children.map(field.props.children, (child) => { + if (child?.type === CippFormComponent) { + return cloneElement(child, { + name: exceptionValue, + }); + } + // For Grid containers with multiple fields (like HeaderContains) + if (child?.type === Grid && child.props.container) { + return cloneElement(child, { + children: React.Children.map(child.props.children, (gridChild) => { + if (gridChild?.props?.children?.type === CippFormComponent) { + const formComponent = gridChild.props.children; + const originalName = formComponent.props.name; + const newName = originalName.includes("MessageHeader") + ? `ExceptIf${originalName}` + : exceptionValue; + return cloneElement(gridChild, { + children: cloneElement(formComponent, { + name: newName, + }), + }); + } + return gridChild; + }), + }); + } + return child; + }), + }); + } + return null; + }; + + return ( + + + + + {/* Basic Information */} + + + Basic Information + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {/* Conditions */} + + + Apply this rule if... + + + + + + + + {applyToAllMessagesWatch && ( + + + This rule will apply to ALL inbound and outbound messages + for the entire organization. + + + )} + + {!applyToAllMessagesWatch && ( + <> + + + Select one or more conditions to target specific messages. If you want this rule to + apply to all messages, enable "Apply to all messages" above. + + + + + + + + {selectedConditions.map((condition) => renderConditionField(condition))} + + )} + + + + {/* Actions */} + + + Do the following... + + + + + + + + {selectedActions.map((action) => renderActionField(action))} + + + + {/* Exceptions */} + + + Except if... (optional) + + + + + ({ + value: `ExceptIf${opt.value}`, + label: opt.label, + }))} + /> + + + {selectedExceptions.map((exception) => renderExceptionField(exception))} + + + + {/* Advanced Settings */} + + + Advanced Settings + + + + + + + + + + + + + + + + + + + + + + ); +}; + +AddTransportRule.getLayout = (page) => {page}; + +export default AddTransportRule; \ No newline at end of file From a92716889404a42850419a2806923e352ce6a2a7 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 30 Sep 2025 12:23:56 -0400 Subject: [PATCH 057/112] remove console log fixes #4733 nitpicking --- src/utils/get-cipp-filter-variant.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/utils/get-cipp-filter-variant.js b/src/utils/get-cipp-filter-variant.js index 5aceb2ac71bc..189005739274 100644 --- a/src/utils/get-cipp-filter-variant.js +++ b/src/utils/get-cipp-filter-variant.js @@ -40,7 +40,6 @@ export const getCippFilterVariant = (providedColumnKeys, arg) => { //First key based filters switch (tailKey) { case "assignedLicenses": - console.log("Assigned Licenses Filter", sampleValue, values); // Extract unique licenses from the data if available let filterSelectOptions = []; From 871817df69f9293b5150e2cc7e8b6e1239d7c60f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Tue, 30 Sep 2025 19:46:17 +0200 Subject: [PATCH 058/112] Feat: Add RestrictedUsers alert for users restricted from sending email due to spam limits --- src/data/alerts.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/data/alerts.json b/src/data/alerts.json index bb336d180e10..7ca2114492e7 100644 --- a/src/data/alerts.json +++ b/src/data/alerts.json @@ -234,5 +234,11 @@ "inputName": "InputValue", "recommendedRunInterval": "1d", "description": "Monitors tenant alignment scores against standards templates and alerts when the alignment percentage falls below the specified threshold. This helps ensure compliance across all managed tenants." + }, + { + "name": "RestrictedUsers", + "label": "Alert on users restricted from sending email", + "recommendedRunInterval": "30m", + "description": "Monitors for users who have been restricted from sending email due to exceeding outbound spam limits. These users typically indicate a compromised account that needs immediate attention." } ] From 0c8797f44b186669c07560bf0d018ccc17bb9e47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Tue, 30 Sep 2025 21:49:50 +0200 Subject: [PATCH 059/112] Feat: Add Restricted Users page to manage email sending restrictions --- src/layouts/config.js | 5 ++ .../administration/restricted-users/index.js | 66 +++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 src/pages/email/administration/restricted-users/index.js diff --git a/src/layouts/config.js b/src/layouts/config.js index db0508ce56f3..1e70fe8622f9 100644 --- a/src/layouts/config.js +++ b/src/layouts/config.js @@ -583,6 +583,11 @@ export const nativeMenuItems = [ path: "/email/administration/quarantine", permissions: ["Exchange.SpamFilter.*"], }, + { + title: "Restricted Users", + path: "/email/administration/restricted-users", + permissions: ["Exchange.Mailbox.*"], + }, { title: "Tenant Allow/Block Lists", path: "/email/administration/tenant-allow-block-lists", diff --git a/src/pages/email/administration/restricted-users/index.js b/src/pages/email/administration/restricted-users/index.js new file mode 100644 index 000000000000..248090f55f6c --- /dev/null +++ b/src/pages/email/administration/restricted-users/index.js @@ -0,0 +1,66 @@ +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { Alert } from "@mui/material"; +import { Block as BlockIcon } from "@mui/icons-material"; + +const Page = () => { + const pageTitle = "Restricted Users"; + + const actions = [ + { + label: "Unblock User", + type: "POST", + icon: , + confirmText: + "Are you sure you want to unblock [SenderAddress]? Unblocking can take up to 1 hour. Make sure you have secured the account before proceeding.", + url: "/api/ExecRemoveRestrictedUser", + data: { SenderAddress: "SenderAddress" }, + color: "success", + }, + ]; + + const simpleColumns = [ + "SenderAddress", + "BlockType", + "CreatedDatetime", + "ChangedDatetime", + "TemporaryBlock", + "InternalCount", + "ExternalCount", + ]; + + return ( + <> + + + Users in this list have been restricted from sending email due to exceeding outbound spam + limits. + +
+ This typically indicates a compromised account. Before unblocking, ensure you have properly + secured the account. Recommended actions include: +
    +
  • Checked for suspicious sign-ins and activities
  • +
  • Reviewed their mailbox rules and forwarding settings
  • +
  • + Investigated any unusual mailbox activity, such as unexpected sent items via message + trace +
  • +
  • Reset the user's password if compromised
  • +
  • Enabled MFA on the account if not already enabled
  • +
+
+ + + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; From 705a4be3e5399c8b865ec890c6f77933637215c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Tue, 30 Sep 2025 22:00:59 +0200 Subject: [PATCH 060/112] Feat: Update DashboardLayout to reflect if it supports all tenants in email administration pages --- .../email/administration/contacts/index.js | 2 +- .../administration/deleted-mailboxes/index.js | 2 +- .../exchange-retention/policies/index.js | 92 ++++---- .../exchange-retention/tags/index.js | 101 +++++---- .../email/administration/mailboxes/index.js | 2 +- .../administration/restricted-users/index.js | 2 +- .../tenant-allow-block-lists/index.js | 2 +- .../SharedMailboxEnabledAccount/index.js | 6 +- .../reports/antiphishing-filters/index.js | 2 +- .../reports/global-address-list/index.js | 2 +- .../reports/mailbox-cas-settings/index.js | 2 +- .../email/reports/malware-filters/index.js | 2 +- .../reports/safeattachments-filters/index.js | 2 +- .../resources/management/equipment/index.js | 2 +- .../resources/management/list-rooms/index.js | 2 +- .../resources/management/room-lists/index.js | 2 +- .../spamfilter/list-connectionfilter/index.js | 2 +- .../list-quarantine-policies/index.js | 205 ++++++++---------- .../email/spamfilter/list-spamfilter/index.js | 2 +- .../email/transport/deploy-rules/index.js | 17 -- .../email/transport/list-connectors/index.js | 2 +- 21 files changed, 213 insertions(+), 240 deletions(-) delete mode 100644 src/pages/email/transport/deploy-rules/index.js diff --git a/src/pages/email/administration/contacts/index.js b/src/pages/email/administration/contacts/index.js index b61034ace42c..c8ab6314827e 100644 --- a/src/pages/email/administration/contacts/index.js +++ b/src/pages/email/administration/contacts/index.js @@ -55,5 +55,5 @@ const Page = () => { ); }; -Page.getLayout = (page) => {page}; +Page.getLayout = (page) => {page}; export default Page; diff --git a/src/pages/email/administration/deleted-mailboxes/index.js b/src/pages/email/administration/deleted-mailboxes/index.js index d276a2ab998f..488c783dd71f 100644 --- a/src/pages/email/administration/deleted-mailboxes/index.js +++ b/src/pages/email/administration/deleted-mailboxes/index.js @@ -15,5 +15,5 @@ const Page = () => { ); }; -Page.getLayout = (page) => {page}; +Page.getLayout = (page) => {page}; export default Page; diff --git a/src/pages/email/administration/exchange-retention/policies/index.js b/src/pages/email/administration/exchange-retention/policies/index.js index c465475879cf..ede3ed7034ea 100644 --- a/src/pages/email/administration/exchange-retention/policies/index.js +++ b/src/pages/email/administration/exchange-retention/policies/index.js @@ -13,54 +13,56 @@ const Page = () => { const pageTitle = "Retention Policy Management"; const tenant = useSettings().currentTenant; - const actions = useMemo(() => [ - { - label: "Edit Policy", - link: "/email/administration/exchange-retention/policies/policy?name=[Name]", - multiPost: false, - postEntireRow: true, - icon: , - color: "warning", - }, - { - label: "Delete Policy", - type: "POST", - url: "/api/ExecManageRetentionPolicies", - confirmText: "Are you sure you want to delete retention policy [Name]? This action cannot be undone.", - color: "danger", - icon: , - customDataformatter: (rows) => { - const policies = Array.isArray(rows) ? rows : [rows]; - return { - DeletePolicies: policies.map(policy => policy.Name), - tenantFilter: tenant, - }; + const actions = useMemo( + () => [ + { + label: "Edit Policy", + link: "/email/administration/exchange-retention/policies/policy?name=[Name]", + multiPost: false, + postEntireRow: true, + icon: , + color: "warning", }, - }, - ], [tenant]); + { + label: "Delete Policy", + type: "POST", + url: "/api/ExecManageRetentionPolicies", + confirmText: + "Are you sure you want to delete retention policy [Name]? This action cannot be undone.", + color: "danger", + icon: , + customDataformatter: (rows) => { + const policies = Array.isArray(rows) ? rows : [rows]; + return { + DeletePolicies: policies.map((policy) => policy.Name), + tenantFilter: tenant, + }; + }, + }, + ], + [tenant] + ); - const simpleColumns = useMemo(() => [ - "Name", - "IsDefault", - "IsDefaultArbitrationMailbox", - "RetentionPolicyTagLinks" - ], []); + const simpleColumns = useMemo( + () => ["Name", "IsDefault", "IsDefaultArbitrationMailbox", "RetentionPolicyTagLinks"], + [] + ); - const cardButton = useMemo(() => ( - - ), []); + const cardButton = useMemo( + () => ( + + ), + [] + ); return ( - + { ); }; -Page.getLayout = (page) => {page}; +Page.getLayout = (page) => {page}; -export default Page; \ No newline at end of file +export default Page; diff --git a/src/pages/email/administration/exchange-retention/tags/index.js b/src/pages/email/administration/exchange-retention/tags/index.js index e8299b401eca..8daa866d0c8a 100644 --- a/src/pages/email/administration/exchange-retention/tags/index.js +++ b/src/pages/email/administration/exchange-retention/tags/index.js @@ -13,56 +13,63 @@ const Page = () => { const pageTitle = "Retention Tag Management"; const tenant = useSettings().currentTenant; - const actions = useMemo(() => [ - { - label: "Edit Tag", - link: "/email/administration/exchange-retention/tags/tag?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, - }; + const actions = useMemo( + () => [ + { + label: "Edit Tag", + link: "/email/administration/exchange-retention/tags/tag?name=[Name]", + multiPost: false, + postEntireRow: true, + icon: , + color: "warning", }, - }, - ], [tenant]); + { + 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 simpleColumns = useMemo( + () => [ + "Name", + "Type", + "RetentionAction", + "AgeLimitForRetention", + "RetentionEnabled", + "Comment", + ], + [] + ); - const cardButton = useMemo(() => ( - - ), []); + const cardButton = useMemo( + () => ( + + ), + [] + ); return ( - + { ); }; -Page.getLayout = (page) => {page}; +Page.getLayout = (page) => {page}; -export default Page; \ No newline at end of file +export default Page; diff --git a/src/pages/email/administration/mailboxes/index.js b/src/pages/email/administration/mailboxes/index.js index 3687c04b512e..c2abbe1b854e 100644 --- a/src/pages/email/administration/mailboxes/index.js +++ b/src/pages/email/administration/mailboxes/index.js @@ -64,6 +64,6 @@ const Page = () => { ); }; -Page.getLayout = (page) => {page}; +Page.getLayout = (page) => {page}; export default Page; diff --git a/src/pages/email/administration/restricted-users/index.js b/src/pages/email/administration/restricted-users/index.js index 248090f55f6c..a2ca33028b47 100644 --- a/src/pages/email/administration/restricted-users/index.js +++ b/src/pages/email/administration/restricted-users/index.js @@ -61,6 +61,6 @@ const Page = () => { ); }; -Page.getLayout = (page) => {page}; +Page.getLayout = (page) => {page}; export default Page; diff --git a/src/pages/email/administration/tenant-allow-block-lists/index.js b/src/pages/email/administration/tenant-allow-block-lists/index.js index 4770163895fa..bf0f3e3275bf 100644 --- a/src/pages/email/administration/tenant-allow-block-lists/index.js +++ b/src/pages/email/administration/tenant-allow-block-lists/index.js @@ -58,5 +58,5 @@ const Page = () => { ); }; -Page.getLayout = (page) => {page}; +Page.getLayout = (page) => {page}; export default Page; diff --git a/src/pages/email/reports/SharedMailboxEnabledAccount/index.js b/src/pages/email/reports/SharedMailboxEnabledAccount/index.js index dbd167e86105..4323f53703aa 100644 --- a/src/pages/email/reports/SharedMailboxEnabledAccount/index.js +++ b/src/pages/email/reports/SharedMailboxEnabledAccount/index.js @@ -37,13 +37,13 @@ const Page = () => { filters={[ { id: "accountEnabled", - value: "Yes" - } + value: "Yes", + }, ]} /> ); }; -Page.getLayout = (page) => {page}; +Page.getLayout = (page) => {page}; export default Page; diff --git a/src/pages/email/reports/antiphishing-filters/index.js b/src/pages/email/reports/antiphishing-filters/index.js index 23cee4f3b5dd..c691d035088b 100644 --- a/src/pages/email/reports/antiphishing-filters/index.js +++ b/src/pages/email/reports/antiphishing-filters/index.js @@ -98,6 +98,6 @@ const Page = () => { }; // Layout configuration: ensure page uses DashboardLayout -Page.getLayout = (page) => {page}; +Page.getLayout = (page) => {page}; export default Page; diff --git a/src/pages/email/reports/global-address-list/index.js b/src/pages/email/reports/global-address-list/index.js index 314fb23f66f0..c8b7ff18d0d7 100644 --- a/src/pages/email/reports/global-address-list/index.js +++ b/src/pages/email/reports/global-address-list/index.js @@ -81,6 +81,6 @@ const Page = () => { ); }; -Page.getLayout = (page) => {page}; +Page.getLayout = (page) => {page}; export default Page; diff --git a/src/pages/email/reports/mailbox-cas-settings/index.js b/src/pages/email/reports/mailbox-cas-settings/index.js index 9d7eaccd5f57..10cb93963b58 100644 --- a/src/pages/email/reports/mailbox-cas-settings/index.js +++ b/src/pages/email/reports/mailbox-cas-settings/index.js @@ -24,6 +24,6 @@ const Page = () => { // No actions were specified in the original code, so no actions are added here. // No off-canvas configuration was provided or specified in the original code. -Page.getLayout = (page) => {page}; +Page.getLayout = (page) => {page}; export default Page; diff --git a/src/pages/email/reports/malware-filters/index.js b/src/pages/email/reports/malware-filters/index.js index 4e0c638e0f8a..20f09161b7d1 100644 --- a/src/pages/email/reports/malware-filters/index.js +++ b/src/pages/email/reports/malware-filters/index.js @@ -89,6 +89,6 @@ const Page = () => { - Additional action for "Delete Rule" is commented for developer convenience, pending further instruction. */ -Page.getLayout = (page) => {page}; +Page.getLayout = (page) => {page}; export default Page; diff --git a/src/pages/email/reports/safeattachments-filters/index.js b/src/pages/email/reports/safeattachments-filters/index.js index 3821410fe6e0..a35d329be6ca 100644 --- a/src/pages/email/reports/safeattachments-filters/index.js +++ b/src/pages/email/reports/safeattachments-filters/index.js @@ -88,6 +88,6 @@ const Page = () => { ); }; -Page.getLayout = (page) => {page}; +Page.getLayout = (page) => {page}; export default Page; diff --git a/src/pages/email/resources/management/equipment/index.js b/src/pages/email/resources/management/equipment/index.js index abaae1ebaec3..538143f24b96 100644 --- a/src/pages/email/resources/management/equipment/index.js +++ b/src/pages/email/resources/management/equipment/index.js @@ -72,6 +72,6 @@ const Page = () => { ); }; -Page.getLayout = (page) => {page}; +Page.getLayout = (page) => {page}; export default Page; diff --git a/src/pages/email/resources/management/list-rooms/index.js b/src/pages/email/resources/management/list-rooms/index.js index 256dfaf3f3f7..61201421734a 100644 --- a/src/pages/email/resources/management/list-rooms/index.js +++ b/src/pages/email/resources/management/list-rooms/index.js @@ -75,6 +75,6 @@ const Page = () => { ); }; -Page.getLayout = (page) => {page}; +Page.getLayout = (page) => {page}; export default Page; diff --git a/src/pages/email/resources/management/room-lists/index.js b/src/pages/email/resources/management/room-lists/index.js index 1ee0cd11e346..ce8573687897 100644 --- a/src/pages/email/resources/management/room-lists/index.js +++ b/src/pages/email/resources/management/room-lists/index.js @@ -64,6 +64,6 @@ const Page = () => { ); }; -Page.getLayout = (page) => {page}; +Page.getLayout = (page) => {page}; export default Page; diff --git a/src/pages/email/spamfilter/list-connectionfilter/index.js b/src/pages/email/spamfilter/list-connectionfilter/index.js index 0ebf8fc05c5b..3ce94c37ec8b 100644 --- a/src/pages/email/spamfilter/list-connectionfilter/index.js +++ b/src/pages/email/spamfilter/list-connectionfilter/index.js @@ -58,5 +58,5 @@ const Page = () => { ); }; -Page.getLayout = (page) => {page}; +Page.getLayout = (page) => {page}; export default Page; diff --git a/src/pages/email/spamfilter/list-quarantine-policies/index.js b/src/pages/email/spamfilter/list-quarantine-policies/index.js index fa301cb631f4..471404b92b64 100644 --- a/src/pages/email/spamfilter/list-quarantine-policies/index.js +++ b/src/pages/email/spamfilter/list-quarantine-policies/index.js @@ -23,7 +23,7 @@ import { ApiGetCall } from "/src/api/ApiCall"; const Page = () => { const pageTitle = "Quarantine Policies"; const { currentTenant } = useSettings(); - + const createDialog = useDialog(); // Use ApiGetCall directly as a hook @@ -40,7 +40,6 @@ const Page = () => { const hasGlobalQuarantinePolicyData = !!globalQuarantineData; - if (hasGlobalQuarantinePolicyData) { globalQuarantineData.EndUserSpamNotificationFrequency = globalQuarantineData?.EndUserSpamNotificationFrequency === "P1D" @@ -49,42 +48,40 @@ const Page = () => { ? "Weekly" : globalQuarantineData?.EndUserSpamNotificationFrequency === "PT4H" ? "4 hours" - : globalQuarantineData?.EndUserSpamNotificationFrequency + : globalQuarantineData?.EndUserSpamNotificationFrequency; } const multiLanguagePropertyItems = hasGlobalQuarantinePolicyData - ? ( - Array.isArray(globalQuarantineData?.MultiLanguageSetting) && globalQuarantineData.MultiLanguageSetting.length > 0 - ? globalQuarantineData.MultiLanguageSetting.map((language, idx) => ({ - language: language == "Default" ? "English_USA" - : language == "English" ? "English_GB" - : language, - senderDisplayName: - globalQuarantineData.MultiLanguageSenderName[idx] && - globalQuarantineData.MultiLanguageSenderName[idx].trim() !== "" - ? globalQuarantineData.MultiLanguageSenderName[idx] - : "None", - subject: - globalQuarantineData.EsnCustomSubject[idx] && - globalQuarantineData.EsnCustomSubject[idx].trim() !== "" - ? globalQuarantineData.EsnCustomSubject[idx] - : "None", - disclaimer: - globalQuarantineData.MultiLanguageCustomDisclaimer[idx] && - globalQuarantineData.MultiLanguageCustomDisclaimer[idx].trim() !== "" - ? globalQuarantineData.MultiLanguageCustomDisclaimer[idx] - : "None", - })) - : [ - { - language: "None", - senderDisplayName: "None", - subject: "None", - disclaimer: "None", - }, - ] - ) - : []; + ? Array.isArray(globalQuarantineData?.MultiLanguageSetting) && + globalQuarantineData.MultiLanguageSetting.length > 0 + ? globalQuarantineData.MultiLanguageSetting.map((language, idx) => ({ + language: + language == "Default" ? "English_USA" : language == "English" ? "English_GB" : language, + senderDisplayName: + globalQuarantineData.MultiLanguageSenderName[idx] && + globalQuarantineData.MultiLanguageSenderName[idx].trim() !== "" + ? globalQuarantineData.MultiLanguageSenderName[idx] + : "None", + subject: + globalQuarantineData.EsnCustomSubject[idx] && + globalQuarantineData.EsnCustomSubject[idx].trim() !== "" + ? globalQuarantineData.EsnCustomSubject[idx] + : "None", + disclaimer: + globalQuarantineData.MultiLanguageCustomDisclaimer[idx] && + globalQuarantineData.MultiLanguageCustomDisclaimer[idx].trim() !== "" + ? globalQuarantineData.MultiLanguageCustomDisclaimer[idx] + : "None", + })) + : [ + { + language: "None", + senderDisplayName: "None", + subject: "None", + disclaimer: "None", + }, + ] + : []; const buttonCardActions = [ <> @@ -92,34 +89,28 @@ const Page = () => { Edit Settings - { - GlobalQuarantinePolicy.refetch(); + { + GlobalQuarantinePolicy.refetch(); + }} + > + - - - - + + + - + , ]; // Actions to perform (Edit,Delete Policy) @@ -140,8 +131,8 @@ const Page = () => { type: "autoComplete", name: "ReleaseActionPreference", label: "Select release action preference", - multiple : false, - creatable : false, + multiple: false, + creatable: false, options: [ { label: "Release", value: "Release" }, { label: "Request Release", value: "RequestRelease" }, @@ -156,7 +147,7 @@ const Page = () => { type: "switch", name: "Preview", label: "Preview", - }, + }, { type: "switch", name: "BlockSender", @@ -196,11 +187,11 @@ const Page = () => { }, confirmText: ( <> - - Are you sure you want to delete this policy? - + Are you sure you want to delete this policy? - Note: This will delete the Quarantine policy, even if it is currently in use.
+ Note: This will delete the Quarantine policy, even if it is currently + in use. +
Removing the Admin and User Access it applies to emails.
@@ -224,7 +215,7 @@ const Page = () => { "Id", // Policy Name/Id "Name", // Policy Name "EndUserQuarantinePermissions", - "Guid", + "Guid", "Builtin", "WhenCreated", // Creation Date "WhenChanged", // Last Modified Date @@ -245,7 +236,6 @@ const Page = () => { }, ]; - const customLanguageOffcanvas = multiLanguagePropertyItems && multiLanguagePropertyItems.length > 0 ? { @@ -269,22 +259,22 @@ const Page = () => { .map(([key, value]) => ( - {key.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase())} - - - {value} + {key + .replace(/([A-Z])/g, " $1") + .replace(/^./, (str) => str.toUpperCase())} + {value} ))}
), })), - } + }, } : {}; - // Simplified columns for the table + // Simplified columns for the table const simpleColumns = [ "Name", "ReleaseActionPreference", @@ -298,8 +288,6 @@ const Page = () => { "WhenChanged", ]; - - // Prepare data for CippInfoBar as a const to clean up the code const infoBarData = [ { @@ -310,31 +298,28 @@ const Page = () => { { icon: , data: hasGlobalQuarantinePolicyData - ? (globalQuarantineData?.OrganizationBrandingEnabled - ? "Enabled" - : "Disabled" - ) + ? globalQuarantineData?.OrganizationBrandingEnabled + ? "Enabled" + : "Disabled" : "n/a", name: "Branding", }, { icon: , - data: hasGlobalQuarantinePolicyData - ? (globalQuarantineData?.EndUserSpamNotificationCustomFromAddress - ? globalQuarantineData?.EndUserSpamNotificationCustomFromAddress - : "None") - : "n/a" , + data: hasGlobalQuarantinePolicyData + ? globalQuarantineData?.EndUserSpamNotificationCustomFromAddress + ? globalQuarantineData?.EndUserSpamNotificationCustomFromAddress + : "None" + : "n/a", name: "Custom Sender Address", }, { icon: , toolTip: "More Info", data: hasGlobalQuarantinePolicyData - ? ( - multiLanguagePropertyItems.length > 0 - ? multiLanguagePropertyItems.map(item => item.language).join(", ") - : "None" - ) + ? multiLanguagePropertyItems.length > 0 + ? multiLanguagePropertyItems.map((item) => item.language).join(", ") + : "None" : "n/a", name: "Custom Language", ...customLanguageOffcanvas, @@ -351,14 +336,11 @@ const Page = () => { > - + - -
+ +
{ filters={filterList} simpleColumns={simpleColumns} cardButton={ - <> - - - } + <> + + + } /> { Identity: "Guid", }, relatedQueryKeys: [`GlobalQuarantinePolicy-${currentTenant}`], - confirmText: - "Are you sure you want to update Global Quarantine settings?", + confirmText: "Are you sure you want to update Global Quarantine settings?", }} // row={globalQuarantineData} row={globalQuarantineData} @@ -402,8 +383,8 @@ const Page = () => { type: "autoComplete", name: "EndUserSpamNotificationFrequency", label: "Notification Frequency", - multiple : false, - creatable : false, + multiple: false, + creatable: false, required: true, options: [ { label: "4 hours", value: "PT4H" }, @@ -430,6 +411,6 @@ const Page = () => { }; // Layout configuration: ensure page uses DashboardLayout -Page.getLayout = (page) => {page}; +Page.getLayout = (page) => {page}; export default Page; diff --git a/src/pages/email/spamfilter/list-spamfilter/index.js b/src/pages/email/spamfilter/list-spamfilter/index.js index f6961ffc7d04..d6a2fe54dd01 100644 --- a/src/pages/email/spamfilter/list-spamfilter/index.js +++ b/src/pages/email/spamfilter/list-spamfilter/index.js @@ -114,5 +114,5 @@ const Page = () => { ); }; -Page.getLayout = (page) => {page}; +Page.getLayout = (page) => {page}; export default Page; diff --git a/src/pages/email/transport/deploy-rules/index.js b/src/pages/email/transport/deploy-rules/index.js deleted file mode 100644 index a12e12a5a58f..000000000000 --- a/src/pages/email/transport/deploy-rules/index.js +++ /dev/null @@ -1,17 +0,0 @@ - -import { Layout as DashboardLayout } from "/src/layouts/index.js"; - -const Page = () => { - const pageTitle = "Deploy Transport rule"; - - return ( -
-

{pageTitle}

-

This is a placeholder page for the deploy transport rule section.

-
- ); -}; - -Page.getLayout = (page) => {page}; - -export default Page; diff --git a/src/pages/email/transport/list-connectors/index.js b/src/pages/email/transport/list-connectors/index.js index 6f2f9fe11539..fdb3ca7d92f4 100644 --- a/src/pages/email/transport/list-connectors/index.js +++ b/src/pages/email/transport/list-connectors/index.js @@ -99,5 +99,5 @@ const Page = () => { ); }; -Page.getLayout = (page) => {page}; +Page.getLayout = (page) => {page}; export default Page; From 861487271aade142c55ee27c09fddd03c1ed76fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Tue, 30 Sep 2025 22:11:35 +0200 Subject: [PATCH 061/112] Feat: Enhance alert message for restricted users with a link to security guidance from MS --- src/pages/email/administration/restricted-users/index.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/pages/email/administration/restricted-users/index.js b/src/pages/email/administration/restricted-users/index.js index a2ca33028b47..3f21e1f50d0a 100644 --- a/src/pages/email/administration/restricted-users/index.js +++ b/src/pages/email/administration/restricted-users/index.js @@ -37,8 +37,11 @@ const Page = () => { limits.
- This typically indicates a compromised account. Before unblocking, ensure you have properly - secured the account. Recommended actions include: + This typically indicates a compromised account.{" "} + + Before unblocking, ensure you have properly secured the account. + + Recommended actions include:
  • Checked for suspicious sign-ins and activities
  • Reviewed their mailbox rules and forwarding settings
  • From 155b7763b1a058b2d282dec66d2bf2a363478427 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Wed, 1 Oct 2025 17:49:24 +0200 Subject: [PATCH 062/112] Fix: Add preselectedEnabled prop to tenant filter and pass requiredPermissions to CippCADeployDrawer --- src/components/CippComponents/CippCADeployDrawer.jsx | 3 ++- src/pages/tenant/conditional/list-policies/index.js | 7 +++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/CippComponents/CippCADeployDrawer.jsx b/src/components/CippComponents/CippCADeployDrawer.jsx index 403c4657fa03..77e604672f9e 100644 --- a/src/components/CippComponents/CippCADeployDrawer.jsx +++ b/src/components/CippComponents/CippCADeployDrawer.jsx @@ -1,5 +1,5 @@ import { useEffect, useState, useCallback } from "react"; -import { Button, Stack, Box } from "@mui/material"; +import { Button, Stack } from "@mui/material"; import { RocketLaunch } from "@mui/icons-material"; import { useForm, useWatch } from "react-hook-form"; import { CippOffCanvas } from "./CippOffCanvas"; @@ -125,6 +125,7 @@ export const CippCADeployDrawer = ({ name="tenantFilter" required={true} disableClearable={false} + preselectedEnabled={true} allTenants={true} type="multiple" /> diff --git a/src/pages/tenant/conditional/list-policies/index.js b/src/pages/tenant/conditional/list-policies/index.js index be83cfaad658..9f22e22ce45d 100644 --- a/src/pages/tenant/conditional/list-policies/index.js +++ b/src/pages/tenant/conditional/list-policies/index.js @@ -5,12 +5,10 @@ import { Check as CheckIcon, Delete as DeleteIcon, MenuBook as MenuBookIcon, - AddModerator as AddModeratorIcon, Visibility as VisibilityIcon, Edit as EditIcon, } from "@mui/icons-material"; -import { Box, Button } from "@mui/material"; -import Link from "next/link"; +import { Box } from "@mui/material"; import CippJsonView from "../../../../components/CippFormPages/CippJSONView"; import { CippCADeployDrawer } from "../../../../components/CippComponents/CippCADeployDrawer"; @@ -18,6 +16,7 @@ import { CippCADeployDrawer } from "../../../../components/CippComponents/CippCA const Page = () => { const pageTitle = "Conditional Access"; const apiUrl = "/api/ListConditionalAccessPolicies"; + const cardButtonPermissions = ["Tenant.ConditionalAccess.ReadWrite"]; // Actions configuration const actions = [ @@ -159,7 +158,7 @@ const Page = () => { - + } title={pageTitle} From 0217f5877de09295d32b759a8b860fb4c41f94af Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Thu, 2 Oct 2025 14:05:14 +0200 Subject: [PATCH 063/112] adds removal of package --- src/pages/endpoint/MEM/list-templates/index.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/pages/endpoint/MEM/list-templates/index.js b/src/pages/endpoint/MEM/list-templates/index.js index 458f090ba53c..a987aa3e1c7c 100644 --- a/src/pages/endpoint/MEM/list-templates/index.js +++ b/src/pages/endpoint/MEM/list-templates/index.js @@ -1,7 +1,7 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; import { PencilIcon, TrashIcon } from "@heroicons/react/24/outline"; -import { Edit, GitHub, LocalOffer } from "@mui/icons-material"; +import { Edit, GitHub, LocalOffer, LocalOfferOutlined } from "@mui/icons-material"; import CippJsonView from "../../../../components/CippFormPages/CippJSONView"; import { ApiGetCall } from "/src/api/ApiCall"; import { CippPolicyImportDrawer } from "/src/components/CippComponents/CippPolicyImportDrawer.jsx"; @@ -68,6 +68,16 @@ const Page = () => { icon: , color: "info", }, + { + label: "Remove from package", + type: "POST", + url: "/api/ExecSetPackageTag", + data: { GUID: "GUID", Remove: true }, + confirmText: "Are you sure you want to remove the selected template(s) from their package?", + multiPost: true, + icon: , + color: "warning", + }, { label: "Save to GitHub", type: "POST", From 0fc71a5e706a1e7c7d1237a335173446ca8fb2da Mon Sep 17 00:00:00 2001 From: Peter Vive <95594418+PeterVive@users.noreply.github.com> Date: Thu, 2 Oct 2025 22:20:59 +0200 Subject: [PATCH 064/112] Allow standard "DisableGuests" to have inactivity period be configurable. FR https://github.com/KelvinTegelaar/CIPP/issues/4747 --- src/data/standards.json | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/data/standards.json b/src/data/standards.json index 816c41bf6c25..a35050716897 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -965,10 +965,18 @@ "name": "standards.DisableGuests", "cat": "Entra (AAD) Standards", "tag": [], - "helpText": "Blocks login for guest users that have not logged in for 90 days", - "executiveText": "Automatically disables external guest accounts that haven't been used for 90 days, reducing security risks from dormant accounts while maintaining access for active external collaborators. This helps maintain a clean user directory and reduces potential attack vectors.", - "addedComponent": [], - "label": "Disable Guest accounts that have not logged on for 90 days", + "helpText": "Blocks login for guest users that have not logged in for a number of days", + "executiveText": "Automatically disables external guest accounts that haven't been used for a number of days, reducing security risks from dormant accounts while maintaining access for active external collaborators. This helps maintain a clean user directory and reduces potential attack vectors.", + "addedComponent": [ + { + "type": "number", + "name": "standards.DisableGuests.days", + "required": true, + "defaultValue": 90, + "label": "Days of inactivity" + } + ], + "label": "Disable Guest accounts that have not logged on for a number of days", "impact": "Medium Impact", "impactColour": "warning", "addedDate": "2022-10-20", From e86660c0a277f17a5f2705344df0c4440bf685d9 Mon Sep 17 00:00:00 2001 From: Peter Vive <95594418+PeterVive@users.noreply.github.com> Date: Thu, 2 Oct 2025 22:49:36 +0200 Subject: [PATCH 065/112] Allow assignment of intune policies to custom group from table list FR #4744 --- src/pages/endpoint/MEM/list-policies/index.js | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/pages/endpoint/MEM/list-policies/index.js b/src/pages/endpoint/MEM/list-policies/index.js index bf23a754fb27..2a3b15d90c26 100644 --- a/src/pages/endpoint/MEM/list-policies/index.js +++ b/src/pages/endpoint/MEM/list-policies/index.js @@ -1,7 +1,7 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; import { Book, LaptopChromebook } from "@mui/icons-material"; -import { GlobeAltIcon, TrashIcon, UserIcon } from "@heroicons/react/24/outline"; +import { GlobeAltIcon, TrashIcon, UserIcon, UserGroupIcon } from "@heroicons/react/24/outline"; import { PermissionButton } from "/src/utils/permissions.js"; import { CippPolicyDeployDrawer } from "/src/components/CippComponents/CippPolicyDeployDrawer.jsx"; @@ -61,6 +61,26 @@ const Page = () => { icon: , color: "info", }, + { + label: "Assign to Custom Group", + type: "POST", + url: "/api/ExecAssignPolicy", + data: { + ID: "id", + type: "URLName", + }, + confirmText: "Enter the name of the group to assign this policy to. Wildcards (*) are allowed.", + icon: , + color: "info", + fields: [ + { + type: "textField", + name: "AssignTo", + label: "Group Name(s), optionally comma-separated", + placeholder: "IT-*, Sales Team", + }, + ], + }, { label: "Delete Policy", type: "POST", From ebf0911054216adc6f5c4ecb270e486566f2b890 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Fri, 3 Oct 2025 00:11:13 +0200 Subject: [PATCH 066/112] Feat:: New Teams Chat Protection settings standard --- src/data/standards.json | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/data/standards.json b/src/data/standards.json index a35050716897..4c6be119947a 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -4221,6 +4221,34 @@ "powershellEquivalent": "Set-CsTeamsMeetingPolicy -AllowAnonymousUsersToJoinMeeting $false -AllowAnonymousUsersToStartMeeting $false -AutoAdmittedUsers EveryoneInCompanyExcludingGuests -AllowPSTNUsersToBypassLobby $false -MeetingChatEnabledType EnabledExceptAnonymous -DesignatedPresenterRoleMode $DesignatedPresenterRoleMode -AllowExternalParticipantGiveRequestControl $false", "recommendedBy": ["CIS"] }, + { + "name": "standards.TeamsChatProtection", + "cat": "Teams Standards", + "tag": [], + "helpText": "Configures Teams chat protection settings including weaponizable file protection and malicious URL protection.", + "docsDescription": "Configures Teams messaging safety features to protect users from weaponizable files and malicious URLs in chats and channels. Weaponizable File Protection automatically blocks messages containing potentially dangerous file types (like .exe, .dll, .bat, etc.). Malicious URL Protection scans URLs in messages and displays warnings when potentially harmful links are detected. These protections work across internal and external collaboration scenarios.", + "executiveText": "Enables automated security protections in Microsoft Teams to block dangerous files and warn users about malicious links in chat messages. This helps protect employees from file-based attacks and phishing attempts. These safeguards work seamlessly in the background, providing essential protection without disrupting normal business communication.", + "addedComponent": [ + { + "type": "switch", + "name": "standards.TeamsChatProtection.FileTypeCheck", + "label": "Enable Weaponizable File Protection", + "defaultValue": true + }, + { + "type": "switch", + "name": "standards.TeamsChatProtection.UrlReputationCheck", + "label": "Enable Malicious URL Protection", + "defaultValue": true + } + ], + "label": "Set Teams Chat Protection Settings", + "impact": "Low Impact", + "impactColour": "info", + "addedDate": "2025-10-02", + "powershellEquivalent": "Set-CsTeamsMessagingConfiguration -FileTypeCheck 'Enabled' -UrlReputationCheck 'Enabled' -ReportIncorrectSecurityDetections 'Enabled'", + "recommendedBy": ["CIPP"] + }, { "name": "standards.TeamsEmailIntegration", "cat": "Teams Standards", From 819333fe92adb9e1e0691004cc9fd0d1884da4cf Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 2 Oct 2025 23:35:53 -0400 Subject: [PATCH 067/112] custom data manual entry support --- .../CippFormPages/CippAddEditUser.jsx | 149 +++++++++---- .../CippCustomDataMappingForm.jsx | 206 ++++++++++++++---- src/pages/cipp/custom-data/mappings/add.js | 149 +++++++------ src/pages/cipp/custom-data/mappings/edit.js | 38 +++- .../administration/users/patch-wizard.jsx | 142 ++++++++++-- 5 files changed, 515 insertions(+), 169 deletions(-) diff --git a/src/components/CippFormPages/CippAddEditUser.jsx b/src/components/CippFormPages/CippAddEditUser.jsx index fbe3f7537b30..fe5a49fb1581 100644 --- a/src/components/CippFormPages/CippAddEditUser.jsx +++ b/src/components/CippFormPages/CippAddEditUser.jsx @@ -1,4 +1,4 @@ -import { Alert, InputAdornment, Typography } from "@mui/material"; +import { Alert, Divider, InputAdornment, Typography } from "@mui/material"; import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; import { CippFormCondition } from "/src/components/CippComponents/CippFormCondition"; import { CippFormDomainSelector } from "/src/components/CippComponents/CippFormDomainSelector"; @@ -42,6 +42,22 @@ const CippAddEditUser = (props) => { waiting: !!userId, }); + // Get manual entry custom data mappings for current tenant + const manualEntryMappings = ApiGetCall({ + url: `/api/ListCustomDataMappings?sourceType=Manual Entry&directoryObject=User&tenantFilter=${tenantDomain}`, + queryKey: `ManualEntryMappings-${tenantDomain}`, + refetchOnMount: false, + refetchOnReconnect: false, + }); + + // Use mappings directly since they're already filtered by the API + const currentTenantManualMappings = useMemo(() => { + if (manualEntryMappings.isSuccess) { + return manualEntryMappings.data?.Results || []; + } + return []; + }, [manualEntryMappings.isSuccess, manualEntryMappings.data]); + // Make new list of groups by removing userGroups from tenantGroups const filteredTenantGroups = useMemo(() => { if (tenantGroups.isSuccess && userGroups.isSuccess) { @@ -410,51 +426,96 @@ const CippAddEditUser = (props) => { /> )} + {/* Manual Entry Custom Data Fields */} + {currentTenantManualMappings.length > 0 && ( + <> + + Custom Data + + {currentTenantManualMappings.map((mapping, index) => { + const fieldName = `customData.${mapping.customDataAttribute.value}`; + const fieldLabel = mapping.manualEntryFieldLabel; + const dataType = mapping.customDataAttribute.addedFields.dataType; + + // Determine field type based on the custom data attribute type + const getFieldType = (dataType) => { + switch (dataType?.toLowerCase()) { + case "boolean": + return "switch"; + case "datetime": + case "date": + return "datePicker"; + case "string": + default: + return "textField"; + } + }; + + return ( + + + + ); + })} + + )} {/* Schedule User Creation */} {formType === "add" && ( - - - - - - - - - - - - - - + <> + + + + + + + + + + + + + + + + + + )} ); diff --git a/src/components/CippFormPages/CippCustomDataMappingForm.jsx b/src/components/CippFormPages/CippCustomDataMappingForm.jsx index bf595e701e97..4fceb43b6295 100644 --- a/src/components/CippFormPages/CippCustomDataMappingForm.jsx +++ b/src/components/CippFormPages/CippCustomDataMappingForm.jsx @@ -11,6 +11,7 @@ import { getCippTranslation } from "/src/utils/get-cipp-translation"; const CippCustomDataMappingForm = ({ formControl }) => { const selectedAttribute = useWatch({ control: formControl.control, name: "customDataAttribute" }); + const selectedDirectoryObjectType = useWatch({ control: formControl.control, name: "directoryObjectType", @@ -19,19 +20,35 @@ const CippCustomDataMappingForm = ({ formControl }) => { control: formControl.control, name: "extensionSyncDataset", }); + const selectedSourceType = useWatch({ + control: formControl.control, + name: "sourceType", + }); + const selectedManualEntryFieldLabel = useWatch({ + control: formControl.control, + name: "manualEntryFieldLabel", + }); + + console.log("Selected directory object type: ", selectedDirectoryObjectType); const staticTargetTypes = [{ value: "user", label: "User" }]; + // Top-level source type selection + const sourceTypeField = { + name: "sourceType", + label: "Source Type", + type: "autoComplete", + required: true, + multiple: false, + placeholder: "Select a Source Type", + options: [ + { value: "extensionSync", label: "Extension Sync" }, + { value: "manualEntry", label: "Manual Entry" }, + ], + }; + + // Extension Sync specific fields const sourceFields = [ - { - name: "sourceType", - label: "Source Type", - type: "autoComplete", - required: true, - multiple: false, - placeholder: "Select a Source Type", - options: [{ value: "extensionSync", label: "Extension Sync" }], - }, { name: "extensionSyncDataset", label: "Extension Sync Dataset", @@ -77,6 +94,58 @@ const CippCustomDataMappingForm = ({ formControl }) => { }, ]; + // Manual Entry specific fields + const manualEntryFields = [ + { + name: "manualEntryFieldLabel", + label: "Field Label", + type: "textField", + required: true, + placeholder: "Enter field label (e.g., Employee ID, Department)", + }, + { + name: "directoryObjectType", + label: "Directory Object Type", + type: "autoComplete", + required: true, + placeholder: "Select an Object Type", + options: staticTargetTypes, + multiple: false, + creatable: false, + }, + { + name: "customDataAttribute", + label: "Attribute", + type: "autoComplete", + required: true, + placeholder: "Select an Attribute", + api: { + url: "/api/ExecCustomData?Action=ListAvailableAttributes", + queryKey: "CustomAttributes", + dataKey: "Results", + dataFilter: (options) => { + if (!selectedDirectoryObjectType?.value) return options; + return options.filter( + (option) => + option?.addedFields?.targetObject?.toLowerCase() === + selectedDirectoryObjectType?.value?.toLowerCase() + ); + }, + valueField: "name", + labelField: "name", + showRefresh: true, + addedField: { + type: "type", + targetObject: "targetObject", + dataType: "dataType", + isMultiValued: "isMultiValued", + }, + }, + multiple: false, + sortOptions: true, + }, + ]; + const destinationFields = [ { name: "directoryObjectType", @@ -143,44 +212,76 @@ const CippCustomDataMappingForm = ({ formControl }) => { - Source Details + Source Type - {sourceFields.map((field, index) => ( - <> - {field?.condition ? ( - - - - ) : ( - - )} - - ))} - - - - - Destination Details - - {destinationFields.map((field, index) => ( - <> - {field?.condition ? ( - - - - ) : ( - - )} - - ))} + + + {selectedSourceType?.value === "extensionSync" && ( + <> + + + Source Details + + {sourceFields.map((field, index) => ( + <> + {field?.condition ? ( + + + + ) : ( + + )} + + ))} + + + + + Destination Details + + {destinationFields.map((field, index) => ( + <> + {field?.condition ? ( + + + + ) : ( + + )} + + ))} + + + + )} + + {selectedSourceType?.value === "manualEntry" && ( + + + Manual Entry Configuration + + {manualEntryFields.map((field, index) => ( + + ))} + + + )} - {selectedExtensionSyncDataset && ( + {selectedExtensionSyncDataset && selectedSourceType?.value === "extensionSync" && ( { /> )} + {selectedSourceType?.value === "manualEntry" && selectedManualEntryFieldLabel && ( + + )} + {selectedAttribute && ( { ); }; -export default CippCustomDataMappingForm; \ No newline at end of file +export default CippCustomDataMappingForm; diff --git a/src/pages/cipp/custom-data/mappings/add.js b/src/pages/cipp/custom-data/mappings/add.js index d672d42d888d..805cbe12caa4 100644 --- a/src/pages/cipp/custom-data/mappings/add.js +++ b/src/pages/cipp/custom-data/mappings/add.js @@ -1,61 +1,88 @@ -import { Layout as DashboardLayout } from "/src/layouts/index.js"; -import { useForm, useFormState } from "react-hook-form"; -import { ApiPostCall } from "/src/api/ApiCall"; -import { useRouter } from "next/router"; -import { Button, Stack, CardContent, CardActions } from "@mui/material"; - -import CippPageCard from "/src/components/CippCards/CippPageCard"; -import { CippApiResults } from "/src/components/CippComponents/CippApiResults"; -import CippCustomDataMappingForm from "/src/components/CippFormPages/CippCustomDataMappingForm"; - -const Page = () => { - const router = useRouter(); - const formControl = useForm({ - mode: "onChange", - }); - - const formState = useFormState({ control: formControl.control }); - - const addMappingApi = ApiPostCall({ - urlFromData: true, - relatedQueryKeys: ["MappingsListPage"], - }); - - const handleAddMapping = (data) => { - addMappingApi.mutate({ - url: "/api/ExecCustomData", - data: { - Action: "AddEditMapping", - Mapping: data, - }, - }); - }; - - return ( - - - - - - - - - - - - - ); -}; - -Page.getLayout = (page) => {page}; - -export default Page; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { useForm, useFormState } from "react-hook-form"; +import { ApiPostCall } from "/src/api/ApiCall"; +import { useRouter } from "next/router"; +import { Button, Stack, CardContent, CardActions } from "@mui/material"; + +import CippPageCard from "/src/components/CippCards/CippPageCard"; +import { CippApiResults } from "/src/components/CippComponents/CippApiResults"; +import CippCustomDataMappingForm from "/src/components/CippFormPages/CippCustomDataMappingForm"; + +const Page = () => { + const router = useRouter(); + const formControl = useForm({ + mode: "onChange", + }); + + const formState = useFormState({ control: formControl.control }); + + const addMappingApi = ApiPostCall({ + urlFromData: true, + relatedQueryKeys: ["MappingsListPage"], + }); + + const handleAddMapping = (data) => { + // Filter data based on source type to only include relevant fields + let filteredData; + + if (data.sourceType?.value === "manualEntry") { + // For manual entry, only include these fields + filteredData = { + sourceType: data.sourceType, + manualEntryFieldLabel: data.manualEntryFieldLabel, + directoryObjectType: data.directoryObjectType, + customDataAttribute: data.customDataAttribute, + tenantFilter: data.tenantFilter, + }; + } else if (data.sourceType?.value === "extensionSync") { + // For extension sync, include the original fields + filteredData = { + sourceType: data.sourceType, + extensionSyncDataset: data.extensionSyncDataset, + extensionSyncProperty: data.extensionSyncProperty, + directoryObjectType: data.directoryObjectType, + customDataAttribute: data.customDataAttribute, + tenantFilter: data.tenantFilter, + }; + } else { + // Fallback to all data if source type is not recognized + filteredData = data; + } + + addMappingApi.mutate({ + url: "/api/ExecCustomData", + data: { + Action: "AddEditMapping", + Mapping: filteredData, + }, + }); + }; + + return ( + + + + + + + + + + + + + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; diff --git a/src/pages/cipp/custom-data/mappings/edit.js b/src/pages/cipp/custom-data/mappings/edit.js index 10bce76f56f2..55a2bbc648e8 100644 --- a/src/pages/cipp/custom-data/mappings/edit.js +++ b/src/pages/cipp/custom-data/mappings/edit.js @@ -3,13 +3,7 @@ import { useForm, useFormState } from "react-hook-form"; import { useRouter } from "next/router"; import { useEffect } from "react"; import { ApiPostCall, ApiGetCall } from "/src/api/ApiCall"; -import { - Button, - Stack, - CardContent, - CardActions, - Skeleton, -} from "@mui/material"; +import { Button, Stack, CardContent, CardActions, Skeleton } from "@mui/material"; import CippPageCard from "/src/components/CippCards/CippPageCard"; import { CippApiResults } from "/src/components/CippComponents/CippApiResults"; @@ -39,11 +33,39 @@ const Page = () => { }); const handleEditMapping = (data) => { + // Filter data based on source type to only include relevant fields + let filteredData; + + if (data.sourceType?.value === "manualEntry") { + // For manual entry, only include these fields + filteredData = { + sourceType: data.sourceType, + manualEntryFieldLabel: data.manualEntryFieldLabel, + directoryObjectType: data.directoryObjectType, + customDataAttribute: data.customDataAttribute, + tenantFilter: data.tenantFilter, + }; + } else if (data.sourceType?.value === "extensionSync") { + // For extension sync, include the original fields + filteredData = { + sourceType: data.sourceType, + extensionSyncDataset: data.extensionSyncDataset, + extensionSyncProperty: data.extensionSyncProperty, + directoryObjectType: data.directoryObjectType, + customDataAttribute: data.customDataAttribute, + tenantFilter: data.tenantFilter, + }; + } else { + // Fallback to all data if source type is not recognized + filteredData = data; + } + editMappingApi.mutate({ url: "/api/ExecCustomData", data: { Action: "AddEditMapping", - Mapping: { ...data, id }, // Include the ID for editing + id: id, // ID at top level for PowerShell function + Mapping: filteredData, }, }); }; diff --git a/src/pages/identity/administration/users/patch-wizard.jsx b/src/pages/identity/administration/users/patch-wizard.jsx index 8ea240386296..4fd9ce9be729 100644 --- a/src/pages/identity/administration/users/patch-wizard.jsx +++ b/src/pages/identity/administration/users/patch-wizard.jsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, useMemo } from "react"; import { useRouter } from "next/router"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; import CippWizardPage from "/src/components/CippWizard/CippWizardPage.jsx"; @@ -17,7 +17,7 @@ import { Autocomplete, } from "@mui/material"; import { CippWizardStepButtons } from "/src/components/CippWizard/CippWizardStepButtons"; -import { ApiPostCall } from "/src/api/ApiCall"; +import { ApiPostCall, ApiGetCall } from "/src/api/ApiCall"; import { CippApiResults } from "/src/components/CippComponents/CippApiResults"; import { CippDataTable } from "/src/components/CippTable/CippDataTable"; import { Delete } from "@mui/icons-material"; @@ -176,9 +176,48 @@ const UsersDisplayStep = (props) => { // Step 2: Property selection and input const PropertySelectionStep = (props) => { - const { onNextStep, onPreviousStep, formControl, currentStep } = props; + const { onNextStep, onPreviousStep, formControl, currentStep, users } = props; const [selectedProperties, setSelectedProperties] = useState([]); + // Get unique tenant domains from users + const tenantDomains = + [...new Set(users?.map((user) => user.Tenant || user.tenantFilter).filter(Boolean))] || []; + + // Fetch custom data mappings for all tenants + const customDataMappings = ApiGetCall({ + url: + tenantDomains.length > 0 + ? `/api/ListCustomDataMappings?sourceType=Manual Entry&directoryObject=User&tenantFilter=${tenantDomains[0]}` + : null, + queryKey: `ManualEntryMappings-${tenantDomains.join(",")}`, + enabled: tenantDomains.length > 0, + refetchOnMount: false, + refetchOnReconnect: false, + }); + + // Process custom data mappings into property format + const customDataProperties = useMemo(() => { + if (customDataMappings.isSuccess && customDataMappings.data?.Results) { + return customDataMappings.data.Results.filter((mapping) => { + // Only include single-value properties, filter out multivalue ones + const dataType = mapping.customDataAttribute.addedFields.dataType?.toLowerCase(); + const isMultiValue = mapping.customDataAttribute.addedFields.isMultiValue; + return !isMultiValue && dataType !== "collection"; + }).map((mapping) => ({ + property: mapping.customDataAttribute.value, // Use the actual attribute name, not nested under customData + label: `${mapping.manualEntryFieldLabel} (Custom)`, + type: mapping.customDataAttribute.addedFields.dataType?.toLowerCase() || "string", + isCustomData: true, + })); + } + return []; + }, [customDataMappings.isSuccess, customDataMappings.data]); + + // Combine standard properties with custom data properties + const allProperties = useMemo(() => { + return [...PATCHABLE_PROPERTIES, ...customDataProperties]; + }, [customDataProperties]); + // Register form fields formControl.register("selectedProperties", { required: true }); formControl.register("propertyValues", { required: false }); @@ -191,7 +230,7 @@ const PropertySelectionStep = (props) => { }; const renderPropertyInput = (propertyName) => { - const property = PATCHABLE_PROPERTIES.find((p) => p.property === propertyName); + const property = allProperties.find((p) => p.property === propertyName); const currentValue = formControl.getValues("propertyValues")?.[propertyName]; if (property?.type === "boolean") { @@ -245,7 +284,15 @@ const PropertySelectionStep = (props) => { Select Properties to update Choose which user properties you want to modify and provide the new values. + {customDataProperties.length > 0 && ( + <> Custom data fields are available based on your tenant's manual entry mappings. + )} + {customDataMappings.isLoading && ( + + Loading custom data mappings... + + )} { options={[ { property: "select-all", - label: `Select All (${PATCHABLE_PROPERTIES.length} properties)`, + label: `Select All (${allProperties.length} properties)`, isSelectAll: true, }, - ...PATCHABLE_PROPERTIES, + ...allProperties, ]} - value={PATCHABLE_PROPERTIES.filter((prop) => selectedProperties.includes(prop.property))} + value={allProperties.filter((prop) => selectedProperties.includes(prop.property))} onChange={(event, newValue) => { // Check if "Select All" was clicked const selectAllOption = newValue.find((option) => option.isSelectAll); if (selectAllOption) { // If Select All is in the new value, select all properties - const allSelected = selectedProperties.length === PATCHABLE_PROPERTIES.length; - const newProperties = allSelected ? [] : PATCHABLE_PROPERTIES.map((p) => p.property); + const allSelected = selectedProperties.length === allProperties.length; + const newProperties = allSelected ? [] : allProperties.map((p) => p.property); setSelectedProperties(newProperties); formControl.setValue("selectedProperties", newProperties); @@ -305,10 +352,9 @@ const PropertySelectionStep = (props) => { isOptionEqualToValue={(option, value) => option.property === value.property} size="small" renderOption={(props, option, { selected }) => { - const isAllSelected = selectedProperties.length === PATCHABLE_PROPERTIES.length; + const isAllSelected = selectedProperties.length === allProperties.length; const isIndeterminate = - selectedProperties.length > 0 && - selectedProperties.length < PATCHABLE_PROPERTIES.length; + selectedProperties.length > 0 && selectedProperties.length < allProperties.length; if (option.isSelectAll) { return ( @@ -390,7 +436,7 @@ const PropertySelectionStep = (props) => { // Step 3: Confirmation const ConfirmationStep = (props) => { - const { lastStep, formControl, onPreviousStep, currentStep, users } = props; + const { lastStep, formControl, onPreviousStep, currentStep, users, allProperties } = props; const formValues = formControl.getValues(); const { selectedProperties = [], propertyValues = {} } = formValues; @@ -412,11 +458,31 @@ const ConfirmationStep = (props) => { id: user.id, tenantFilter: user.Tenant || user.tenantFilter, }; + selectedProperties.forEach((propName) => { if (propertyValues[propName] !== undefined && propertyValues[propName] !== "") { - userData[propName] = propertyValues[propName]; + // Handle dot notation properties (e.g., "extension_abc123.customField") + if (propName.includes('.')) { + const parts = propName.split('.'); + let current = userData; + + // Navigate to the nested object, creating it if it doesn't exist + for (let i = 0; i < parts.length - 1; i++) { + if (!current[parts[i]]) { + current[parts[i]] = {}; + } + current = current[parts[i]]; + } + + // Set the final property value + current[parts[parts.length - 1]] = propertyValues[propName]; + } else { + // Handle regular properties + userData[propName] = propertyValues[propName]; + } } }); + return userData; }); @@ -459,7 +525,7 @@ const ConfirmationStep = (props) => { {selectedProperties.map((propName) => { - const property = PATCHABLE_PROPERTIES.find((p) => p.property === propName); + const property = allProperties.find((p) => p.property === propName); const value = propertyValues[propName]; const displayValue = property?.type === "boolean" ? (value ? "Yes" : "No") : value || "Not set"; @@ -565,6 +631,45 @@ const Page = () => { } }, [router.query.users]); + // Get unique tenant domains from users + const tenantDomains = useMemo(() => { + return [...new Set(users?.map(user => user.Tenant || user.tenantFilter).filter(Boolean))] || []; + }, [users]); + + // Fetch custom data mappings for all tenants + const customDataMappings = ApiGetCall({ + url: tenantDomains.length > 0 ? `/api/ListCustomDataMappings?sourceType=Manual Entry&directoryObject=User&tenantFilter=${tenantDomains[0]}` : null, + queryKey: `ManualEntryMappings-${tenantDomains.join(',')}`, + enabled: tenantDomains.length > 0, + refetchOnMount: false, + refetchOnReconnect: false, + }); + + // Process custom data mappings into property format + const customDataProperties = useMemo(() => { + if (customDataMappings.isSuccess && customDataMappings.data?.Results) { + return customDataMappings.data.Results + .filter(mapping => { + // Only include single-value properties, filter out multivalue ones + const dataType = mapping.customDataAttribute.addedFields.dataType?.toLowerCase(); + const isMultiValue = mapping.customDataAttribute.addedFields.isMultiValue; + return !isMultiValue && dataType !== 'collection'; + }) + .map(mapping => ({ + property: mapping.customDataAttribute.value, // Use the actual attribute name, not nested under customData + label: `${mapping.manualEntryFieldLabel} (Custom)`, + type: mapping.customDataAttribute.addedFields.dataType?.toLowerCase() || "string", + isCustomData: true, + })); + } + return []; + }, [customDataMappings.isSuccess, customDataMappings.data]); + + // Combine standard properties with custom data properties + const allProperties = useMemo(() => { + return [...PATCHABLE_PROPERTIES, ...customDataProperties]; + }, [customDataProperties]); + const steps = [ { title: "Step 1", @@ -579,6 +684,12 @@ const Page = () => { title: "Step 2", description: "Select Properties", component: PropertySelectionStep, + componentProps: { + users: users, + allProperties: allProperties, + customDataMappings: customDataMappings, + customDataProperties: customDataProperties, + }, }, { title: "Step 3", @@ -586,6 +697,7 @@ const Page = () => { component: ConfirmationStep, componentProps: { users: users, + allProperties: allProperties, }, }, ]; From ea929febb4431da7941976ca32a2addf8572c8ac Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 3 Oct 2025 00:08:22 -0400 Subject: [PATCH 068/112] Update patch-wizard.jsx --- .../administration/users/patch-wizard.jsx | 39 ++++++++++--------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/src/pages/identity/administration/users/patch-wizard.jsx b/src/pages/identity/administration/users/patch-wizard.jsx index 4fd9ce9be729..1fece737f175 100644 --- a/src/pages/identity/administration/users/patch-wizard.jsx +++ b/src/pages/identity/administration/users/patch-wizard.jsx @@ -462,8 +462,8 @@ const ConfirmationStep = (props) => { selectedProperties.forEach((propName) => { if (propertyValues[propName] !== undefined && propertyValues[propName] !== "") { // Handle dot notation properties (e.g., "extension_abc123.customField") - if (propName.includes('.')) { - const parts = propName.split('.'); + if (propName.includes(".")) { + const parts = propName.split("."); let current = userData; // Navigate to the nested object, creating it if it doesn't exist @@ -633,13 +633,18 @@ const Page = () => { // Get unique tenant domains from users const tenantDomains = useMemo(() => { - return [...new Set(users?.map(user => user.Tenant || user.tenantFilter).filter(Boolean))] || []; + return ( + [...new Set(users?.map((user) => user.Tenant || user.tenantFilter).filter(Boolean))] || [] + ); }, [users]); // Fetch custom data mappings for all tenants const customDataMappings = ApiGetCall({ - url: tenantDomains.length > 0 ? `/api/ListCustomDataMappings?sourceType=Manual Entry&directoryObject=User&tenantFilter=${tenantDomains[0]}` : null, - queryKey: `ManualEntryMappings-${tenantDomains.join(',')}`, + url: + tenantDomains.length > 0 + ? `/api/ListCustomDataMappings?sourceType=Manual Entry&directoryObject=User&tenantFilter=${tenantDomains[0]}` + : null, + queryKey: `ManualEntryMappings-${tenantDomains.join(",")}`, enabled: tenantDomains.length > 0, refetchOnMount: false, refetchOnReconnect: false, @@ -648,19 +653,17 @@ const Page = () => { // Process custom data mappings into property format const customDataProperties = useMemo(() => { if (customDataMappings.isSuccess && customDataMappings.data?.Results) { - return customDataMappings.data.Results - .filter(mapping => { - // Only include single-value properties, filter out multivalue ones - const dataType = mapping.customDataAttribute.addedFields.dataType?.toLowerCase(); - const isMultiValue = mapping.customDataAttribute.addedFields.isMultiValue; - return !isMultiValue && dataType !== 'collection'; - }) - .map(mapping => ({ - property: mapping.customDataAttribute.value, // Use the actual attribute name, not nested under customData - label: `${mapping.manualEntryFieldLabel} (Custom)`, - type: mapping.customDataAttribute.addedFields.dataType?.toLowerCase() || "string", - isCustomData: true, - })); + return customDataMappings.data.Results.filter((mapping) => { + // Only include single-value properties, filter out multivalue ones + const dataType = mapping.customDataAttribute.addedFields.dataType?.toLowerCase(); + const isMultiValue = mapping.customDataAttribute.addedFields.isMultiValue; + return !isMultiValue && dataType !== "collection"; + }).map((mapping) => ({ + property: mapping.customDataAttribute.value, // Use the actual attribute name, not nested under customData + label: `${mapping.manualEntryFieldLabel} (Custom)`, + type: mapping.customDataAttribute.addedFields.dataType?.toLowerCase() || "string", + isCustomData: true, + })); } return []; }, [customDataMappings.isSuccess, customDataMappings.data]); From d46cbec54b9aae11a5215791a680c28addfbd681 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 3 Oct 2025 10:52:59 -0400 Subject: [PATCH 069/112] custom variable autocomplete --- src/api/ApiCall.jsx | 91 +++++++- .../CippAuditLogSearchDrawer.jsx | 1 + .../CippComponents/CippCustomVariables.jsx | 37 +++- .../CippComponents/CippFormComponent.jsx | 84 ++++++- .../CippTextFieldWithVariables.jsx | 206 ++++++++++++++++++ .../CippVariableAutocomplete.jsx | 143 ++++++++++++ .../CippCustomDataMappingForm.jsx | 1 + .../CippApiClientManagement.jsx | 1 + src/components/CippSettings/CippRoles.jsx | 1 + src/components/CippWizard/CippTenantTable.jsx | 1 + src/hooks/useCustomVariables.js | 129 +++++++++++ src/pages/cipp/advanced/table-maintenance.js | 2 + .../custom-data/directory-extensions/add.js | 1 + .../cipp/custom-data/schema-extensions/add.js | 2 + .../custom-data/schema-extensions/index.js | 1 + src/pages/cipp/settings/licenses.js | 2 + .../standards/manage-drift/edit-tenant.js | 2 +- 17 files changed, 685 insertions(+), 20 deletions(-) create mode 100644 src/components/CippComponents/CippTextFieldWithVariables.jsx create mode 100644 src/components/CippComponents/CippVariableAutocomplete.jsx create mode 100644 src/hooks/useCustomVariables.js diff --git a/src/api/ApiCall.jsx b/src/api/ApiCall.jsx index e39aeb8718ee..45668df63086 100644 --- a/src/api/ApiCall.jsx +++ b/src/api/ApiCall.jsx @@ -76,7 +76,25 @@ export function ApiGetCall(props) { if (relatedQueryKeys) { const clearKeys = Array.isArray(relatedQueryKeys) ? relatedQueryKeys : [relatedQueryKeys]; setTimeout(() => { - clearKeys.forEach((key) => { + // Separate wildcard patterns from exact keys + const wildcardPatterns = clearKeys + .filter((key) => key.endsWith("*")) + .map((key) => key.slice(0, -1)); + const exactKeys = clearKeys.filter((key) => !key.endsWith("*")); + + // Use single predicate call for all wildcard patterns + if (wildcardPatterns.length > 0) { + queryClient.invalidateQueries({ + predicate: (query) => { + if (!query.queryKey || !query.queryKey[0]) return false; + const queryKeyStr = String(query.queryKey[0]); + return wildcardPatterns.some((pattern) => queryKeyStr.startsWith(pattern)); + }, + }); + } + + // Handle exact keys + exactKeys.forEach((key) => { queryClient.invalidateQueries({ queryKey: [key] }); }); }, 1000); @@ -96,7 +114,25 @@ export function ApiGetCall(props) { if (relatedQueryKeys) { const clearKeys = Array.isArray(relatedQueryKeys) ? relatedQueryKeys : [relatedQueryKeys]; setTimeout(() => { - clearKeys.forEach((key) => { + // Separate wildcard patterns from exact keys + const wildcardPatterns = clearKeys + .filter((key) => key.endsWith("*")) + .map((key) => key.slice(0, -1)); + const exactKeys = clearKeys.filter((key) => !key.endsWith("*")); + + // Use single predicate call for all wildcard patterns + if (wildcardPatterns.length > 0) { + queryClient.invalidateQueries({ + predicate: (query) => { + if (!query.queryKey || !query.queryKey[0]) return false; + const queryKeyStr = String(query.queryKey[0]); + return wildcardPatterns.some((pattern) => queryKeyStr.startsWith(pattern)); + }, + }); + } + + // Handle exact keys + exactKeys.forEach((key) => { queryClient.invalidateQueries({ queryKey: [key] }); }); }, 1000); @@ -117,8 +153,11 @@ export function ApiGetCall(props) { export function ApiPostCall({ relatedQueryKeys, onResult }) { const queryClient = useQueryClient(); + console.log("ApiPostCall created with relatedQueryKeys:", relatedQueryKeys); + const mutation = useMutation({ mutationFn: async (props) => { + console.log("ApiPostCall mutationFn called with props:", props); const { url, data, bulkRequest } = props; if (bulkRequest && Array.isArray(data)) { const results = []; @@ -140,17 +179,63 @@ export function ApiPostCall({ relatedQueryKeys, onResult }) { } }, onSuccess: () => { + console.log("ApiPostCall onSuccess triggered with relatedQueryKeys:", relatedQueryKeys); + if (relatedQueryKeys) { const clearKeys = Array.isArray(relatedQueryKeys) ? relatedQueryKeys : [relatedQueryKeys]; setTimeout(() => { if (relatedQueryKeys === "*") { + console.log("Invalidating all queries"); queryClient.invalidateQueries(); } else { - clearKeys.forEach((key) => { + // Separate wildcard patterns from exact keys + const wildcardPatterns = clearKeys + .filter((key) => key.endsWith("*")) + .map((key) => key.slice(0, -1)); + const exactKeys = clearKeys.filter((key) => !key.endsWith("*")); + + console.log("ApiPostCall Invalidation Debug:", { + clearKeys, + wildcardPatterns, + exactKeys, + }); + + // Use single predicate call for all wildcard patterns + if (wildcardPatterns.length > 0) { + console.log("Running predicate invalidation for patterns:", wildcardPatterns); + queryClient.invalidateQueries({ + predicate: (query) => { + if (!query.queryKey || !query.queryKey[0]) return false; + const queryKeyStr = String(query.queryKey[0]); + const matches = wildcardPatterns.some((pattern) => + queryKeyStr.startsWith(pattern) + ); + + // Debug logging for each query check + if (matches) { + console.log("Invalidating query:", { + queryKey: query.queryKey, + queryKeyStr, + matchedPattern: wildcardPatterns.find((pattern) => + queryKeyStr.startsWith(pattern) + ), + }); + } + + return matches; + }, + }); + } + + // Handle exact keys + exactKeys.forEach((key) => { + console.log("Invalidating exact key:", key); queryClient.invalidateQueries({ queryKey: [key] }); }); } }, 1000); + } else { + console.log("No relatedQueryKeys provided to ApiPostCall"); } }, }); diff --git a/src/components/CippComponents/CippAuditLogSearchDrawer.jsx b/src/components/CippComponents/CippAuditLogSearchDrawer.jsx index 2665715034ca..93628842b386 100644 --- a/src/components/CippComponents/CippAuditLogSearchDrawer.jsx +++ b/src/components/CippComponents/CippAuditLogSearchDrawer.jsx @@ -172,6 +172,7 @@ export const CippAuditLogSearchDrawer = ({ label: "Search Name", required: true, validators: { required: "Search name is required" }, + disableVariables: true, }, { type: "autoComplete", diff --git a/src/components/CippComponents/CippCustomVariables.jsx b/src/components/CippComponents/CippCustomVariables.jsx index 53ec4f601013..c3e79c60acd6 100644 --- a/src/components/CippComponents/CippCustomVariables.jsx +++ b/src/components/CippComponents/CippCustomVariables.jsx @@ -9,9 +9,14 @@ import { ApiPostCall } from "/src/api/ApiCall"; const CippCustomVariables = ({ id }) => { const [openAddDialog, setOpenAddDialog] = useState(false); + // Simple cache invalidation using React Query wildcard support + const allRelatedKeys = ["CustomVariables*"]; + + console.log("CippCustomVariables component - allRelatedKeys:", allRelatedKeys, "id:", id); + const updateCustomVariablesApi = ApiPostCall({ urlFromData: true, - relatedQueryKeys: [`CustomVariables_${id}`], + relatedQueryKeys: allRelatedKeys, }); const reservedVariables = [ @@ -64,15 +69,25 @@ const CippCustomVariables = ({ id }) => { label: "Variable Name", placeholder: "Enter the key for the custom variable.", required: true, + disableVariables: true, validators: { validate: validateVariableName }, }, { type: "textField", name: "Value", label: "Value", + disableVariables: true, placeholder: "Enter the value for the custom variable.", required: true, }, + { + type: "textField", + name: "Description", + label: "Description", + placeholder: "Enter a description for the custom variable.", + required: false, + disableVariables: true, + }, ], type: "POST", url: "/api/ExecCippReplacemap", @@ -80,7 +95,7 @@ const CippCustomVariables = ({ id }) => { Action: "!AddEdit", tenantId: id, }, - relatedQueryKeys: [`CustomVariables_${id}`], + relatedQueryKeys: allRelatedKeys, }, { label: "Delete", @@ -93,7 +108,7 @@ const CippCustomVariables = ({ id }) => { RowKey: "RowKey", tenantId: id, }, - relatedQueryKeys: [`CustomVariables_${id}`], + relatedQueryKeys: allRelatedKeys, multiPost: false, }, ]; @@ -110,14 +125,14 @@ const CippCustomVariables = ({ id }) => { : "Custom variables are key-value pairs that can be used to store additional information about a tenant. These are applied to templates in standards using the format %variablename%."} { label: "Variable Name", placeholder: "Enter the name for the custom variable without %.", required: true, + disableVariables: true, validators: { validate: validateVariableName }, }, { type: "textField", name: "Value", label: "Value", + disableVariables: true, placeholder: "Enter the value for the custom variable.", required: true, }, + { + type: "textField", + name: "Description", + label: "Description", + placeholder: "Enter a description for the custom variable.", + required: false, + disableVariables: true, + }, ]} api={{ type: "POST", url: "/api/ExecCippReplacemap", data: { Action: "AddEdit", tenantId: id }, - relatedQueryKeys: [`CustomVariables_${id}`], + relatedQueryKeys: allRelatedKeys, }} /> diff --git a/src/components/CippComponents/CippFormComponent.jsx b/src/components/CippComponents/CippFormComponent.jsx index 56d8f4b81225..374ac0280d63 100644 --- a/src/components/CippComponents/CippFormComponent.jsx +++ b/src/components/CippComponents/CippFormComponent.jsx @@ -13,6 +13,7 @@ import { Input, } from "@mui/material"; import { CippAutoComplete } from "./CippAutocomplete"; +import { CippTextFieldWithVariables } from "./CippTextFieldWithVariables"; import { Controller, useFormState } from "react-hook-form"; import { DateTimePicker } from "@mui/x-date-pickers"; // Make sure to install @mui/x-date-pickers import CSVReader from "../CSVReader"; @@ -52,6 +53,9 @@ export const CippFormComponent = (props) => { labelLocation = "behind", // Default location for switches defaultValue, helperText, + disableVariables = false, // Default to false - variables enabled by default + includeSystemVariables = true, // Include system variables by default + tenantFilter = null, // Tenant filter for variable context ...other } = props; const { errors } = useFormState({ control: formControl.control }); @@ -121,16 +125,76 @@ export const CippFormComponent = (props) => { return ( <>
    - ( + + )} + /> + ) : ( + + )} +
    + + {get(errors, convertedName, {})?.message} + + {helperText && ( + + {helperText} + + )} + + ); + case "textFieldWithVariables": + return ( + <> +
    + ( + + )} />
    diff --git a/src/components/CippComponents/CippTextFieldWithVariables.jsx b/src/components/CippComponents/CippTextFieldWithVariables.jsx new file mode 100644 index 000000000000..a758cc638051 --- /dev/null +++ b/src/components/CippComponents/CippTextFieldWithVariables.jsx @@ -0,0 +1,206 @@ +import React, { useState, useRef, useEffect } from "react"; +import { TextField } from "@mui/material"; +import { CippVariableAutocomplete } from "./CippVariableAutocomplete"; + +/** + * Enhanced TextField that supports custom variable autocomplete + * Triggers when user types % character + */ +export const CippTextFieldWithVariables = ({ + value = "", + onChange, + tenantFilter = null, + includeSystemVariables = true, + ...textFieldProps +}) => { + const [showAutocomplete, setShowAutocomplete] = useState(false); + const [autocompleteAnchor, setAutocompleteAnchor] = useState(null); + const [searchQuery, setSearchQuery] = useState(""); + const [cursorPosition, setCursorPosition] = useState(0); + const textFieldRef = useRef(null); + + // Safely close autocomplete + const closeAutocomplete = () => { + setShowAutocomplete(false); + setSearchQuery(""); + setAutocompleteAnchor(null); + }; + + // Track cursor position + const handleSelectionChange = () => { + if (textFieldRef.current) { + setCursorPosition(textFieldRef.current.selectionStart || 0); + } + }; + + // Get cursor position for floating autocomplete + const getCursorPosition = () => { + if (!textFieldRef.current) return { top: 0, left: 0 }; + + const rect = textFieldRef.current.getBoundingClientRect(); + return { + top: rect.bottom + window.scrollY, + left: rect.left + window.scrollX + cursorPosition * 8, // Approximate character width + }; + }; + + // Handle input changes and detect % trigger + const handleInputChange = (event) => { + const newValue = event.target.value; + const cursorPos = event.target.selectionStart; + + // Update cursor position state immediately + setCursorPosition(cursorPos); + + // Call parent onChange + if (onChange) { + onChange(event); + } + + // Check if % was just typed + if (newValue[cursorPos - 1] === "%") { + // Position autocomplete near cursor + setAutocompleteAnchor(textFieldRef.current); + setSearchQuery(""); + setShowAutocomplete(true); + } else if (showAutocomplete) { + // Update search query if autocomplete is open + const lastPercentIndex = newValue.lastIndexOf("%", cursorPos - 1); + if (lastPercentIndex !== -1) { + const query = newValue.substring(lastPercentIndex + 1, cursorPos); + setSearchQuery(query); + + // Close autocomplete if user typed space or special characters (except %) + if (query.includes(" ") || /[^a-zA-Z0-9_]/.test(query)) { + closeAutocomplete(); + } + } else { + closeAutocomplete(); + } + } + }; + + // Handle variable selection + const handleVariableSelect = (variableString) => { + if (!onChange) { + return; + } + + // Use the value prop instead of DOM value since we're in a controlled component + const currentValue = value || ""; + + // Get fresh cursor position from the DOM + let cursorPos = cursorPosition; + if (textFieldRef.current) { + const inputElement = textFieldRef.current.querySelector("input") || textFieldRef.current; + if (inputElement && typeof inputElement.selectionStart === "number") { + cursorPos = inputElement.selectionStart; + } + } + + // Find the % that triggered the autocomplete + const lastPercentIndex = currentValue.lastIndexOf("%", cursorPos - 1); + + if (lastPercentIndex !== -1) { + // Replace from % to cursor position with the selected variable + const beforePercent = currentValue.substring(0, lastPercentIndex); + const afterCursor = currentValue.substring(cursorPos); + const newValue = beforePercent + variableString + afterCursor; + + // Create synthetic event for onChange + const syntheticEvent = { + target: { + name: textFieldRef.current?.name || "", + value: newValue, + }, + }; + + onChange(syntheticEvent); + + // Set cursor position after the inserted variable + setTimeout(() => { + if (textFieldRef.current) { + const newCursorPos = lastPercentIndex + variableString.length; + + // Access the actual input element for Material-UI TextField + const inputElement = textFieldRef.current.querySelector("input") || textFieldRef.current; + if (inputElement && inputElement.setSelectionRange) { + inputElement.setSelectionRange(newCursorPos, newCursorPos); + inputElement.focus(); + } + setCursorPosition(newCursorPos); + } + }, 0); + } + + closeAutocomplete(); + }; + + // Handle key events + const handleKeyDown = (event) => { + if (showAutocomplete) { + // Let the autocomplete handle arrow keys and enter + if (["ArrowDown", "ArrowUp", "Enter", "Tab"].includes(event.key)) { + return; // Let autocomplete handle these + } + + // Close autocomplete on Escape + if (event.key === "Escape") { + closeAutocomplete(); + event.preventDefault(); + } + } + + // Call original onKeyDown if provided + if (textFieldProps.onKeyDown) { + textFieldProps.onKeyDown(event); + } + }; + + // Close autocomplete when clicking outside + useEffect(() => { + const handleClickOutside = (event) => { + if ( + showAutocomplete && + textFieldRef.current && + !textFieldRef.current.contains(event.target) + ) { + // Check if click is on autocomplete dropdown + const autocompleteElement = document.querySelector("[data-cipp-autocomplete]"); + if (autocompleteElement && autocompleteElement.contains(event.target)) { + return; // Don't close if clicking inside autocomplete + } + + closeAutocomplete(); + } + }; + + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, [showAutocomplete]); + + return ( + <> + + + + + ); +}; diff --git a/src/components/CippComponents/CippVariableAutocomplete.jsx b/src/components/CippComponents/CippVariableAutocomplete.jsx new file mode 100644 index 000000000000..a598857ca589 --- /dev/null +++ b/src/components/CippComponents/CippVariableAutocomplete.jsx @@ -0,0 +1,143 @@ +import React, { useState, useEffect, useRef } from "react"; +import { Paper, Typography, Box, Chip, Popper, ListItem, useTheme } from "@mui/material"; +import { useCustomVariables } from "/src/hooks/useCustomVariables"; + +/** + * Autocomplete component specifically for custom variables + * Shows when user types % in a text field + */ +export const CippVariableAutocomplete = ({ + open, + anchorEl, + onClose, + onSelect, + searchQuery = "", + tenantFilter = null, + includeSystemVariables = true, + position = { top: 0, left: 0 }, // Cursor position for floating box +}) => { + const theme = useTheme(); + const { variables, isLoading, groupedVariables } = useCustomVariables( + tenantFilter, + includeSystemVariables + ); + const [filteredVariables, setFilteredVariables] = useState([]); + + // Filter variables based on search query + useEffect(() => { + if (!searchQuery) { + setFilteredVariables(variables); + return; + } + + const lowerQuery = searchQuery.toLowerCase(); + const filtered = variables.filter( + (variable) => + variable.name.toLowerCase().includes(lowerQuery) || + variable.description.toLowerCase().includes(lowerQuery) + ); + setFilteredVariables(filtered); + }, [searchQuery, variables]); + + const handleSelect = (event, value) => { + if (value && onSelect) { + onSelect(value.variable); // Pass the full variable string like %tenantname% + } + onClose(); + }; + + if (!open) { + console.log("Not rendering autocomplete - open is false"); + return null; + } + + if (filteredVariables.length === 0) { + return null; + } + + return ( + + { + e.stopPropagation(); + }} + > + {filteredVariables.map((variable, index) => ( + { + e.stopPropagation(); + e.preventDefault(); + handleSelect(e, variable); + }} + sx={{ + display: "flex", + justifyContent: "space-between", + alignItems: "center", + py: 1, + px: 2, + borderBottom: `1px solid ${theme.palette.divider}`, + "&:hover": { + backgroundColor: theme.palette.action.hover, + }, + cursor: "pointer", + }} + > + + + {variable.variable} + + + {variable.description} + + + + + + + + ))} + + + ); +}; diff --git a/src/components/CippFormPages/CippCustomDataMappingForm.jsx b/src/components/CippFormPages/CippCustomDataMappingForm.jsx index 4fceb43b6295..acb0c7134a56 100644 --- a/src/components/CippFormPages/CippCustomDataMappingForm.jsx +++ b/src/components/CippFormPages/CippCustomDataMappingForm.jsx @@ -102,6 +102,7 @@ const CippCustomDataMappingForm = ({ formControl }) => { type: "textField", required: true, placeholder: "Enter field label (e.g., Employee ID, Department)", + disableVariables: true, }, { name: "directoryObjectType", diff --git a/src/components/CippIntegrations/CippApiClientManagement.jsx b/src/components/CippIntegrations/CippApiClientManagement.jsx index 65914ea19dd0..13cb698b8c56 100644 --- a/src/components/CippIntegrations/CippApiClientManagement.jsx +++ b/src/components/CippIntegrations/CippApiClientManagement.jsx @@ -306,6 +306,7 @@ const CippApiClientManagement = () => { name: "AppName", label: "App Name", placeholder: "Enter a name for this Application Registration.", + disableVariables: true, }, { type: "autoComplete", diff --git a/src/components/CippSettings/CippRoles.jsx b/src/components/CippSettings/CippRoles.jsx index 4de9acfeddb4..c155064b634a 100644 --- a/src/components/CippSettings/CippRoles.jsx +++ b/src/components/CippSettings/CippRoles.jsx @@ -41,6 +41,7 @@ const CippRoles = () => { required: true, helperText: "Enter a name for the new cloned role. This cannot be the same as an existing role.", + disableVariables: true, }, ], relatedQueryKeys: ["customRoleList"], diff --git a/src/components/CippWizard/CippTenantTable.jsx b/src/components/CippWizard/CippTenantTable.jsx index 10d7490babdc..81c1ae4cde73 100644 --- a/src/components/CippWizard/CippTenantTable.jsx +++ b/src/components/CippWizard/CippTenantTable.jsx @@ -149,6 +149,7 @@ export const CippTenantTable = ({ type: "textField", name: "tenantFilter", label: "Default Domain Name or Tenant ID", + disableVariables: true, }, ]} api={{ diff --git a/src/hooks/useCustomVariables.js b/src/hooks/useCustomVariables.js new file mode 100644 index 000000000000..37d0f1781487 --- /dev/null +++ b/src/hooks/useCustomVariables.js @@ -0,0 +1,129 @@ +import { useMemo } from "react"; +import { ApiGetCall } from "/src/api/ApiCall"; + +/** + * Hook to fetch and format custom variables for autocomplete + * @param {string} tenantFilter - Optional tenant filter for tenant-specific variables + * @param {boolean} includeSystemVariables - Whether to include system variables + * @returns {object} { variables, isLoading, error } + */ +export const useCustomVariables = (tenantFilter = null, includeSystemVariables = true) => { + // Simple, consistent query key using prefix pattern + // React Query can invalidate with wildcards like "CustomVariables*" + const queryKey = `CustomVariables-${tenantFilter || "global"}-${ + includeSystemVariables ? "withSystem" : "noSystem" + }`; + + // Simple related keys pattern - React Query supports predicate-based invalidation + const relatedQueryKeys = ["CustomVariables*"]; + + // Build API URL with optional tenant filter and system variables setting + let apiUrl = "/api/ListCustomVariables"; + const params = new URLSearchParams(); + + if (tenantFilter) { + params.append("tenantFilter", tenantFilter); + } + + if (!includeSystemVariables) { + params.append("includeSystem", "false"); + } + + if (params.toString()) { + apiUrl += `?${params.toString()}`; + } + + // Fetch variables from API + const apiCall = ApiGetCall({ + url: apiUrl, + queryKey, + relatedQueryKeys, + refetchOnMount: false, + refetchOnReconnect: false, + staleTime: 5 * 60 * 1000, // 5 minutes - variables don't change often + }); + + // Format variables for autocomplete component + const variables = useMemo(() => { + if (!apiCall.isSuccess || !apiCall.data?.Results) { + return []; + } + + return apiCall.data.Results.map((variable) => ({ + // Core properties + name: variable.Name, + variable: variable.Variable, + label: variable.Variable, // What shows in autocomplete + value: variable.Variable, // What gets inserted + + // Metadata for display and filtering + description: variable.Description, + type: variable.Type, // 'reserved' or 'custom' + category: variable.Category, // 'system', 'tenant', 'partner', 'cipp', 'global', 'tenant-custom' + + // Custom variable specific + ...(variable.Type === "custom" && { + customValue: variable.Value, + scope: variable.Scope, + }), + + // For grouping in autocomplete + group: + variable.Type === "reserved" + ? `Reserved (${variable.Category})` + : variable.Category === "global" + ? "Global Custom Variables" + : "Tenant Custom Variables", + })); + }, [apiCall.isSuccess, apiCall.data]); + + // Group variables by category for better UX + const groupedVariables = useMemo(() => { + const groups = {}; + variables.forEach((variable) => { + const groupName = variable.group; + if (!groups[groupName]) { + groups[groupName] = []; + } + groups[groupName].push(variable); + }); + return groups; + }, [variables]); + + // Filter functions for different use cases + const filterVariables = useMemo( + () => ({ + // Get only reserved variables + reserved: () => variables.filter((v) => v.type === "reserved"), + + // Get only custom variables + custom: () => variables.filter((v) => v.type === "custom"), + + // Get variables by category + byCategory: (category) => variables.filter((v) => v.category === category), + + // Search variables by name or description + search: (query) => { + const lowerQuery = query.toLowerCase(); + return variables.filter( + (v) => + v.name.toLowerCase().includes(lowerQuery) || + v.description.toLowerCase().includes(lowerQuery) + ); + }, + }), + [variables] + ); + + return { + variables, + groupedVariables, + filterVariables, + isLoading: apiCall.isLoading, + isSuccess: apiCall.isSuccess, + isError: apiCall.isError, + error: apiCall.error, + metadata: apiCall.data?.Metadata, + relatedQueryKeys, // Expose related query keys for other components + }; +}; diff --git a/src/pages/cipp/advanced/table-maintenance.js b/src/pages/cipp/advanced/table-maintenance.js index d7bc058697ee..4f4c53c30fb7 100644 --- a/src/pages/cipp/advanced/table-maintenance.js +++ b/src/pages/cipp/advanced/table-maintenance.js @@ -72,6 +72,7 @@ const CustomAddEditRowDialog = ({ formControl, open, onClose, onSubmit, defaultV name={`fields[${index}].name`} formControl={formControl} label="Name" + disableVariables={true} /> @@ -101,6 +102,7 @@ const CustomAddEditRowDialog = ({ formControl, open, onClose, onSubmit, defaultV return {}; } }} + disableVariables={true} /> diff --git a/src/pages/cipp/custom-data/directory-extensions/add.js b/src/pages/cipp/custom-data/directory-extensions/add.js index 7238792aa4dd..975c7be997de 100644 --- a/src/pages/cipp/custom-data/directory-extensions/add.js +++ b/src/pages/cipp/custom-data/directory-extensions/add.js @@ -62,6 +62,7 @@ const Page = () => { type: "textField", required: true, placeholder: "Enter a unique name for the directory extension", + disableVariables: true, }, { name: "dataType", diff --git a/src/pages/cipp/custom-data/schema-extensions/add.js b/src/pages/cipp/custom-data/schema-extensions/add.js index 605d50ed4f15..07be1c409067 100644 --- a/src/pages/cipp/custom-data/schema-extensions/add.js +++ b/src/pages/cipp/custom-data/schema-extensions/add.js @@ -101,6 +101,7 @@ const Page = () => { required: true, placeholder: "Enter a schema id (e.g. cippUser). The prefix is generated automatically after creation.", + disableVariables: true, }, { name: "description", @@ -108,6 +109,7 @@ const Page = () => { type: "textField", required: true, placeholder: "Enter a description for the schema extension", + disableVariables: true, }, { name: "status", diff --git a/src/pages/cipp/custom-data/schema-extensions/index.js b/src/pages/cipp/custom-data/schema-extensions/index.js index f7b5729187ba..daca34251209 100644 --- a/src/pages/cipp/custom-data/schema-extensions/index.js +++ b/src/pages/cipp/custom-data/schema-extensions/index.js @@ -40,6 +40,7 @@ const Page = () => { label: "Property Name", type: "textField", required: true, + disableVariables: true, }, { name: "type", diff --git a/src/pages/cipp/settings/licenses.js b/src/pages/cipp/settings/licenses.js index a33e9f1f612a..f59032fbcba5 100644 --- a/src/pages/cipp/settings/licenses.js +++ b/src/pages/cipp/settings/licenses.js @@ -75,11 +75,13 @@ const Page = () => { type: "textField", name: "GUID", label: "GUID", + disableVariables: true, }, { type: "textField", name: "SKUName", label: "SKU Name", + disableVariables: true, }, ]} api={{ diff --git a/src/pages/tenant/standards/manage-drift/edit-tenant.js b/src/pages/tenant/standards/manage-drift/edit-tenant.js index e54dec9a3e6f..9dab02f75766 100644 --- a/src/pages/tenant/standards/manage-drift/edit-tenant.js +++ b/src/pages/tenant/standards/manage-drift/edit-tenant.js @@ -45,7 +45,7 @@ const Page = () => { // API call for updating offboarding defaults const updateOffboardingDefaults = ApiPostCall({ urlFromData: true, - relatedQueryKeys: [`TenantProperties_${currentTenant}`], + relatedQueryKeys: [`TenantProperties_${currentTenant}`, "CustomVariables*"], }); const { isValid: isFormValid } = useFormState({ control: formControl.control }); From 4ab1d1a4749a17d9caa55fdbf95aa44dbca8284d Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 3 Oct 2025 10:59:07 -0400 Subject: [PATCH 070/112] fix querykey --- src/pages/cipp/custom-data/mappings/add.js | 176 ++++++++++----------- 1 file changed, 88 insertions(+), 88 deletions(-) diff --git a/src/pages/cipp/custom-data/mappings/add.js b/src/pages/cipp/custom-data/mappings/add.js index 805cbe12caa4..cd77fc9eff6e 100644 --- a/src/pages/cipp/custom-data/mappings/add.js +++ b/src/pages/cipp/custom-data/mappings/add.js @@ -1,88 +1,88 @@ -import { Layout as DashboardLayout } from "/src/layouts/index.js"; -import { useForm, useFormState } from "react-hook-form"; -import { ApiPostCall } from "/src/api/ApiCall"; -import { useRouter } from "next/router"; -import { Button, Stack, CardContent, CardActions } from "@mui/material"; - -import CippPageCard from "/src/components/CippCards/CippPageCard"; -import { CippApiResults } from "/src/components/CippComponents/CippApiResults"; -import CippCustomDataMappingForm from "/src/components/CippFormPages/CippCustomDataMappingForm"; - -const Page = () => { - const router = useRouter(); - const formControl = useForm({ - mode: "onChange", - }); - - const formState = useFormState({ control: formControl.control }); - - const addMappingApi = ApiPostCall({ - urlFromData: true, - relatedQueryKeys: ["MappingsListPage"], - }); - - const handleAddMapping = (data) => { - // Filter data based on source type to only include relevant fields - let filteredData; - - if (data.sourceType?.value === "manualEntry") { - // For manual entry, only include these fields - filteredData = { - sourceType: data.sourceType, - manualEntryFieldLabel: data.manualEntryFieldLabel, - directoryObjectType: data.directoryObjectType, - customDataAttribute: data.customDataAttribute, - tenantFilter: data.tenantFilter, - }; - } else if (data.sourceType?.value === "extensionSync") { - // For extension sync, include the original fields - filteredData = { - sourceType: data.sourceType, - extensionSyncDataset: data.extensionSyncDataset, - extensionSyncProperty: data.extensionSyncProperty, - directoryObjectType: data.directoryObjectType, - customDataAttribute: data.customDataAttribute, - tenantFilter: data.tenantFilter, - }; - } else { - // Fallback to all data if source type is not recognized - filteredData = data; - } - - addMappingApi.mutate({ - url: "/api/ExecCustomData", - data: { - Action: "AddEditMapping", - Mapping: filteredData, - }, - }); - }; - - return ( - - - - - - - - - - - - - ); -}; - -Page.getLayout = (page) => {page}; - -export default Page; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { useForm, useFormState } from "react-hook-form"; +import { ApiPostCall } from "/src/api/ApiCall"; +import { useRouter } from "next/router"; +import { Button, Stack, CardContent, CardActions } from "@mui/material"; + +import CippPageCard from "/src/components/CippCards/CippPageCard"; +import { CippApiResults } from "/src/components/CippComponents/CippApiResults"; +import CippCustomDataMappingForm from "/src/components/CippFormPages/CippCustomDataMappingForm"; + +const Page = () => { + const router = useRouter(); + const formControl = useForm({ + mode: "onChange", + }); + + const formState = useFormState({ control: formControl.control }); + + const addMappingApi = ApiPostCall({ + urlFromData: true, + relatedQueryKeys: ["MappingsListPage", "ManualEntryMappings*"], + }); + + const handleAddMapping = (data) => { + // Filter data based on source type to only include relevant fields + let filteredData; + + if (data.sourceType?.value === "manualEntry") { + // For manual entry, only include these fields + filteredData = { + sourceType: data.sourceType, + manualEntryFieldLabel: data.manualEntryFieldLabel, + directoryObjectType: data.directoryObjectType, + customDataAttribute: data.customDataAttribute, + tenantFilter: data.tenantFilter, + }; + } else if (data.sourceType?.value === "extensionSync") { + // For extension sync, include the original fields + filteredData = { + sourceType: data.sourceType, + extensionSyncDataset: data.extensionSyncDataset, + extensionSyncProperty: data.extensionSyncProperty, + directoryObjectType: data.directoryObjectType, + customDataAttribute: data.customDataAttribute, + tenantFilter: data.tenantFilter, + }; + } else { + // Fallback to all data if source type is not recognized + filteredData = data; + } + + addMappingApi.mutate({ + url: "/api/ExecCustomData", + data: { + Action: "AddEditMapping", + Mapping: filteredData, + }, + }); + }; + + return ( + + + + + + + + + + + + + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; From 4f7ecec0a08802ab9130e59e2eeb225cb98c5650 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 3 Oct 2025 11:05:30 -0400 Subject: [PATCH 071/112] null safety on empty $select options --- src/api/ApiCall.jsx | 4 +--- src/components/CippTable/CIPPTableToptoolbar.js | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/api/ApiCall.jsx b/src/api/ApiCall.jsx index 45668df63086..5ab064cb856c 100644 --- a/src/api/ApiCall.jsx +++ b/src/api/ApiCall.jsx @@ -153,11 +153,9 @@ export function ApiGetCall(props) { export function ApiPostCall({ relatedQueryKeys, onResult }) { const queryClient = useQueryClient(); - console.log("ApiPostCall created with relatedQueryKeys:", relatedQueryKeys); - + const mutation = useMutation({ mutationFn: async (props) => { - console.log("ApiPostCall mutationFn called with props:", props); const { url, data, bulkRequest } = props; if (bulkRequest && Array.isArray(data)) { const results = []; diff --git a/src/components/CippTable/CIPPTableToptoolbar.js b/src/components/CippTable/CIPPTableToptoolbar.js index 02ba791a705a..b783da5a3a9c 100644 --- a/src/components/CippTable/CIPPTableToptoolbar.js +++ b/src/components/CippTable/CIPPTableToptoolbar.js @@ -500,8 +500,8 @@ export const CIPPTableToptoolbar = ({ let selectedColumns = []; if (Array.isArray(filter?.$select)) { selectedColumns = filter?.$select; - } else { - selectedColumns = filter?.$select.split(","); + } else if (typeof filter?.$select === 'string') { + selectedColumns = filter.$select.split(","); } if (selectedColumns.length > 0) { setConfiguredSimpleColumns(selectedColumns); From d95bea12bae1828c4578a172632e985e4e8fd5ea Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 3 Oct 2025 11:14:12 -0400 Subject: [PATCH 072/112] null safety around persisted filters --- src/components/CippTable/CIPPTableToptoolbar.js | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/components/CippTable/CIPPTableToptoolbar.js b/src/components/CippTable/CIPPTableToptoolbar.js index b783da5a3a9c..93fe1ba86063 100644 --- a/src/components/CippTable/CIPPTableToptoolbar.js +++ b/src/components/CippTable/CIPPTableToptoolbar.js @@ -278,10 +278,15 @@ export const CIPPTableToptoolbar = ({ setActiveFilterName(last.name); if (last.value?.$select) { - const selectColumns = last.value.$select - .split(",") - .map((col) => col.trim()) - .filter((col) => usedColumns.includes(col)); + let selectColumns = []; + if (Array.isArray(last.value.$select)) { + selectColumns = last.value.$select; + } else if (typeof last.value.$select === 'string') { + selectColumns = last.value.$select + .split(",") + .map((col) => col.trim()) + .filter((col) => usedColumns.includes(col)); + } if (selectColumns.length > 0) { setConfiguredSimpleColumns(selectColumns); } @@ -1195,8 +1200,8 @@ export const CIPPTableToptoolbar = ({ let selectedColumns = []; if (Array.isArray(filter?.$select)) { selectedColumns = filter?.$select; - } else { - selectedColumns = filter?.$select.split(","); + } else if (typeof filter?.$select === 'string') { + selectedColumns = filter.$select.split(","); } if (selectedColumns.length > 0) { setConfiguredSimpleColumns(selectedColumns); From 2ac5d6111b1be502e004154ab1f42ada506cf11b Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 3 Oct 2025 11:29:12 -0400 Subject: [PATCH 073/112] debugging --- src/api/ApiCall.jsx | 2 +- .../CippTextFieldWithVariables.jsx | 12 +++++++++ .../CippVariableAutocomplete.jsx | 26 +++++++++++++++++++ .../CippTable/CIPPTableToptoolbar.js | 6 ++--- src/hooks/useCustomVariables.js | 13 ++++++++++ 5 files changed, 55 insertions(+), 4 deletions(-) diff --git a/src/api/ApiCall.jsx b/src/api/ApiCall.jsx index 5ab064cb856c..8b07fdcf99da 100644 --- a/src/api/ApiCall.jsx +++ b/src/api/ApiCall.jsx @@ -153,7 +153,7 @@ export function ApiGetCall(props) { export function ApiPostCall({ relatedQueryKeys, onResult }) { const queryClient = useQueryClient(); - + const mutation = useMutation({ mutationFn: async (props) => { const { url, data, bulkRequest } = props; diff --git a/src/components/CippComponents/CippTextFieldWithVariables.jsx b/src/components/CippComponents/CippTextFieldWithVariables.jsx index a758cc638051..e411fb12d8f4 100644 --- a/src/components/CippComponents/CippTextFieldWithVariables.jsx +++ b/src/components/CippComponents/CippTextFieldWithVariables.jsx @@ -19,8 +19,14 @@ export const CippTextFieldWithVariables = ({ const [cursorPosition, setCursorPosition] = useState(0); const textFieldRef = useRef(null); + // Debug showAutocomplete state changes + useEffect(() => { + console.log("showAutocomplete changed to:", showAutocomplete); + }, [showAutocomplete]); + // Safely close autocomplete const closeAutocomplete = () => { + console.log("Closing autocomplete"); setShowAutocomplete(false); setSearchQuery(""); setAutocompleteAnchor(null); @@ -49,6 +55,8 @@ export const CippTextFieldWithVariables = ({ const newValue = event.target.value; const cursorPos = event.target.selectionStart; + console.log("Input changed:", { newValue, cursorPos, lastChar: newValue[cursorPos - 1] }); + // Update cursor position state immediately setCursorPosition(cursorPos); @@ -59,6 +67,7 @@ export const CippTextFieldWithVariables = ({ // Check if % was just typed if (newValue[cursorPos - 1] === "%") { + console.log("% detected, opening autocomplete"); // Position autocomplete near cursor setAutocompleteAnchor(textFieldRef.current); setSearchQuery(""); @@ -68,13 +77,16 @@ export const CippTextFieldWithVariables = ({ const lastPercentIndex = newValue.lastIndexOf("%", cursorPos - 1); if (lastPercentIndex !== -1) { const query = newValue.substring(lastPercentIndex + 1, cursorPos); + console.log("Updating search query:", query); setSearchQuery(query); // Close autocomplete if user typed space or special characters (except %) if (query.includes(" ") || /[^a-zA-Z0-9_]/.test(query)) { + console.log("Closing autocomplete due to invalid characters"); closeAutocomplete(); } } else { + console.log("No % found, closing autocomplete"); closeAutocomplete(); } } diff --git a/src/components/CippComponents/CippVariableAutocomplete.jsx b/src/components/CippComponents/CippVariableAutocomplete.jsx index a598857ca589..98d0b3691254 100644 --- a/src/components/CippComponents/CippVariableAutocomplete.jsx +++ b/src/components/CippComponents/CippVariableAutocomplete.jsx @@ -23,6 +23,16 @@ export const CippVariableAutocomplete = ({ ); const [filteredVariables, setFilteredVariables] = useState([]); + // Debug logging for production issues + useEffect(() => { + console.log("CippVariableAutocomplete - Variables loaded:", { + variables: variables?.length || 0, + isLoading, + tenantFilter, + includeSystemVariables, + }); + }, [variables, isLoading, tenantFilter, includeSystemVariables]); + // Filter variables based on search query useEffect(() => { if (!searchQuery) { @@ -51,10 +61,26 @@ export const CippVariableAutocomplete = ({ return null; } + if (isLoading) { + console.log("Not rendering autocomplete - still loading variables"); + return null; + } + + if (!variables || variables.length === 0) { + console.log("Not rendering autocomplete - no variables available", { variables }); + return null; + } + if (filteredVariables.length === 0) { + console.log("Not rendering autocomplete - no filtered variables", { + searchQuery, + totalVariables: variables?.length || 0, + }); return null; } + console.log("Rendering autocomplete with", filteredVariables.length, "variables"); + return ( col.trim()) @@ -505,7 +505,7 @@ export const CIPPTableToptoolbar = ({ let selectedColumns = []; if (Array.isArray(filter?.$select)) { selectedColumns = filter?.$select; - } else if (typeof filter?.$select === 'string') { + } else if (typeof filter?.$select === "string") { selectedColumns = filter.$select.split(","); } if (selectedColumns.length > 0) { @@ -1200,7 +1200,7 @@ export const CIPPTableToptoolbar = ({ let selectedColumns = []; if (Array.isArray(filter?.$select)) { selectedColumns = filter?.$select; - } else if (typeof filter?.$select === 'string') { + } else if (typeof filter?.$select === "string") { selectedColumns = filter.$select.split(","); } if (selectedColumns.length > 0) { diff --git a/src/hooks/useCustomVariables.js b/src/hooks/useCustomVariables.js index 37d0f1781487..e12fadc0dbf1 100644 --- a/src/hooks/useCustomVariables.js +++ b/src/hooks/useCustomVariables.js @@ -43,6 +43,19 @@ export const useCustomVariables = (tenantFilter = null, includeSystemVariables = staleTime: 5 * 60 * 1000, // 5 minutes - variables don't change often }); + // Debug logging for production issues + console.log("useCustomVariables API call status:", { + url: apiUrl, + queryKey, + isLoading: apiCall.isLoading, + isSuccess: apiCall.isSuccess, + isError: apiCall.isError, + error: apiCall.error, + hasData: !!apiCall.data, + hasResults: !!apiCall.data?.Results, + resultsCount: apiCall.data?.Results?.length || 0, + }); + // Format variables for autocomplete component const variables = useMemo(() => { if (!apiCall.isSuccess || !apiCall.data?.Results) { From 64581be111d65802ee319bf0e2e9676a3d2166e9 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 3 Oct 2025 11:39:14 -0400 Subject: [PATCH 074/112] fix tenant filter on variable autocomplete --- src/components/CippComponents/CippFormComponent.jsx | 2 -- .../CippComponents/CippTextFieldWithVariables.jsx | 7 +++++-- src/components/CippComponents/CippVariableAutocomplete.jsx | 2 +- src/hooks/useCustomVariables.js | 4 ++++ 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/components/CippComponents/CippFormComponent.jsx b/src/components/CippComponents/CippFormComponent.jsx index 374ac0280d63..2c018525a60b 100644 --- a/src/components/CippComponents/CippFormComponent.jsx +++ b/src/components/CippComponents/CippFormComponent.jsx @@ -55,7 +55,6 @@ export const CippFormComponent = (props) => { helperText, disableVariables = false, // Default to false - variables enabled by default includeSystemVariables = true, // Include system variables by default - tenantFilter = null, // Tenant filter for variable context ...other } = props; const { errors } = useFormState({ control: formControl.control }); @@ -142,7 +141,6 @@ export const CippFormComponent = (props) => { label={label} value={field.value || ""} onChange={field.onChange} - tenantFilter={tenantFilter} includeSystemVariables={includeSystemVariables} /> )} diff --git a/src/components/CippComponents/CippTextFieldWithVariables.jsx b/src/components/CippComponents/CippTextFieldWithVariables.jsx index e411fb12d8f4..4883284cf0fd 100644 --- a/src/components/CippComponents/CippTextFieldWithVariables.jsx +++ b/src/components/CippComponents/CippTextFieldWithVariables.jsx @@ -1,6 +1,7 @@ import React, { useState, useRef, useEffect } from "react"; import { TextField } from "@mui/material"; import { CippVariableAutocomplete } from "./CippVariableAutocomplete"; +import { useSettings } from "/src/hooks/use-settings.js"; /** * Enhanced TextField that supports custom variable autocomplete @@ -9,8 +10,7 @@ import { CippVariableAutocomplete } from "./CippVariableAutocomplete"; export const CippTextFieldWithVariables = ({ value = "", onChange, - tenantFilter = null, - includeSystemVariables = true, + includeSystemVariables = false, ...textFieldProps }) => { const [showAutocomplete, setShowAutocomplete] = useState(false); @@ -19,6 +19,9 @@ export const CippTextFieldWithVariables = ({ const [cursorPosition, setCursorPosition] = useState(0); const textFieldRef = useRef(null); + const tenant = useSettings().currentTenant; + const tenantFilter = tenant || null; + // Debug showAutocomplete state changes useEffect(() => { console.log("showAutocomplete changed to:", showAutocomplete); diff --git a/src/components/CippComponents/CippVariableAutocomplete.jsx b/src/components/CippComponents/CippVariableAutocomplete.jsx index 98d0b3691254..5a1db60c88f3 100644 --- a/src/components/CippComponents/CippVariableAutocomplete.jsx +++ b/src/components/CippComponents/CippVariableAutocomplete.jsx @@ -13,7 +13,7 @@ export const CippVariableAutocomplete = ({ onSelect, searchQuery = "", tenantFilter = null, - includeSystemVariables = true, + includeSystemVariables = false, position = { top: 0, left: 0 }, // Cursor position for floating box }) => { const theme = useTheme(); diff --git a/src/hooks/useCustomVariables.js b/src/hooks/useCustomVariables.js index e12fadc0dbf1..7b0069c79853 100644 --- a/src/hooks/useCustomVariables.js +++ b/src/hooks/useCustomVariables.js @@ -10,6 +10,10 @@ import { ApiGetCall } from "/src/api/ApiCall"; export const useCustomVariables = (tenantFilter = null, includeSystemVariables = true) => { // Simple, consistent query key using prefix pattern // React Query can invalidate with wildcards like "CustomVariables*" + + if (tenantFilter === "AllTenants") { + tenantFilter = null; // Normalize to null for global context + } const queryKey = `CustomVariables-${tenantFilter || "global"}-${ includeSystemVariables ? "withSystem" : "noSystem" }`; From 06e26eadfffed3b74148d10247f4d137ec7a6cef Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 3 Oct 2025 11:45:44 -0400 Subject: [PATCH 075/112] fix defaults cleanup debug logs --- .../CippComponents/CippFormComponent.jsx | 4 ++-- .../CippTextFieldWithVariables.jsx | 12 ------------ .../CippVariableAutocomplete.jsx | 19 ------------------- src/hooks/useCustomVariables.js | 15 +-------------- 4 files changed, 3 insertions(+), 47 deletions(-) diff --git a/src/components/CippComponents/CippFormComponent.jsx b/src/components/CippComponents/CippFormComponent.jsx index 2c018525a60b..0b51f2040c9b 100644 --- a/src/components/CippComponents/CippFormComponent.jsx +++ b/src/components/CippComponents/CippFormComponent.jsx @@ -53,8 +53,8 @@ export const CippFormComponent = (props) => { labelLocation = "behind", // Default location for switches defaultValue, helperText, - disableVariables = false, // Default to false - variables enabled by default - includeSystemVariables = true, // Include system variables by default + disableVariables = false, + includeSystemVariables = false, ...other } = props; const { errors } = useFormState({ control: formControl.control }); diff --git a/src/components/CippComponents/CippTextFieldWithVariables.jsx b/src/components/CippComponents/CippTextFieldWithVariables.jsx index 4883284cf0fd..530981e216aa 100644 --- a/src/components/CippComponents/CippTextFieldWithVariables.jsx +++ b/src/components/CippComponents/CippTextFieldWithVariables.jsx @@ -22,14 +22,8 @@ export const CippTextFieldWithVariables = ({ const tenant = useSettings().currentTenant; const tenantFilter = tenant || null; - // Debug showAutocomplete state changes - useEffect(() => { - console.log("showAutocomplete changed to:", showAutocomplete); - }, [showAutocomplete]); - // Safely close autocomplete const closeAutocomplete = () => { - console.log("Closing autocomplete"); setShowAutocomplete(false); setSearchQuery(""); setAutocompleteAnchor(null); @@ -58,8 +52,6 @@ export const CippTextFieldWithVariables = ({ const newValue = event.target.value; const cursorPos = event.target.selectionStart; - console.log("Input changed:", { newValue, cursorPos, lastChar: newValue[cursorPos - 1] }); - // Update cursor position state immediately setCursorPosition(cursorPos); @@ -70,7 +62,6 @@ export const CippTextFieldWithVariables = ({ // Check if % was just typed if (newValue[cursorPos - 1] === "%") { - console.log("% detected, opening autocomplete"); // Position autocomplete near cursor setAutocompleteAnchor(textFieldRef.current); setSearchQuery(""); @@ -80,16 +71,13 @@ export const CippTextFieldWithVariables = ({ const lastPercentIndex = newValue.lastIndexOf("%", cursorPos - 1); if (lastPercentIndex !== -1) { const query = newValue.substring(lastPercentIndex + 1, cursorPos); - console.log("Updating search query:", query); setSearchQuery(query); // Close autocomplete if user typed space or special characters (except %) if (query.includes(" ") || /[^a-zA-Z0-9_]/.test(query)) { - console.log("Closing autocomplete due to invalid characters"); closeAutocomplete(); } } else { - console.log("No % found, closing autocomplete"); closeAutocomplete(); } } diff --git a/src/components/CippComponents/CippVariableAutocomplete.jsx b/src/components/CippComponents/CippVariableAutocomplete.jsx index 5a1db60c88f3..961f601e80d6 100644 --- a/src/components/CippComponents/CippVariableAutocomplete.jsx +++ b/src/components/CippComponents/CippVariableAutocomplete.jsx @@ -23,16 +23,6 @@ export const CippVariableAutocomplete = ({ ); const [filteredVariables, setFilteredVariables] = useState([]); - // Debug logging for production issues - useEffect(() => { - console.log("CippVariableAutocomplete - Variables loaded:", { - variables: variables?.length || 0, - isLoading, - tenantFilter, - includeSystemVariables, - }); - }, [variables, isLoading, tenantFilter, includeSystemVariables]); - // Filter variables based on search query useEffect(() => { if (!searchQuery) { @@ -57,30 +47,21 @@ export const CippVariableAutocomplete = ({ }; if (!open) { - console.log("Not rendering autocomplete - open is false"); return null; } if (isLoading) { - console.log("Not rendering autocomplete - still loading variables"); return null; } if (!variables || variables.length === 0) { - console.log("Not rendering autocomplete - no variables available", { variables }); return null; } if (filteredVariables.length === 0) { - console.log("Not rendering autocomplete - no filtered variables", { - searchQuery, - totalVariables: variables?.length || 0, - }); return null; } - console.log("Rendering autocomplete with", filteredVariables.length, "variables"); - return ( { +export const useCustomVariables = (tenantFilter = null, includeSystemVariables = false) => { // Simple, consistent query key using prefix pattern // React Query can invalidate with wildcards like "CustomVariables*" @@ -47,19 +47,6 @@ export const useCustomVariables = (tenantFilter = null, includeSystemVariables = staleTime: 5 * 60 * 1000, // 5 minutes - variables don't change often }); - // Debug logging for production issues - console.log("useCustomVariables API call status:", { - url: apiUrl, - queryKey, - isLoading: apiCall.isLoading, - isSuccess: apiCall.isSuccess, - isError: apiCall.isError, - error: apiCall.error, - hasData: !!apiCall.data, - hasResults: !!apiCall.data?.Results, - resultsCount: apiCall.data?.Results?.length || 0, - }); - // Format variables for autocomplete component const variables = useMemo(() => { if (!apiCall.isSuccess || !apiCall.data?.Results) { From a29c5978bf0ec9f0b6e78a56a2eab7aee66821ca Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 3 Oct 2025 11:51:20 -0400 Subject: [PATCH 076/112] add system fields to intune policy page --- src/components/CippComponents/CippTemplateFieldRenderer.jsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/components/CippComponents/CippTemplateFieldRenderer.jsx b/src/components/CippComponents/CippTemplateFieldRenderer.jsx index 9db3cfe458f8..7fae93ec413f 100644 --- a/src/components/CippComponents/CippTemplateFieldRenderer.jsx +++ b/src/components/CippComponents/CippTemplateFieldRenderer.jsx @@ -341,6 +341,7 @@ const CippTemplateFieldRenderer = ({ name={`${fieldPath}.settings.${index}.settingInstance.simpleSettingValue.value`} formControl={formControl} helperText={`Definition ID: ${settingInstance.settingDefinitionId}`} + includeSystemVariables={true} />
    ); @@ -416,6 +417,7 @@ const CippTemplateFieldRenderer = ({ label="Value" name={`${fieldPath}.omaSettings.${index}.value`} formControl={formControl} + includeSystemVariables={true} /> @@ -510,6 +512,7 @@ const CippTemplateFieldRenderer = ({ label={`${getCippTranslation(key)} ${index + 1}`} name={`${fieldPath}.${index}`} formControl={formControl} + includeSystemVariables={true} /> )} @@ -623,6 +626,7 @@ const CippTemplateFieldRenderer = ({ label={getCippTranslation(key)} name={fieldPath} formControl={formControl} + includeSystemVariables={true} /> ); @@ -668,6 +672,7 @@ const CippTemplateFieldRenderer = ({ label={getCippTranslation(key)} name={fieldPath} formControl={formControl} + includeSystemVariables={true} /> ); From 8e61d48792b232ee18f7b60b9918792fdf1de520 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 3 Oct 2025 14:44:35 -0400 Subject: [PATCH 077/112] fix query key --- src/components/CippTable/CippGraphExplorerFilter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/CippTable/CippGraphExplorerFilter.js b/src/components/CippTable/CippGraphExplorerFilter.js index 5ce4ca6f1b16..a6b82ccc80ff 100644 --- a/src/components/CippTable/CippGraphExplorerFilter.js +++ b/src/components/CippTable/CippGraphExplorerFilter.js @@ -163,7 +163,7 @@ const CippGraphExplorerFilter = ({ }, [currentEndpoint, debouncedRefetch]); const savePresetApi = ApiPostCall({ - relatedQueryKeys: ["ListGraphExplorerPresets", "ListGraphRequest", ...relatedQueryKeys], + relatedQueryKeys: ["ListGraphExplorerPresets*", "ListGraphRequest", ...relatedQueryKeys], }); // Save preset function From a5b60966f652cc96d603a83c4c4b661f0de6b0da Mon Sep 17 00:00:00 2001 From: Peter Vive <95594418+PeterVive@users.noreply.github.com> Date: Fri, 3 Oct 2025 20:51:20 +0200 Subject: [PATCH 078/112] Fix #4751 - Format non-compliant policy JSON in standards compare nicely. --- .../tenant/standards/manage-drift/compare.js | 35 +++++++++++++------ 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/src/pages/tenant/standards/manage-drift/compare.js b/src/pages/tenant/standards/manage-drift/compare.js index 2f26f3cb929f..1dc45c1a36b1 100644 --- a/src/pages/tenant/standards/manage-drift/compare.js +++ b/src/pages/tenant/standards/manage-drift/compare.js @@ -1304,16 +1304,29 @@ const Page = () => { JSON.stringify(actualValue) !== JSON.stringify(standardValueForKey); + // Format the display value + let displayValue; + if (typeof value === "object" && value !== null) { + displayValue = value?.label || JSON.stringify(value, null, 2); + } else if (value === true) { + displayValue = "Enabled"; + } else if (value === false) { + displayValue = "Disabled"; + } else { + displayValue = String(value); + } + return ( - + {key}: - {" "} + { isDifferent ? "medium" : "inherit", + wordBreak: "break-word", + overflowWrap: "break-word", + whiteSpace: "pre-wrap", + flex: 1, + minWidth: 0, + fontFamily: typeof value === "object" && value !== null && !value?.label ? "monospace" : "inherit", + fontSize: typeof value === "object" && value !== null && !value?.label ? "0.75rem" : "inherit", + m: 0, }} > - {typeof value === "object" && value !== null - ? value?.label || JSON.stringify(value) - : value === true - ? "Enabled" - : value === false - ? "Disabled" - : String(value)} + {displayValue} ); From 72362c0bd1d0d6a981ec1e536eb7031fd2e5595b Mon Sep 17 00:00:00 2001 From: Peter Vive <95594418+PeterVive@users.noreply.github.com> Date: Fri, 3 Oct 2025 20:59:23 +0200 Subject: [PATCH 079/112] Fixes #4743 - Switch assignedLicenses filter to "some" instead of "every" (OR instead of AND) --- src/utils/get-cipp-filter-variant.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/get-cipp-filter-variant.js b/src/utils/get-cipp-filter-variant.js index 189005739274..e49ece0b1fb2 100644 --- a/src/utils/get-cipp-filter-variant.js +++ b/src/utils/get-cipp-filter-variant.js @@ -63,7 +63,7 @@ export const getCippFilterVariant = (providedColumnKeys, arg) => { return false; } const userSkuIds = userLicenses.map((license) => license.skuId).filter(Boolean); - return filterValue.every((selectedSkuId) => userSkuIds.includes(selectedSkuId)); + return filterValue.some((selectedSkuId) => userSkuIds.includes(selectedSkuId)); }, filterSelectOptions: filterSelectOptions, }; From 695ff4e6797f6cf9bba711e2f913e527d713c6a7 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 3 Oct 2025 15:46:39 -0400 Subject: [PATCH 080/112] null safety on the tenant form selector --- .../CippComponents/CippFormTenantSelector.jsx | 57 +++++++++++-------- 1 file changed, 32 insertions(+), 25 deletions(-) diff --git a/src/components/CippComponents/CippFormTenantSelector.jsx b/src/components/CippComponents/CippFormTenantSelector.jsx index 42b0adca698b..0ce271515d29 100644 --- a/src/components/CippComponents/CippFormTenantSelector.jsx +++ b/src/components/CippComponents/CippFormTenantSelector.jsx @@ -33,24 +33,26 @@ export const CippFormTenantSelector = ({ const buildApiUrl = () => { const baseUrl = allTenants ? "/api/ListTenants?AllTenantSelector=true" : "/api/ListTenants"; const params = new URLSearchParams(); - + if (allTenants) { params.append("AllTenantSelector", "true"); } - + if (includeOffboardingDefaults) { params.append("IncludeOffboardingDefaults", "true"); } - - return params.toString() ? `${baseUrl.split('?')[0]}?${params.toString()}` : baseUrl.split('?')[0]; + + return params.toString() + ? `${baseUrl.split("?")[0]}?${params.toString()}` + : baseUrl.split("?")[0]; }; - + // Fetch tenant list const tenantList = ApiGetCall({ url: buildApiUrl(), - queryKey: allTenants - ? `ListTenants-FormAllTenantSelector${includeOffboardingDefaults ? '-WithOffboarding' : ''}` - : `ListTenants-FormnotAllTenants${includeOffboardingDefaults ? '-WithOffboarding' : ''}`, + queryKey: allTenants + ? `ListTenants-FormAllTenantSelector${includeOffboardingDefaults ? "-WithOffboarding" : ""}` + : `ListTenants-FormnotAllTenants${includeOffboardingDefaults ? "-WithOffboarding" : ""}`, }); // Fetch tenant group list if includeGroups is true @@ -65,26 +67,31 @@ export const CippFormTenantSelector = ({ useEffect(() => { if (tenantList.isSuccess && (!includeGroups || tenantGroupList.isSuccess)) { - const tenantData = tenantList.data.map((tenant) => ({ - value: tenant[valueField], - label: `${tenant.displayName} (${tenant.defaultDomainName})`, - type: "Tenant", - addedFields: { - defaultDomainName: tenant.defaultDomainName, - displayName: tenant.displayName, - customerId: tenant.customerId, - ...(includeOffboardingDefaults && { offboardingDefaults: tenant.offboardingDefaults }), - }, - })); - - const groupData = includeGroups - ? tenantGroupList?.data?.Results?.map((group) => ({ - value: group.Id, - label: group.Name, - type: "Group", + const tenantData = Array.isArray(tenantList.data) + ? tenantList.data.map((tenant) => ({ + value: tenant[valueField], + label: `${tenant.displayName} (${tenant.defaultDomainName})`, + type: "Tenant", + addedFields: { + defaultDomainName: tenant.defaultDomainName, + displayName: tenant.displayName, + customerId: tenant.customerId, + ...(includeOffboardingDefaults && { + offboardingDefaults: tenant.offboardingDefaults, + }), + }, })) : []; + const groupData = + includeGroups && Array.isArray(tenantGroupList?.data?.Results) + ? tenantGroupList.data.Results.map((group) => ({ + value: group.Id, + label: group.Name, + type: "Group", + })) + : []; + setOptions([...tenantData, ...groupData]); } }, [tenantList.isSuccess, tenantGroupList.isSuccess, includeGroups, includeOffboardingDefaults]); From bc976349b5be67f90dd110184cb03dc570be7f35 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 3 Oct 2025 22:30:43 -0400 Subject: [PATCH 081/112] memoize autocomplete --- .../CippTextFieldWithVariables.jsx | 15 ++--- .../CippVariableAutocomplete.jsx | 4 +- src/hooks/useCustomVariables.js | 62 ++++++++++--------- 3 files changed, 43 insertions(+), 38 deletions(-) diff --git a/src/components/CippComponents/CippTextFieldWithVariables.jsx b/src/components/CippComponents/CippTextFieldWithVariables.jsx index 530981e216aa..1ff1cffd9582 100644 --- a/src/components/CippComponents/CippTextFieldWithVariables.jsx +++ b/src/components/CippComponents/CippTextFieldWithVariables.jsx @@ -1,4 +1,4 @@ -import React, { useState, useRef, useEffect } from "react"; +import React, { useState, useRef, useEffect, useMemo, useCallback } from "react"; import { TextField } from "@mui/material"; import { CippVariableAutocomplete } from "./CippVariableAutocomplete"; import { useSettings } from "/src/hooks/use-settings.js"; @@ -19,15 +19,16 @@ export const CippTextFieldWithVariables = ({ const [cursorPosition, setCursorPosition] = useState(0); const textFieldRef = useRef(null); - const tenant = useSettings().currentTenant; - const tenantFilter = tenant || null; + const settings = useSettings(); + // Memoize tenant filter to prevent unnecessary re-renders + const tenantFilter = useMemo(() => settings?.currentTenant || null, [settings?.currentTenant]); // Safely close autocomplete - const closeAutocomplete = () => { + const closeAutocomplete = useCallback(() => { setShowAutocomplete(false); setSearchQuery(""); setAutocompleteAnchor(null); - }; + }, []); // Track cursor position const handleSelectionChange = () => { @@ -84,7 +85,7 @@ export const CippTextFieldWithVariables = ({ }; // Handle variable selection - const handleVariableSelect = (variableString) => { + const handleVariableSelect = useCallback((variableString) => { if (!onChange) { return; } @@ -137,7 +138,7 @@ export const CippTextFieldWithVariables = ({ } closeAutocomplete(); - }; + }, [value, cursorPosition, onChange, closeAutocomplete]); // Handle key events const handleKeyDown = (event) => { diff --git a/src/components/CippComponents/CippVariableAutocomplete.jsx b/src/components/CippComponents/CippVariableAutocomplete.jsx index 961f601e80d6..642005eb8499 100644 --- a/src/components/CippComponents/CippVariableAutocomplete.jsx +++ b/src/components/CippComponents/CippVariableAutocomplete.jsx @@ -6,7 +6,7 @@ import { useCustomVariables } from "/src/hooks/useCustomVariables"; * Autocomplete component specifically for custom variables * Shows when user types % in a text field */ -export const CippVariableAutocomplete = ({ +export const CippVariableAutocomplete = React.memo(({ open, anchorEl, onClose, @@ -147,4 +147,4 @@ export const CippVariableAutocomplete = ({ ); -}; +}); diff --git a/src/hooks/useCustomVariables.js b/src/hooks/useCustomVariables.js index 3c3aa887ef8a..1e7958763be3 100644 --- a/src/hooks/useCustomVariables.js +++ b/src/hooks/useCustomVariables.js @@ -8,40 +8,44 @@ import { ApiGetCall } from "/src/api/ApiCall"; * @returns {object} { variables, isLoading, error } */ export const useCustomVariables = (tenantFilter = null, includeSystemVariables = false) => { - // Simple, consistent query key using prefix pattern - // React Query can invalidate with wildcards like "CustomVariables*" - - if (tenantFilter === "AllTenants") { - tenantFilter = null; // Normalize to null for global context - } - const queryKey = `CustomVariables-${tenantFilter || "global"}-${ - includeSystemVariables ? "withSystem" : "noSystem" - }`; - - // Simple related keys pattern - React Query supports predicate-based invalidation - const relatedQueryKeys = ["CustomVariables*"]; - - // Build API URL with optional tenant filter and system variables setting - let apiUrl = "/api/ListCustomVariables"; - const params = new URLSearchParams(); + // Memoize normalized tenant filter and query parameters + const normalizedParams = useMemo(() => { + // Normalize tenantFilter + const normalizedTenantFilter = tenantFilter === "AllTenants" ? null : tenantFilter; + + // Generate query key + const queryKey = `CustomVariables-${normalizedTenantFilter || "global"}-${ + includeSystemVariables ? "withSystem" : "noSystem" + }`; + + // Build API URL + let apiUrl = "/api/ListCustomVariables"; + const params = new URLSearchParams(); + + if (normalizedTenantFilter) { + params.append("tenantFilter", normalizedTenantFilter); + } - if (tenantFilter) { - params.append("tenantFilter", tenantFilter); - } + if (!includeSystemVariables) { + params.append("includeSystem", "false"); + } - if (!includeSystemVariables) { - params.append("includeSystem", "false"); - } + if (params.toString()) { + apiUrl += `?${params.toString()}`; + } - if (params.toString()) { - apiUrl += `?${params.toString()}`; - } + return { + queryKey, + apiUrl, + relatedQueryKeys: ["CustomVariables*"] + }; + }, [tenantFilter, includeSystemVariables]); // Fetch variables from API const apiCall = ApiGetCall({ - url: apiUrl, - queryKey, - relatedQueryKeys, + url: normalizedParams.apiUrl, + queryKey: normalizedParams.queryKey, + relatedQueryKeys: normalizedParams.relatedQueryKeys, refetchOnMount: false, refetchOnReconnect: false, staleTime: 5 * 60 * 1000, // 5 minutes - variables don't change often @@ -128,6 +132,6 @@ export const useCustomVariables = (tenantFilter = null, includeSystemVariables = isError: apiCall.isError, error: apiCall.error, metadata: apiCall.data?.Metadata, - relatedQueryKeys, // Expose related query keys for other components + relatedQueryKeys: normalizedParams.relatedQueryKeys, // Expose related query keys for other components }; }; From 428d2f02f0a8530728033404ff2b2d0fadf71b9c Mon Sep 17 00:00:00 2001 From: Zac Richards <107489668+Zacgoose@users.noreply.github.com> Date: Sat, 4 Oct 2025 10:35:33 +0800 Subject: [PATCH 082/112] Add array checks for backup and config data Ensures backupList.data and existingBackupConfig.data are arrays before processing to prevent runtime errors when API responses are not arrays. --- .../tenant/standards/manage-drift/configuration-backup.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pages/tenant/standards/manage-drift/configuration-backup.js b/src/pages/tenant/standards/manage-drift/configuration-backup.js index 26364fb41c82..da15ad6e38f9 100644 --- a/src/pages/tenant/standards/manage-drift/configuration-backup.js +++ b/src/pages/tenant/standards/manage-drift/configuration-backup.js @@ -65,7 +65,7 @@ const Page = () => { }); // Use the actual backup files as the backup data - const filteredBackupData = backupList.data || []; + const filteredBackupData = Array.isArray(backupList.data) ? backupList.data : []; // Generate backup tags from actual API response items - use raw items directly const generateBackupTags = (backup) => { // Use the Items array directly from the API response without any translation @@ -88,9 +88,9 @@ const Page = () => { })); // Process existing backup configuration, find tenantFilter. by comparing settings.currentTenant with Tenant.value - const currentConfig = existingBackupConfig.data?.find( - (tenant) => tenant.Tenant.value === settings.currentTenant - ); + const currentConfig = Array.isArray(existingBackupConfig.data) + ? existingBackupConfig.data.find((tenant) => tenant.Tenant.value === settings.currentTenant) + : null; const hasExistingConfig = currentConfig && currentConfig.Parameters?.ScheduledBackupValues; // Create property items for current configuration From 068e4e0499b777d07f9d27b5f50cf8d2a014e5cf Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 3 Oct 2025 23:00:10 -0400 Subject: [PATCH 083/112] fix variable autocomplete --- .../CippComponents/CippFormComponent.jsx | 44 +-- .../CippTextFieldWithVariables.jsx | 100 +++---- .../CippVariableAutocomplete.jsx | 266 +++++++++--------- src/hooks/useCustomVariables.js | 4 +- 4 files changed, 210 insertions(+), 204 deletions(-) diff --git a/src/components/CippComponents/CippFormComponent.jsx b/src/components/CippComponents/CippFormComponent.jsx index 0b51f2040c9b..af77e98ed1e1 100644 --- a/src/components/CippComponents/CippFormComponent.jsx +++ b/src/components/CippComponents/CippFormComponent.jsx @@ -124,13 +124,13 @@ export const CippFormComponent = (props) => { return ( <>
    - {!disableVariables ? ( - ( + + !disableVariables ? ( { onChange={field.onChange} includeSystemVariables={includeSystemVariables} /> - )} - /> - ) : ( - - )} + ) : ( + + ) + } + />
    {get(errors, convertedName, {})?.message} diff --git a/src/components/CippComponents/CippTextFieldWithVariables.jsx b/src/components/CippComponents/CippTextFieldWithVariables.jsx index 1ff1cffd9582..0f6abd9fea99 100644 --- a/src/components/CippComponents/CippTextFieldWithVariables.jsx +++ b/src/components/CippComponents/CippTextFieldWithVariables.jsx @@ -85,60 +85,64 @@ export const CippTextFieldWithVariables = ({ }; // Handle variable selection - const handleVariableSelect = useCallback((variableString) => { - if (!onChange) { - return; - } + const handleVariableSelect = useCallback( + (variableString) => { + if (!onChange) { + return; + } - // Use the value prop instead of DOM value since we're in a controlled component - const currentValue = value || ""; + // Use the value prop instead of DOM value since we're in a controlled component + const currentValue = value || ""; - // Get fresh cursor position from the DOM - let cursorPos = cursorPosition; - if (textFieldRef.current) { - const inputElement = textFieldRef.current.querySelector("input") || textFieldRef.current; - if (inputElement && typeof inputElement.selectionStart === "number") { - cursorPos = inputElement.selectionStart; + // Get fresh cursor position from the DOM + let cursorPos = cursorPosition; + if (textFieldRef.current) { + const inputElement = textFieldRef.current.querySelector("input") || textFieldRef.current; + if (inputElement && typeof inputElement.selectionStart === "number") { + cursorPos = inputElement.selectionStart; + } } - } - // Find the % that triggered the autocomplete - const lastPercentIndex = currentValue.lastIndexOf("%", cursorPos - 1); - - if (lastPercentIndex !== -1) { - // Replace from % to cursor position with the selected variable - const beforePercent = currentValue.substring(0, lastPercentIndex); - const afterCursor = currentValue.substring(cursorPos); - const newValue = beforePercent + variableString + afterCursor; - - // Create synthetic event for onChange - const syntheticEvent = { - target: { - name: textFieldRef.current?.name || "", - value: newValue, - }, - }; - - onChange(syntheticEvent); - - // Set cursor position after the inserted variable - setTimeout(() => { - if (textFieldRef.current) { - const newCursorPos = lastPercentIndex + variableString.length; - - // Access the actual input element for Material-UI TextField - const inputElement = textFieldRef.current.querySelector("input") || textFieldRef.current; - if (inputElement && inputElement.setSelectionRange) { - inputElement.setSelectionRange(newCursorPos, newCursorPos); - inputElement.focus(); + // Find the % that triggered the autocomplete + const lastPercentIndex = currentValue.lastIndexOf("%", cursorPos - 1); + + if (lastPercentIndex !== -1) { + // Replace from % to cursor position with the selected variable + const beforePercent = currentValue.substring(0, lastPercentIndex); + const afterCursor = currentValue.substring(cursorPos); + const newValue = beforePercent + variableString + afterCursor; + + // Create synthetic event for onChange + const syntheticEvent = { + target: { + name: textFieldRef.current?.name || "", + value: newValue, + }, + }; + + onChange(syntheticEvent); + + // Set cursor position after the inserted variable + setTimeout(() => { + if (textFieldRef.current) { + const newCursorPos = lastPercentIndex + variableString.length; + + // Access the actual input element for Material-UI TextField + const inputElement = + textFieldRef.current.querySelector("input") || textFieldRef.current; + if (inputElement && inputElement.setSelectionRange) { + inputElement.setSelectionRange(newCursorPos, newCursorPos); + inputElement.focus(); + } + setCursorPosition(newCursorPos); } - setCursorPosition(newCursorPos); - } - }, 0); - } + }, 0); + } - closeAutocomplete(); - }, [value, cursorPosition, onChange, closeAutocomplete]); + closeAutocomplete(); + }, + [value, cursorPosition, onChange, closeAutocomplete] + ); // Handle key events const handleKeyDown = (event) => { diff --git a/src/components/CippComponents/CippVariableAutocomplete.jsx b/src/components/CippComponents/CippVariableAutocomplete.jsx index 642005eb8499..f09bea204a3f 100644 --- a/src/components/CippComponents/CippVariableAutocomplete.jsx +++ b/src/components/CippComponents/CippVariableAutocomplete.jsx @@ -6,145 +6,147 @@ import { useCustomVariables } from "/src/hooks/useCustomVariables"; * Autocomplete component specifically for custom variables * Shows when user types % in a text field */ -export const CippVariableAutocomplete = React.memo(({ - open, - anchorEl, - onClose, - onSelect, - searchQuery = "", - tenantFilter = null, - includeSystemVariables = false, - position = { top: 0, left: 0 }, // Cursor position for floating box -}) => { - const theme = useTheme(); - const { variables, isLoading, groupedVariables } = useCustomVariables( - tenantFilter, - includeSystemVariables - ); - const [filteredVariables, setFilteredVariables] = useState([]); +export const CippVariableAutocomplete = React.memo( + ({ + open, + anchorEl, + onClose, + onSelect, + searchQuery = "", + tenantFilter = null, + includeSystemVariables = false, + position = { top: 0, left: 0 }, // Cursor position for floating box + }) => { + const theme = useTheme(); + const { variables, isLoading, groupedVariables } = useCustomVariables( + tenantFilter, + includeSystemVariables + ); + const [filteredVariables, setFilteredVariables] = useState([]); - // Filter variables based on search query - useEffect(() => { - if (!searchQuery) { - setFilteredVariables(variables); - return; - } + // Filter variables based on search query + useEffect(() => { + if (!searchQuery) { + setFilteredVariables(variables); + return; + } - const lowerQuery = searchQuery.toLowerCase(); - const filtered = variables.filter( - (variable) => - variable.name.toLowerCase().includes(lowerQuery) || - variable.description.toLowerCase().includes(lowerQuery) - ); - setFilteredVariables(filtered); - }, [searchQuery, variables]); + const lowerQuery = searchQuery.toLowerCase(); + const filtered = variables.filter( + (variable) => + variable.name.toLowerCase().includes(lowerQuery) || + variable.description.toLowerCase().includes(lowerQuery) + ); + setFilteredVariables(filtered); + }, [searchQuery, variables]); - const handleSelect = (event, value) => { - if (value && onSelect) { - onSelect(value.variable); // Pass the full variable string like %tenantname% - } - onClose(); - }; + const handleSelect = (event, value) => { + if (value && onSelect) { + onSelect(value.variable); // Pass the full variable string like %tenantname% + } + onClose(); + }; - if (!open) { - return null; - } + if (!open) { + return null; + } - if (isLoading) { - return null; - } + if (isLoading) { + return null; + } - if (!variables || variables.length === 0) { - return null; - } + if (!variables || variables.length === 0) { + return null; + } - if (filteredVariables.length === 0) { - return null; - } + if (filteredVariables.length === 0) { + return null; + } - return ( - - { - e.stopPropagation(); - }} + return ( + - {filteredVariables.map((variable, index) => ( - { - e.stopPropagation(); - e.preventDefault(); - handleSelect(e, variable); - }} - sx={{ - display: "flex", - justifyContent: "space-between", - alignItems: "center", - py: 1, - px: 2, - borderBottom: `1px solid ${theme.palette.divider}`, - "&:hover": { - backgroundColor: theme.palette.action.hover, - }, - cursor: "pointer", - }} - > - - - {variable.variable} - - - {variable.description} - - + { + e.stopPropagation(); + }} + > + {filteredVariables.map((variable, index) => ( + { + e.stopPropagation(); + e.preventDefault(); + handleSelect(e, variable); + }} + sx={{ + display: "flex", + justifyContent: "space-between", + alignItems: "center", + py: 1, + px: 2, + borderBottom: `1px solid ${theme.palette.divider}`, + "&:hover": { + backgroundColor: theme.palette.action.hover, + }, + cursor: "pointer", + }} + > + + + {variable.variable} + + + {variable.description} + + - - - - - ))} - - - ); -}); + + + + + ))} + + + ); + } +); diff --git a/src/hooks/useCustomVariables.js b/src/hooks/useCustomVariables.js index 1e7958763be3..76c2c5f54fbf 100644 --- a/src/hooks/useCustomVariables.js +++ b/src/hooks/useCustomVariables.js @@ -12,7 +12,7 @@ export const useCustomVariables = (tenantFilter = null, includeSystemVariables = const normalizedParams = useMemo(() => { // Normalize tenantFilter const normalizedTenantFilter = tenantFilter === "AllTenants" ? null : tenantFilter; - + // Generate query key const queryKey = `CustomVariables-${normalizedTenantFilter || "global"}-${ includeSystemVariables ? "withSystem" : "noSystem" @@ -37,7 +37,7 @@ export const useCustomVariables = (tenantFilter = null, includeSystemVariables = return { queryKey, apiUrl, - relatedQueryKeys: ["CustomVariables*"] + relatedQueryKeys: ["CustomVariables*"], }; }, [tenantFilter, includeSystemVariables]); From 8c941c000487ea0ca8a876dfc20db877460d70f7 Mon Sep 17 00:00:00 2001 From: Zac Richards <107489668+Zacgoose@users.noreply.github.com> Date: Sat, 4 Oct 2025 11:29:35 +0800 Subject: [PATCH 084/112] Add custom variables to backups --- .../CippComponents/CippBackupScheduleDrawer.jsx | 11 +++++++++++ .../CippComponents/CippRestoreBackupDrawer.jsx | 10 ++++++++++ .../standards/manage-drift/configuration-backup.js | 1 + 3 files changed, 22 insertions(+) diff --git a/src/components/CippComponents/CippBackupScheduleDrawer.jsx b/src/components/CippComponents/CippBackupScheduleDrawer.jsx index d1fc1709737c..36f064d06110 100644 --- a/src/components/CippComponents/CippBackupScheduleDrawer.jsx +++ b/src/components/CippComponents/CippBackupScheduleDrawer.jsx @@ -34,6 +34,7 @@ export const CippBackupScheduleDrawer = ({ antiphishing: true, CippWebhookAlerts: true, CippScriptedAlerts: true, + CippCustomVariables: true, }, }); @@ -58,6 +59,7 @@ export const CippBackupScheduleDrawer = ({ antiphishing: true, CippWebhookAlerts: true, CippScriptedAlerts: true, + CippCustomVariables: true, }); // Call onSuccess callback if provided if (onSuccess) { @@ -109,6 +111,7 @@ export const CippBackupScheduleDrawer = ({ antiphishing: true, CippWebhookAlerts: true, CippScriptedAlerts: true, + CippCustomVariables: true, }); }; @@ -267,6 +270,14 @@ export const CippBackupScheduleDrawer = ({ formControl={formControl} /> + + +
    diff --git a/src/components/CippComponents/CippRestoreBackupDrawer.jsx b/src/components/CippComponents/CippRestoreBackupDrawer.jsx index 275bf4e31abc..f011b499820f 100644 --- a/src/components/CippComponents/CippRestoreBackupDrawer.jsx +++ b/src/components/CippComponents/CippRestoreBackupDrawer.jsx @@ -35,6 +35,7 @@ export const CippRestoreBackupDrawer = ({ antiphishing: true, CippWebhookAlerts: true, CippScriptedAlerts: true, + CippCustomVariables: true, CippStandards: true, overwrite: false, webhook: false, @@ -65,6 +66,7 @@ export const CippRestoreBackupDrawer = ({ antiphishing: true, CippWebhookAlerts: true, CippScriptedAlerts: true, + CippCustomVariables: true, CippStandards: true, overwrite: false, webhook: false, @@ -103,6 +105,7 @@ export const CippRestoreBackupDrawer = ({ antiphishing: values.antiphishing, CippWebhookAlerts: values.CippWebhookAlerts, CippScriptedAlerts: values.CippScriptedAlerts, + CippCustomVariables: values.CippCustomVariables, overwrite: values.overwrite, }, }, @@ -135,6 +138,7 @@ export const CippRestoreBackupDrawer = ({ antiphishing: true, CippWebhookAlerts: true, CippScriptedAlerts: true, + CippCustomVariables: true, CippStandards: true, overwrite: false, webhook: false, @@ -303,6 +307,12 @@ export const CippRestoreBackupDrawer = ({ name="CippScriptedAlerts" formControl={formControl} /> + {/* Overwrite Existing Entries */} diff --git a/src/pages/tenant/standards/manage-drift/configuration-backup.js b/src/pages/tenant/standards/manage-drift/configuration-backup.js index da15ad6e38f9..0cf7dd569c72 100644 --- a/src/pages/tenant/standards/manage-drift/configuration-backup.js +++ b/src/pages/tenant/standards/manage-drift/configuration-backup.js @@ -143,6 +143,7 @@ const Page = () => { if (values.antiphishing) enabledComponents.push("Anti-Phishing"); if (values.CippWebhookAlerts) enabledComponents.push("CIPP Webhook Alerts"); if (values.CippScriptedAlerts) enabledComponents.push("CIPP Scripted Alerts"); + if (values.CippCustomVariables) enabledComponents.push("Custom Variables"); return enabledComponents; }; From e4b828036beaf53e6c164288bc8e50a497e7033d Mon Sep 17 00:00:00 2001 From: Zac Richards <107489668+Zacgoose@users.noreply.github.com> Date: Sat, 4 Oct 2025 12:20:11 +0800 Subject: [PATCH 085/112] use tenant id instead of default domain for tenant group editing --- src/pages/tenant/standards/manage-drift/edit-tenant.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/tenant/standards/manage-drift/edit-tenant.js b/src/pages/tenant/standards/manage-drift/edit-tenant.js index 9dab02f75766..a49d0afe00b8 100644 --- a/src/pages/tenant/standards/manage-drift/edit-tenant.js +++ b/src/pages/tenant/standards/manage-drift/edit-tenant.js @@ -214,7 +214,7 @@ const Page = () => { groupId: group.value, groupName: group.label, })), - customerId: currentTenant, + customerId: tenantDetails.data?.id, }; updateTenant.mutate({ url: "/api/EditTenant", From 08378772b9992f778a65cc28ff6b81f0fb0e5afd Mon Sep 17 00:00:00 2001 From: Zac Richards <107489668+Zacgoose@users.noreply.github.com> Date: Sat, 4 Oct 2025 13:27:06 +0800 Subject: [PATCH 086/112] Refactor user stats and chart card props Removed GlobalAdminList API call and updated user statistics to use dashboard.data.Gas for global admins. Simplified unlicensed users calculation and added totalLabel and customTotal props to CippChartCard for customizable total display. --- src/components/CippCards/CippChartCard.jsx | 7 +++-- src/pages/index.js | 34 ++++++---------------- 2 files changed, 14 insertions(+), 27 deletions(-) diff --git a/src/components/CippCards/CippChartCard.jsx b/src/components/CippCards/CippChartCard.jsx index b05652ef9ec8..577a3f2bbaf1 100644 --- a/src/components/CippCards/CippChartCard.jsx +++ b/src/components/CippCards/CippChartCard.jsx @@ -93,12 +93,15 @@ export const CippChartCard = ({ title, actions, onClick, + totalLabel = "Total", + customTotal, }) => { const [range, setRange] = useState("Last 7 days"); const [barSeries, setBarSeries] = useState([]); const chartOptions = useChartOptions(labels, chartType); chartSeries = chartSeries.filter((item) => item !== null); - const total = chartSeries.reduce((acc, value) => acc + value, 0); + const calculatedTotal = chartSeries.reduce((acc, value) => acc + value, 0); + const total = customTotal !== undefined ? customTotal : calculatedTotal; useEffect(() => { if (chartType === "bar") { setBarSeries( @@ -160,7 +163,7 @@ export const CippChartCard = ({ > {labels.length > 0 && ( <> - Total + {totalLabel} {isFetching ? "0" : total} )} diff --git a/src/pages/index.js b/src/pages/index.js index 4aaa3606a4f1..4d8478b2f58e 100644 --- a/src/pages/index.js +++ b/src/pages/index.js @@ -32,16 +32,6 @@ const Page = () => { queryKey: `${currentTenant}-ListuserCounts`, }); - const GlobalAdminList = ApiGetCall({ - url: "/api/ListGraphRequest", - queryKey: `${currentTenant}-ListGraphRequest`, - data: { - tenantFilter: currentTenant, - Endpoint: "/directoryRoles(roleTemplateId='62e90394-69f5-4237-9190-012177145e10')/members", - $select: "displayName,userPrincipalName,accountEnabled", - }, - }); - const sharepoint = ApiGetCall({ url: "/api/ListSharepointQuota", queryKey: `${currentTenant}-ListSharepointQuota`, @@ -293,11 +283,11 @@ const Page = () => { tenantId={organization.data?.id} userStats={{ licensedUsers: dashboard.data?.LicUsers || 0, - unlicensedUsers: dashboard.data?.Users && dashboard.data?.LicUsers && GlobalAdminList.data?.Results && dashboard.data?.Guests - ? dashboard.data?.Users - dashboard.data?.LicUsers - dashboard.data?.Guests - GlobalAdminList.data?.Results?.length + unlicensedUsers: dashboard.data?.Users && dashboard.data?.LicUsers + ? dashboard.data?.Users - dashboard.data?.LicUsers : 0, guests: dashboard.data?.Guests || 0, - globalAdmins: GlobalAdminList.data?.Results?.length || 0 + globalAdmins: dashboard.data?.Gas || 0 }} standardsData={driftApi.data} organizationData={organization.data} @@ -316,23 +306,17 @@ const Page = () => { From 4965b8c880717fb23bbc667e64b88dc27270db03 Mon Sep 17 00:00:00 2001 From: Peter Vive <95594418+PeterVive@users.noreply.github.com> Date: Sat, 4 Oct 2025 10:02:54 +0200 Subject: [PATCH 087/112] Implemented Assignment Filters - With template support and pages to support it. - Includes standard for deployment - Allows optional usage in "Intune Template" standard for assignment. Fixes https://github.com/KelvinTegelaar/CIPP/issues/4721 --- .../CippComponents/CippTranslations.jsx | 1 + .../CippAddAssignmentFilterForm.jsx | 101 +++++++++++++ .../CippAddAssignmentFilterTemplateForm.jsx | 100 +++++++++++++ .../CippWizardAssignmentFilterTemplates.jsx | 132 +++++++++++++++++ src/data/standards.json | 53 +++++++ src/layouts/config.js | 10 ++ .../MEM/assignment-filter-templates/add.jsx | 37 +++++ .../MEM/assignment-filter-templates/deploy.js | 43 ++++++ .../MEM/assignment-filter-templates/edit.jsx | 77 ++++++++++ .../MEM/assignment-filter-templates/index.js | 133 ++++++++++++++++++ .../endpoint/MEM/assignment-filters/add.jsx | 42 ++++++ .../endpoint/MEM/assignment-filters/edit.jsx | 78 ++++++++++ .../endpoint/MEM/assignment-filters/index.js | 92 ++++++++++++ 13 files changed, 899 insertions(+) create mode 100644 src/components/CippFormPages/CippAddAssignmentFilterForm.jsx create mode 100644 src/components/CippFormPages/CippAddAssignmentFilterTemplateForm.jsx create mode 100644 src/components/CippWizard/CippWizardAssignmentFilterTemplates.jsx create mode 100644 src/pages/endpoint/MEM/assignment-filter-templates/add.jsx create mode 100644 src/pages/endpoint/MEM/assignment-filter-templates/deploy.js create mode 100644 src/pages/endpoint/MEM/assignment-filter-templates/edit.jsx create mode 100644 src/pages/endpoint/MEM/assignment-filter-templates/index.js create mode 100644 src/pages/endpoint/MEM/assignment-filters/add.jsx create mode 100644 src/pages/endpoint/MEM/assignment-filters/edit.jsx create mode 100644 src/pages/endpoint/MEM/assignment-filters/index.js diff --git a/src/components/CippComponents/CippTranslations.jsx b/src/components/CippComponents/CippTranslations.jsx index 6ab449050f2e..99d46a6e5182 100644 --- a/src/components/CippComponents/CippTranslations.jsx +++ b/src/components/CippComponents/CippTranslations.jsx @@ -51,4 +51,5 @@ export const CippTranslations = { sendtoIntegration: "Send Notifications to Integration", includeTenantId: "Include Tenant ID in Notifications", logsToInclude: "Logs to Include in notifications", + assignmentFilterManagementType: "Filter Type", }; diff --git a/src/components/CippFormPages/CippAddAssignmentFilterForm.jsx b/src/components/CippFormPages/CippAddAssignmentFilterForm.jsx new file mode 100644 index 000000000000..f94cd59b3780 --- /dev/null +++ b/src/components/CippFormPages/CippAddAssignmentFilterForm.jsx @@ -0,0 +1,101 @@ +import { useEffect } from "react"; +import "@mui/material"; +import { Grid } from "@mui/system"; +import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; + +const CippAddAssignmentFilterForm = (props) => { + const { formControl, isEdit = false } = props; + + useEffect(() => { + const subscription = formControl.watch((value, { name, type }) => {}); + return () => subscription.unsubscribe(); + }, [formControl]); + + return ( + + + + + + + + + + + + + + + + + + + Enter the filter rule using Intune filter syntax. See{" "} + + Microsoft documentation + {" "} + for supported properties and operators. + + } + required + multiline + rows={6} + fullWidth + /> + + + ); +}; + +export default CippAddAssignmentFilterForm; diff --git a/src/components/CippFormPages/CippAddAssignmentFilterTemplateForm.jsx b/src/components/CippFormPages/CippAddAssignmentFilterTemplateForm.jsx new file mode 100644 index 000000000000..3fb22fa45306 --- /dev/null +++ b/src/components/CippFormPages/CippAddAssignmentFilterTemplateForm.jsx @@ -0,0 +1,100 @@ +import { useEffect } from "react"; +import "@mui/material"; +import { Grid } from "@mui/system"; +import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; + +const CippAddAssignmentFilterTemplateForm = (props) => { + const { formControl } = props; + + useEffect(() => { + const subscription = formControl.watch((value, { name, type }) => {}); + return () => subscription.unsubscribe(); + }, [formControl]); + + return ( + + {/* Hidden field to store the template GUID when editing */} + + + + + + + + + + + + + + + + + + + + Enter the filter rule using Intune filter syntax. See{" "} + + Microsoft documentation + {" "} + for supported properties and operators. + + } + required + multiline + rows={6} + fullWidth + /> + + + ); +}; + +export default CippAddAssignmentFilterTemplateForm; diff --git a/src/components/CippWizard/CippWizardAssignmentFilterTemplates.jsx b/src/components/CippWizard/CippWizardAssignmentFilterTemplates.jsx new file mode 100644 index 000000000000..7a39a12ce1ee --- /dev/null +++ b/src/components/CippWizard/CippWizardAssignmentFilterTemplates.jsx @@ -0,0 +1,132 @@ +import { Stack } from "@mui/material"; +import CippWizardStepButtons from "./CippWizardStepButtons"; +import CippFormComponent from "../CippComponents/CippFormComponent"; +import { Grid } from "@mui/system"; +import { useWatch } from "react-hook-form"; +import { useEffect } from "react"; + +export const CippWizardAssignmentFilterTemplates = (props) => { + const { postUrl, formControl, onPreviousStep, onNextStep, currentStep } = props; + const watcher = useWatch({ control: formControl.control, name: "TemplateList" }); + + const platformOptions = [ + { label: "Windows 10 and Later", value: "windows10AndLater" }, + { label: "iOS", value: "iOS" }, + { label: "Android", value: "android" }, + { label: "macOS", value: "macOS" }, + { label: "Android Work Profile", value: "androidWorkProfile" }, + { label: "Android AOSP", value: "androidAOSP" }, + ]; + + const filterTypeOptions = [ + { label: "Devices", value: "devices" }, + { label: "Apps", value: "apps" }, + ]; + + useEffect(() => { + if (watcher?.value) { + console.log("Loading template:", watcher); + + // Set platform first to ensure conditional fields are visible + formControl.setValue("platform", watcher.addedFields.platform); + + // Use setTimeout to ensure the DOM updates before setting other fields + setTimeout(() => { + formControl.setValue("displayName", watcher.addedFields.displayName); + formControl.setValue("description", watcher.addedFields.description); + formControl.setValue("rule", watcher.addedFields.rule); + formControl.setValue("assignmentFilterManagementType", watcher.addedFields.assignmentFilterManagementType); + + console.log("Set rule to:", watcher.addedFields.rule); + }, 100); + } + }, [watcher]); + + return ( + + + + + `${option.Displayname || option.displayName} (${option.platform})`, + valueField: "GUID", + addedField: { + platform: "platform", + displayName: "displayName", + description: "description", + rule: "rule", + assignmentFilterManagementType: "assignmentFilterManagementType", + }, + showRefresh: true, + }} + /> + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/src/data/standards.json b/src/data/standards.json index 4c6be119947a..b4c5a51f8583 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -4846,6 +4846,30 @@ "type": "textField", "required": false, "helpText": "Enter the group name(s) to exclude from the assignment. Wildcards are allowed. Multiple group names are comma-seperated." + }, + { + "type": "textField", + "required": false, + "name": "assignmentFilter", + "label": "Assignment Filter Name (Optional)", + "helpText": "Enter the assignment filter name to apply to this policy assignment. Wildcards are allowed." + }, + { + "name": "assignmentFilterType", + "label": "Assignment Filter Mode (Optional)", + "type": "radio", + "required": false, + "helpText": "Choose whether to include or exclude devices matching the filter. Only applies if you specified a filter name above. Defaults to Include if not specified.", + "options": [ + { + "label": "Include - Assign to devices matching the filter", + "value": "include" + }, + { + "label": "Exclude - Assign to devices NOT matching the filter", + "value": "exclude" + } + ] } ] }, @@ -4989,6 +5013,35 @@ } ] }, + { + "name": "standards.AssignmentFilterTemplate", + "label": "Assignment Filter Template", + "multi": true, + "cat": "Templates", + "disabledFeatures": { + "report": true, + "warn": true, + "remediate": false + }, + "impact": "Medium Impact", + "addedDate": "2025-10-04", + "helpText": "Deploy and manage assignment filter templates.", + "executiveText": "Creates standardized assignment filters with predefined settings. These templates ensure consistent assignment filter configurations across the organization, streamlining assignment management.", + "addedComponent": [ + { + "type": "autoComplete", + "name": "assignmentFilterTemplate", + "label": "Select Assignment Filter Template", + "api": { + "url": "/api/ListAssignmentFilterTemplates", + "labelField": "Displayname", + "altLabelField": "displayName", + "valueField": "GUID", + "queryKey": "ListAssignmentFilterTemplates" + } + } + ] + }, { "name": "standards.MailboxRecipientLimits", "cat": "Exchange Standards", diff --git a/src/layouts/config.js b/src/layouts/config.js index 1e70fe8622f9..e23ff8a95dba 100644 --- a/src/layouts/config.js +++ b/src/layouts/config.js @@ -445,6 +445,16 @@ export const nativeMenuItems = [ path: "/endpoint/MEM/list-templates", permissions: ["Endpoint.MEM.*"], }, + { + title: "Assignment Filters", + path: "/endpoint/MEM/assignment-filters", + permissions: ["Endpoint.MEM.*"], + }, + { + title: "Assignment Filter Templates", + path: "/endpoint/MEM/assignment-filter-templates", + permissions: ["Endpoint.MEM.*"], + }, { title: "Scripts", path: "/endpoint/MEM/list-scripts", diff --git a/src/pages/endpoint/MEM/assignment-filter-templates/add.jsx b/src/pages/endpoint/MEM/assignment-filter-templates/add.jsx new file mode 100644 index 000000000000..ca731ffd54cb --- /dev/null +++ b/src/pages/endpoint/MEM/assignment-filter-templates/add.jsx @@ -0,0 +1,37 @@ +import { Box } from "@mui/material"; +import CippFormPage from "../../../../components/CippFormPages/CippFormPage"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { useForm } from "react-hook-form"; +import { useSettings } from "../../../../hooks/use-settings"; +import CippAddAssignmentFilterTemplateForm from "../../../../components/CippFormPages/CippAddAssignmentFilterTemplateForm"; +const Page = () => { + const userSettingsDefaults = useSettings(); + + const formControl = useForm({ + mode: "onChange", + defaultValues: { + tenantFilter: userSettingsDefaults.currentTenant, + }, + }); + + return ( + <> + + + + + + + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; diff --git a/src/pages/endpoint/MEM/assignment-filter-templates/deploy.js b/src/pages/endpoint/MEM/assignment-filter-templates/deploy.js new file mode 100644 index 000000000000..5c5f9c0ce1df --- /dev/null +++ b/src/pages/endpoint/MEM/assignment-filter-templates/deploy.js @@ -0,0 +1,43 @@ +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { CippWizardConfirmation } from "/src/components/CippWizard/CippWizardConfirmation"; +import CippWizardPage from "/src/components/CippWizard/CippWizardPage.jsx"; +import { CippTenantStep } from "/src/components/CippWizard/CippTenantStep.jsx"; +import { CippWizardAssignmentFilterTemplates } from "../../../../components/CippWizard/CippWizardAssignmentFilterTemplates"; + +const Page = () => { + const steps = [ + { + title: "Step 1", + description: "Tenant Selection", + component: CippTenantStep, + componentProps: { + allTenants: false, + type: "multiple", + }, + }, + { + title: "Step 2", + description: "Choose Template", + component: CippWizardAssignmentFilterTemplates, + }, + { + title: "Step 3", + description: "Confirmation", + component: CippWizardConfirmation, + }, + ]; + + return ( + <> + + + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; diff --git a/src/pages/endpoint/MEM/assignment-filter-templates/edit.jsx b/src/pages/endpoint/MEM/assignment-filter-templates/edit.jsx new file mode 100644 index 000000000000..2d10d7502e61 --- /dev/null +++ b/src/pages/endpoint/MEM/assignment-filter-templates/edit.jsx @@ -0,0 +1,77 @@ +import { Box, CircularProgress } from "@mui/material"; +import CippFormPage from "../../../../components/CippFormPages/CippFormPage"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { useForm } from "react-hook-form"; +import { useSettings } from "../../../../hooks/use-settings"; +import CippAddAssignmentFilterTemplateForm from "../../../../components/CippFormPages/CippAddAssignmentFilterTemplateForm"; +import { useRouter } from "next/router"; +import { ApiGetCall } from "../../../../api/ApiCall"; +import { useEffect } from "react"; + +const Page = () => { + const userSettingsDefaults = useSettings(); + const router = useRouter(); + const { id } = router.query; + + const formControl = useForm({ + mode: "onChange", + defaultValues: { + tenantFilter: userSettingsDefaults.currentTenant, + }, + }); + + // Fetch template data + const { data: template, isFetching } = ApiGetCall({ + url: `/api/ListAssignmentFilterTemplates?id=${id}`, + queryKey: `AssignmentFilterTemplate-${id}`, + waiting: !!id, + }); + + // Map groupType values to valid radio options + + // Set form values when template data is loaded + useEffect(() => { + if (template) { + const templateData = template[0]; + + // Make sure we have the necessary data before proceeding + if (templateData) { + formControl.reset({ + GUID: templateData.GUID, + displayName: templateData.displayName, + description: templateData.description, + platform: templateData.platform, + rule: templateData.rule, + assignmentFilterManagementType: templateData.assignmentFilterManagementType, + tenantFilter: userSettingsDefaults.currentTenant, + }); + } + } + }, [template, formControl, userSettingsDefaults.currentTenant]); + + return ( + <> + + {/* Add debugging output to check what values are set */} +
    {JSON.stringify(formControl.watch(), null, 2)}
    + + + + +
    + + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; diff --git a/src/pages/endpoint/MEM/assignment-filter-templates/index.js b/src/pages/endpoint/MEM/assignment-filter-templates/index.js new file mode 100644 index 000000000000..b3736a0fc346 --- /dev/null +++ b/src/pages/endpoint/MEM/assignment-filter-templates/index.js @@ -0,0 +1,133 @@ +import { Button } from "@mui/material"; +import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { AddBox, RocketLaunch, Delete, GitHub, Edit } from "@mui/icons-material"; +import Link from "next/link"; +import { ApiGetCall } from "/src/api/ApiCall"; +import { CippPropertyListCard } from "../../../../components/CippCards/CippPropertyListCard"; +import { getCippTranslation } from "../../../../utils/get-cipp-translation"; +import { getCippFormatting } from "../../../../utils/get-cipp-formatting"; + +const Page = () => { + const pageTitle = "Assignment Filter Templates"; + const integrations = ApiGetCall({ + url: "/api/ListExtensionsConfig", + queryKey: "Integrations", + refetchOnMount: false, + refetchOnReconnect: false, + }); + const actions = [ + { + label: "Edit Template", + icon: , + link: "/endpoint/MEM/assignment-filter-templates/edit?id=[GUID]", + }, + { + label: "Save to GitHub", + type: "POST", + url: "/api/ExecCommunityRepo", + icon: , + data: { + Action: "UploadTemplate", + GUID: "GUID", + }, + fields: [ + { + label: "Repository", + name: "FullName", + type: "select", + api: { + url: "/api/ListCommunityRepos", + data: { + WriteAccess: true, + }, + queryKey: "CommunityRepos-Write", + dataKey: "Results", + valueField: "FullName", + labelField: "FullName", + }, + multiple: false, + creatable: false, + required: true, + validators: { + required: { value: true, message: "This field is required" }, + }, + }, + { + label: "Commit Message", + placeholder: "Enter a commit message for adding this file to GitHub", + name: "Message", + type: "textField", + multiline: true, + required: true, + rows: 4, + }, + ], + confirmText: "Are you sure you want to save this template to the selected repository?", + condition: () => integrations.isSuccess && integrations?.data?.GitHub?.Enabled, + }, + { + label: "Delete Template", + type: "POST", + url: "/api/RemoveAssignmentFilterTemplate", + icon: , + data: { + ID: "GUID", + }, + confirmText: "Do you want to delete the template?", + multiPost: false, + }, + ]; + + const offCanvas = { + children: (data) => { + const keys = Object.keys(data).filter( + (key) => !key.includes("@odata") && !key.includes("@data") + ); + const properties = []; + keys.forEach((key) => { + if (data[key] && data[key].length > 0) { + properties.push({ + label: getCippTranslation(key), + value: getCippFormatting(data[key], key), + }); + } + }); + return ( + + ); + }, + }; + + return ( + + + + + } + offCanvas={offCanvas} + simpleColumns={["displayName", "description", "platform", "GUID"]} + /> + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; diff --git a/src/pages/endpoint/MEM/assignment-filters/add.jsx b/src/pages/endpoint/MEM/assignment-filters/add.jsx new file mode 100644 index 000000000000..68c8cb027983 --- /dev/null +++ b/src/pages/endpoint/MEM/assignment-filters/add.jsx @@ -0,0 +1,42 @@ +import { Box } from "@mui/material"; +import CippFormPage from "../../../../components/CippFormPages/CippFormPage"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { useForm } from "react-hook-form"; +import { useSettings } from "../../../../hooks/use-settings"; +import { useEffect } from "react"; +import CippAddAssignmentFilterForm from "../../../../components/CippFormPages/CippAddAssignmentFilterForm"; + +const Page = () => { + const userSettingsDefaults = useSettings(); + + const formControl = useForm({ + mode: "onChange", + defaultValues: { + assignmentFilterManagementType: "devices", + }, + }); + + useEffect(() => { + formControl.setValue("tenantFilter", userSettingsDefaults?.currentTenant || ""); + }, [userSettingsDefaults, formControl]); + + return ( + <> + + + + + + + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; diff --git a/src/pages/endpoint/MEM/assignment-filters/edit.jsx b/src/pages/endpoint/MEM/assignment-filters/edit.jsx new file mode 100644 index 000000000000..0fae5e9b6b0d --- /dev/null +++ b/src/pages/endpoint/MEM/assignment-filters/edit.jsx @@ -0,0 +1,78 @@ +import { useEffect, useState } from "react"; +import { Box } from "@mui/material"; +import { useForm } from "react-hook-form"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import CippFormPage from "/src/components/CippFormPages/CippFormPage"; +import { useRouter } from "next/router"; +import { ApiGetCall } from "../../../../api/ApiCall"; +import { useSettings } from "../../../../hooks/use-settings"; +import CippAddAssignmentFilterForm from "../../../../components/CippFormPages/CippAddAssignmentFilterForm"; + +const EditAssignmentFilter = () => { + const router = useRouter(); + const { filterId } = router.query; + const [filterIdReady, setFilterIdReady] = useState(false); + const tenantFilter = useSettings().currentTenant; + + const filterInfo = ApiGetCall({ + url: `/api/ListAssignmentFilters?filterId=${filterId}&tenantFilter=${tenantFilter}`, + queryKey: `ListAssignmentFilters-${filterId}`, + waiting: filterIdReady, + }); + + useEffect(() => { + if (filterId) { + setFilterIdReady(true); + filterInfo.refetch(); + } + }, [router.query, filterId, tenantFilter]); + + const formControl = useForm({ + mode: "onChange", + defaultValues: { + tenantFilter: tenantFilter, + assignmentFilterManagementType: "devices", + }, + }); + + useEffect(() => { + if (filterInfo.isSuccess && filterInfo.data) { + const filter = Array.isArray(filterInfo.data) ? filterInfo.data[0] : filterInfo.data; + + if (filter) { + const formValues = { + tenantFilter: tenantFilter, + filterId: filter.id, + displayName: filter.displayName || "", + description: filter.description || "", + platform: filter.platform || "", + rule: filter.rule || "", + assignmentFilterManagementType: filter.assignmentFilterManagementType || "devices", + }; + + formControl.reset(formValues); + } + } + }, [filterInfo.isSuccess, filterInfo.data, tenantFilter]); + + return ( + <> + + + + + + + ); +}; + +EditAssignmentFilter.getLayout = (page) => {page}; + +export default EditAssignmentFilter; diff --git a/src/pages/endpoint/MEM/assignment-filters/index.js b/src/pages/endpoint/MEM/assignment-filters/index.js new file mode 100644 index 000000000000..0a69dcd1460b --- /dev/null +++ b/src/pages/endpoint/MEM/assignment-filters/index.js @@ -0,0 +1,92 @@ +import { Button } from "@mui/material"; +import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import Link from "next/link"; +import { TrashIcon } from "@heroicons/react/24/outline"; +import { FilterAlt, Edit, Add } from "@mui/icons-material"; +import { Stack } from "@mui/system"; +import { useSettings } from "../../../../hooks/use-settings"; + +const Page = () => { + const pageTitle = "Assignment Filters"; + const { currentTenant } = useSettings(); + + const actions = [ + { + label: "Edit Filter", + link: "/endpoint/MEM/assignment-filters/edit?filterId=[id]", + multiPost: false, + icon: , + color: "success", + }, + { + label: "Create template based on filter", + type: "POST", + url: "/api/AddAssignmentFilterTemplate", + icon: , + data: { + displayName: "displayName", + description: "description", + platform: "platform", + rule: "rule", + assignmentFilterManagementType: "assignmentFilterManagementType", + }, + confirmText: "Are you sure you want to create a template based on this filter?", + multiPost: false, + }, + { + label: "Delete Filter", + type: "POST", + url: "/api/ExecAssignmentFilter", + icon: , + data: { + ID: "id", + Action: "Delete", + }, + confirmText: "Are you sure you want to delete this assignment filter?", + multiPost: false, + }, + ]; + + const offCanvas = { + extendedInfoFields: [ + "displayName", + "description", + "id", + "platform", + "rule", + "assignmentFilterManagementType", + "createdDateTime", + "lastModifiedDateTime", + ], + actions: actions, + }; + + return ( + + + + } + apiUrl="/api/ListAssignmentFilters" + queryKey={`assignment-filters-${currentTenant}`} + actions={actions} + offCanvas={offCanvas} + simpleColumns={[ + "displayName", + "description", + "platform", + "assignmentFilterManagementType", + "rule", + ]} + /> + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; From 481eaff761a598f5371ddffac5ce1e02fd68a323 Mon Sep 17 00:00:00 2001 From: Peter Vive <95594418+PeterVive@users.noreply.github.com> Date: Sat, 4 Oct 2025 14:29:48 +0200 Subject: [PATCH 088/112] Implemented #4759 - retains full backwards compatibility. --- src/data/standards.json | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/data/standards.json b/src/data/standards.json index 4c6be119947a..c5d83df48acf 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -2291,6 +2291,13 @@ ], "helpText": "This creates a Safe Links policy that automatically scans, tracks, and and enables safe links for Email, Office, and Teams for both external and internal senders", "addedComponent": [ + { + "type": "textField", + "name": "standards.SafeLinksPolicy.name", + "label": "Policy Name", + "required": true, + "defaultValue": "CIPP Default SafeLinks Policy" + }, { "type": "switch", "label": "AllowClickThrough", @@ -2338,6 +2345,13 @@ ], "helpText": "This creates a Anti-Phishing policy that automatically enables Mailbox Intelligence and spoofing, optional switches for Mail tips.", "addedComponent": [ + { + "type": "textField", + "name": "standards.AntiPhishPolicy.name", + "label": "Policy Name", + "required": true, + "defaultValue": "CIPP Default Anti-Phishing Policy" + }, { "type": "number", "label": "Phishing email threshold. (Default 1)", @@ -2548,6 +2562,13 @@ ], "helpText": "This creates a Safe Attachment policy", "addedComponent": [ + { + "type": "textField", + "name": "standards.SafeAttachmentPolicy.name", + "label": "Policy Name", + "required": true, + "defaultValue": "CIPP Default Safe Attachment Policy" + }, { "type": "select", "multiple": false, @@ -2692,6 +2713,13 @@ ], "helpText": "This creates a Malware filter policy that enables the default File filter and Zero-hour auto purge for malware.", "addedComponent": [ + { + "type": "textField", + "name": "standards.MalwareFilterPolicy.name", + "label": "Policy Name", + "required": true, + "defaultValue": "CIPP Default Malware Policy" + }, { "type": "select", "multiple": false, @@ -2813,6 +2841,13 @@ "helpText": "This standard creates a Spam filter policy similar to the default strict policy.", "docsDescription": "This standard creates a Spam filter policy similar to the default strict policy, the following settings are configured to on by default: IncreaseScoreWithNumericIps, IncreaseScoreWithRedirectToOtherPort, MarkAsSpamEmptyMessages, MarkAsSpamJavaScriptInHtml, MarkAsSpamSpfRecordHardFail, MarkAsSpamFromAddressAuthFail, MarkAsSpamNdrBackscatter, MarkAsSpamBulkMail, InlineSafetyTipsEnabled, PhishZapEnabled, SpamZapEnabled", "addedComponent": [ + { + "type": "textField", + "name": "standards.SpamFilterPolicy.name", + "label": "Policy Name", + "required": true, + "defaultValue": "CIPP Default Spam Filter Policy" + }, { "type": "number", "label": "Bulk email threshold (Default 7)", From 0368ba6b248fdc64c3bf0f2eba95f35fcb4f2607 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Sat, 4 Oct 2025 09:48:07 -0400 Subject: [PATCH 089/112] fix rerender loop cleanup debug logs --- .../CippComponents/CippCustomVariables.jsx | 2 - .../CippVariableAutocomplete.jsx | 144 ++++++++++++++++-- src/hooks/useCustomVariables.js | 137 ----------------- 3 files changed, 133 insertions(+), 150 deletions(-) delete mode 100644 src/hooks/useCustomVariables.js diff --git a/src/components/CippComponents/CippCustomVariables.jsx b/src/components/CippComponents/CippCustomVariables.jsx index c3e79c60acd6..b408deaae8ea 100644 --- a/src/components/CippComponents/CippCustomVariables.jsx +++ b/src/components/CippComponents/CippCustomVariables.jsx @@ -12,8 +12,6 @@ const CippCustomVariables = ({ id }) => { // Simple cache invalidation using React Query wildcard support const allRelatedKeys = ["CustomVariables*"]; - console.log("CippCustomVariables component - allRelatedKeys:", allRelatedKeys, "id:", id); - const updateCustomVariablesApi = ApiPostCall({ urlFromData: true, relatedQueryKeys: allRelatedKeys, diff --git a/src/components/CippComponents/CippVariableAutocomplete.jsx b/src/components/CippComponents/CippVariableAutocomplete.jsx index f09bea204a3f..05e7faf1abb1 100644 --- a/src/components/CippComponents/CippVariableAutocomplete.jsx +++ b/src/components/CippComponents/CippVariableAutocomplete.jsx @@ -1,6 +1,8 @@ -import React, { useState, useEffect, useRef } from "react"; -import { Paper, Typography, Box, Chip, Popper, ListItem, useTheme } from "@mui/material"; -import { useCustomVariables } from "/src/hooks/useCustomVariables"; +import React, { useState, useEffect, useRef, useMemo } from "react"; +import { Paper, Typography, Box, Chip, Popper, ListItem, useTheme, CircularProgress } from "@mui/material"; +import { ApiGetCall } from "/src/api/ApiCall"; +import { useSettings } from "/src/hooks/use-settings.js"; +import { getCippError } from "/src/utils/get-cipp-error"; /** * Autocomplete component specifically for custom variables @@ -18,12 +20,104 @@ export const CippVariableAutocomplete = React.memo( position = { top: 0, left: 0 }, // Cursor position for floating box }) => { const theme = useTheme(); - const { variables, isLoading, groupedVariables } = useCustomVariables( - tenantFilter, - includeSystemVariables - ); + const settings = useSettings(); + + // State management similar to CippAutocomplete + const [variables, setVariables] = useState([]); + const [getRequestInfo, setGetRequestInfo] = useState({ url: "", waiting: false, queryKey: "" }); const [filteredVariables, setFilteredVariables] = useState([]); + // Get current tenant like CippAutocomplete does + const currentTenant = tenantFilter || settings.currentTenant; + + // API call using the same pattern as CippAutocomplete + const actionGetRequest = ApiGetCall({ + ...getRequestInfo, + }); + + // Setup API request when component mounts or tenant changes + useEffect(() => { + if (open) { + // Normalize tenant filter + const normalizedTenantFilter = currentTenant === "AllTenants" ? null : currentTenant; + + // Build API URL + let apiUrl = "/api/ListCustomVariables"; + const params = new URLSearchParams(); + + if (normalizedTenantFilter) { + params.append("tenantFilter", normalizedTenantFilter); + } + + if (!includeSystemVariables) { + params.append("includeSystem", "false"); + } + + if (params.toString()) { + apiUrl += `?${params.toString()}`; + } + + // Generate query key + const queryKey = `CustomVariables-${normalizedTenantFilter || "global"}-${ + includeSystemVariables ? "withSystem" : "noSystem" + }`; + + setGetRequestInfo({ + url: apiUrl, + waiting: true, + queryKey: queryKey, + staleTime: Infinity, // Never goes stale like in the updated hook + refetchOnMount: false, + refetchOnReconnect: false, + refetchOnWindowFocus: false, + }); + } + }, [open, currentTenant, includeSystemVariables]); + + // Process API response like CippAutocomplete does + useEffect(() => { + if (actionGetRequest.isSuccess && actionGetRequest.data?.Results) { + const processedVariables = actionGetRequest.data.Results.map((variable) => ({ + // Core properties + name: variable.Name, + variable: variable.Variable, + label: variable.Variable, // What shows in autocomplete + value: variable.Variable, // What gets inserted + + // Metadata for display and filtering + description: variable.Description, + type: variable.Type, // 'reserved' or 'custom' + category: variable.Category, // 'system', 'tenant', 'partner', 'cipp', 'global', 'tenant-custom' + + // Custom variable specific + ...(variable.Type === "custom" && { + customValue: variable.Value, + scope: variable.Scope, + }), + + // For grouping in autocomplete + group: + variable.Type === "reserved" + ? `Reserved (${variable.Category})` + : variable.category === "global" + ? "Global Custom Variables" + : "Tenant Custom Variables", + })); + + setVariables(processedVariables); + } + + if (actionGetRequest.isError) { + setVariables([{ + label: getCippError(actionGetRequest.error), + value: "error", + name: "error", + variable: "error", + description: "Error loading variables" + }]); + } + }, [actionGetRequest.isSuccess, actionGetRequest.isError, actionGetRequest.data]); + // Filter variables based on search query useEffect(() => { if (!searchQuery) { @@ -34,8 +128,8 @@ export const CippVariableAutocomplete = React.memo( const lowerQuery = searchQuery.toLowerCase(); const filtered = variables.filter( (variable) => - variable.name.toLowerCase().includes(lowerQuery) || - variable.description.toLowerCase().includes(lowerQuery) + variable.name?.toLowerCase().includes(lowerQuery) || + variable.description?.toLowerCase().includes(lowerQuery) ); setFilteredVariables(filtered); }, [searchQuery, variables]); @@ -51,8 +145,36 @@ export const CippVariableAutocomplete = React.memo( return null; } - if (isLoading) { - return null; + // Show loading state like CippAutocomplete + if (actionGetRequest.isLoading && (!variables || variables.length === 0)) { + return ( + + + + + + Loading variables... + + + + + ); } if (!variables || variables.length === 0) { diff --git a/src/hooks/useCustomVariables.js b/src/hooks/useCustomVariables.js deleted file mode 100644 index 76c2c5f54fbf..000000000000 --- a/src/hooks/useCustomVariables.js +++ /dev/null @@ -1,137 +0,0 @@ -import { useMemo } from "react"; -import { ApiGetCall } from "/src/api/ApiCall"; - -/** - * Hook to fetch and format custom variables for autocomplete - * @param {string} tenantFilter - Optional tenant filter for tenant-specific variables - * @param {boolean} includeSystemVariables - Whether to include system variables - * @returns {object} { variables, isLoading, error } - */ -export const useCustomVariables = (tenantFilter = null, includeSystemVariables = false) => { - // Memoize normalized tenant filter and query parameters - const normalizedParams = useMemo(() => { - // Normalize tenantFilter - const normalizedTenantFilter = tenantFilter === "AllTenants" ? null : tenantFilter; - - // Generate query key - const queryKey = `CustomVariables-${normalizedTenantFilter || "global"}-${ - includeSystemVariables ? "withSystem" : "noSystem" - }`; - - // Build API URL - let apiUrl = "/api/ListCustomVariables"; - const params = new URLSearchParams(); - - if (normalizedTenantFilter) { - params.append("tenantFilter", normalizedTenantFilter); - } - - if (!includeSystemVariables) { - params.append("includeSystem", "false"); - } - - if (params.toString()) { - apiUrl += `?${params.toString()}`; - } - - return { - queryKey, - apiUrl, - relatedQueryKeys: ["CustomVariables*"], - }; - }, [tenantFilter, includeSystemVariables]); - - // Fetch variables from API - const apiCall = ApiGetCall({ - url: normalizedParams.apiUrl, - queryKey: normalizedParams.queryKey, - relatedQueryKeys: normalizedParams.relatedQueryKeys, - refetchOnMount: false, - refetchOnReconnect: false, - staleTime: 5 * 60 * 1000, // 5 minutes - variables don't change often - }); - - // Format variables for autocomplete component - const variables = useMemo(() => { - if (!apiCall.isSuccess || !apiCall.data?.Results) { - return []; - } - - return apiCall.data.Results.map((variable) => ({ - // Core properties - name: variable.Name, - variable: variable.Variable, - label: variable.Variable, // What shows in autocomplete - value: variable.Variable, // What gets inserted - - // Metadata for display and filtering - description: variable.Description, - type: variable.Type, // 'reserved' or 'custom' - category: variable.Category, // 'system', 'tenant', 'partner', 'cipp', 'global', 'tenant-custom' - - // Custom variable specific - ...(variable.Type === "custom" && { - customValue: variable.Value, - scope: variable.Scope, - }), - - // For grouping in autocomplete - group: - variable.Type === "reserved" - ? `Reserved (${variable.Category})` - : variable.Category === "global" - ? "Global Custom Variables" - : "Tenant Custom Variables", - })); - }, [apiCall.isSuccess, apiCall.data]); - - // Group variables by category for better UX - const groupedVariables = useMemo(() => { - const groups = {}; - variables.forEach((variable) => { - const groupName = variable.group; - if (!groups[groupName]) { - groups[groupName] = []; - } - groups[groupName].push(variable); - }); - return groups; - }, [variables]); - - // Filter functions for different use cases - const filterVariables = useMemo( - () => ({ - // Get only reserved variables - reserved: () => variables.filter((v) => v.type === "reserved"), - - // Get only custom variables - custom: () => variables.filter((v) => v.type === "custom"), - - // Get variables by category - byCategory: (category) => variables.filter((v) => v.category === category), - - // Search variables by name or description - search: (query) => { - const lowerQuery = query.toLowerCase(); - return variables.filter( - (v) => - v.name.toLowerCase().includes(lowerQuery) || - v.description.toLowerCase().includes(lowerQuery) - ); - }, - }), - [variables] - ); - - return { - variables, - groupedVariables, - filterVariables, - isLoading: apiCall.isLoading, - isSuccess: apiCall.isSuccess, - isError: apiCall.isError, - error: apiCall.error, - metadata: apiCall.data?.Metadata, - relatedQueryKeys: normalizedParams.relatedQueryKeys, // Expose related query keys for other components - }; -}; From 054e2b92d4b40ba5a06bf68d23952e5a619b2d94 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Sat, 4 Oct 2025 09:56:05 -0400 Subject: [PATCH 090/112] keyboard shortcuts for autocomplete --- .../CippVariableAutocomplete.jsx | 89 ++++++++++++++++--- 1 file changed, 77 insertions(+), 12 deletions(-) diff --git a/src/components/CippComponents/CippVariableAutocomplete.jsx b/src/components/CippComponents/CippVariableAutocomplete.jsx index 05e7faf1abb1..32735ad2b481 100644 --- a/src/components/CippComponents/CippVariableAutocomplete.jsx +++ b/src/components/CippComponents/CippVariableAutocomplete.jsx @@ -1,5 +1,14 @@ -import React, { useState, useEffect, useRef, useMemo } from "react"; -import { Paper, Typography, Box, Chip, Popper, ListItem, useTheme, CircularProgress } from "@mui/material"; +import React, { useState, useEffect, useRef, useMemo, useCallback } from "react"; +import { + Paper, + Typography, + Box, + Chip, + Popper, + ListItem, + useTheme, + CircularProgress, +} from "@mui/material"; import { ApiGetCall } from "/src/api/ApiCall"; import { useSettings } from "/src/hooks/use-settings.js"; import { getCippError } from "/src/utils/get-cipp-error"; @@ -21,11 +30,12 @@ export const CippVariableAutocomplete = React.memo( }) => { const theme = useTheme(); const settings = useSettings(); - + // State management similar to CippAutocomplete const [variables, setVariables] = useState([]); const [getRequestInfo, setGetRequestInfo] = useState({ url: "", waiting: false, queryKey: "" }); const [filteredVariables, setFilteredVariables] = useState([]); + const [selectedIndex, setSelectedIndex] = useState(0); // For keyboard navigation // Get current tenant like CippAutocomplete does const currentTenant = tenantFilter || settings.currentTenant; @@ -40,7 +50,7 @@ export const CippVariableAutocomplete = React.memo( if (open) { // Normalize tenant filter const normalizedTenantFilter = currentTenant === "AllTenants" ? null : currentTenant; - + // Build API URL let apiUrl = "/api/ListCustomVariables"; const params = new URLSearchParams(); @@ -103,18 +113,20 @@ export const CippVariableAutocomplete = React.memo( ? "Global Custom Variables" : "Tenant Custom Variables", })); - + setVariables(processedVariables); } if (actionGetRequest.isError) { - setVariables([{ - label: getCippError(actionGetRequest.error), - value: "error", - name: "error", - variable: "error", - description: "Error loading variables" - }]); + setVariables([ + { + label: getCippError(actionGetRequest.error), + value: "error", + name: "error", + variable: "error", + description: "Error loading variables", + }, + ]); } }, [actionGetRequest.isSuccess, actionGetRequest.isError, actionGetRequest.data]); @@ -122,6 +134,7 @@ export const CippVariableAutocomplete = React.memo( useEffect(() => { if (!searchQuery) { setFilteredVariables(variables); + setSelectedIndex(0); // Reset selection when filtering return; } @@ -132,6 +145,7 @@ export const CippVariableAutocomplete = React.memo( variable.description?.toLowerCase().includes(lowerQuery) ); setFilteredVariables(filtered); + setSelectedIndex(0); // Reset selection when filtering }, [searchQuery, variables]); const handleSelect = (event, value) => { @@ -141,6 +155,45 @@ export const CippVariableAutocomplete = React.memo( onClose(); }; + // Keyboard navigation handlers + const handleKeyDown = useCallback((event) => { + if (!open || filteredVariables.length === 0) return; + + switch (event.key) { + case 'ArrowDown': + event.preventDefault(); + setSelectedIndex(prev => + prev < filteredVariables.length - 1 ? prev + 1 : 0 + ); + break; + case 'ArrowUp': + event.preventDefault(); + setSelectedIndex(prev => + prev > 0 ? prev - 1 : filteredVariables.length - 1 + ); + break; + case 'Tab': + case 'Enter': + event.preventDefault(); + if (filteredVariables[selectedIndex]) { + handleSelect(event, filteredVariables[selectedIndex]); + } + break; + case 'Escape': + event.preventDefault(); + onClose(); + break; + } + }, [open, filteredVariables, selectedIndex, onClose]); + + // Set up keyboard event listeners + useEffect(() => { + if (open) { + document.addEventListener('keydown', handleKeyDown); + return () => document.removeEventListener('keydown', handleKeyDown); + } + }, [open, handleKeyDown]); + if (!open) { return null; } @@ -211,6 +264,12 @@ export const CippVariableAutocomplete = React.memo( {filteredVariables.map((variable, index) => ( { + // Scroll selected item into view + if (el) { + el.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); + } + } : null} onClick={(e) => { e.stopPropagation(); e.preventDefault(); @@ -223,6 +282,12 @@ export const CippVariableAutocomplete = React.memo( py: 1, px: 2, borderBottom: `1px solid ${theme.palette.divider}`, + backgroundColor: index === selectedIndex + ? theme.palette.action.selected + : 'transparent', + borderLeft: index === selectedIndex + ? `3px solid ${theme.palette.primary.main}` + : '3px solid transparent', "&:hover": { backgroundColor: theme.palette.action.hover, }, From aeb9f72f1bf3bda0a25a1807b2ad7b0e373bd70c Mon Sep 17 00:00:00 2001 From: John Duprey Date: Sat, 4 Oct 2025 10:05:56 -0400 Subject: [PATCH 091/112] autocomplete ux tweaks retain focus on multiple mode and allow tab selection --- .../CippComponents/CippAutocomplete.jsx | 27 ++++++ .../CippVariableAutocomplete.jsx | 87 ++++++++++--------- 2 files changed, 72 insertions(+), 42 deletions(-) diff --git a/src/components/CippComponents/CippAutocomplete.jsx b/src/components/CippComponents/CippAutocomplete.jsx index 2e0df7f25c1f..9ed0e358d594 100644 --- a/src/components/CippComponents/CippAutocomplete.jsx +++ b/src/components/CippComponents/CippAutocomplete.jsx @@ -79,6 +79,7 @@ export const CippAutoComplete = (props) => { const [usedOptions, setUsedOptions] = useState(options); const [getRequestInfo, setGetRequestInfo] = useState({ url: "", waiting: false, queryKey: "" }); const hasPreselectedRef = useRef(false); + const autocompleteRef = useRef(null); // Ref for focusing input after selection const filter = createFilterOptions({ stringify: (option) => JSON.stringify(option), }); @@ -276,6 +277,7 @@ export const CippAutoComplete = (props) => { return ( { if (onChange) { onChange(newValue, newValue?.addedFields); } + + // In multiple mode, refocus the input after selection to allow continuous adding + if (multiple && newValue && autocompleteRef.current) { + // Use setTimeout to ensure the selection is processed first + setTimeout(() => { + const input = autocompleteRef.current?.querySelector("input"); + if (input) { + input.focus(); + } + }, 0); + } }} options={memoizedOptions} getOptionLabel={useCallback( @@ -380,6 +393,20 @@ export const CippAutoComplete = (props) => { }, [api] )} + onKeyDown={(event) => { + // Handle Tab key to select highlighted option + if (event.key === "Tab" && !event.shiftKey) { + // Check if there's a highlighted option + const listbox = document.querySelector('[role="listbox"]'); + const highlightedOption = listbox?.querySelector('[data-focus="true"], .Mui-focused'); + + if (highlightedOption && listbox?.style.display !== "none") { + event.preventDefault(); + // Trigger a click on the highlighted option + highlightedOption.click(); + } + } + }} sx={sx} renderInput={(params) => ( diff --git a/src/components/CippComponents/CippVariableAutocomplete.jsx b/src/components/CippComponents/CippVariableAutocomplete.jsx index 32735ad2b481..9910e9771afd 100644 --- a/src/components/CippComponents/CippVariableAutocomplete.jsx +++ b/src/components/CippComponents/CippVariableAutocomplete.jsx @@ -156,41 +156,40 @@ export const CippVariableAutocomplete = React.memo( }; // Keyboard navigation handlers - const handleKeyDown = useCallback((event) => { - if (!open || filteredVariables.length === 0) return; + const handleKeyDown = useCallback( + (event) => { + if (!open || filteredVariables.length === 0) return; - switch (event.key) { - case 'ArrowDown': - event.preventDefault(); - setSelectedIndex(prev => - prev < filteredVariables.length - 1 ? prev + 1 : 0 - ); - break; - case 'ArrowUp': - event.preventDefault(); - setSelectedIndex(prev => - prev > 0 ? prev - 1 : filteredVariables.length - 1 - ); - break; - case 'Tab': - case 'Enter': - event.preventDefault(); - if (filteredVariables[selectedIndex]) { - handleSelect(event, filteredVariables[selectedIndex]); - } - break; - case 'Escape': - event.preventDefault(); - onClose(); - break; - } - }, [open, filteredVariables, selectedIndex, onClose]); + switch (event.key) { + case "ArrowDown": + event.preventDefault(); + setSelectedIndex((prev) => (prev < filteredVariables.length - 1 ? prev + 1 : 0)); + break; + case "ArrowUp": + event.preventDefault(); + setSelectedIndex((prev) => (prev > 0 ? prev - 1 : filteredVariables.length - 1)); + break; + case "Tab": + case "Enter": + event.preventDefault(); + if (filteredVariables[selectedIndex]) { + handleSelect(event, filteredVariables[selectedIndex]); + } + break; + case "Escape": + event.preventDefault(); + onClose(); + break; + } + }, + [open, filteredVariables, selectedIndex, onClose] + ); // Set up keyboard event listeners useEffect(() => { if (open) { - document.addEventListener('keydown', handleKeyDown); - return () => document.removeEventListener('keydown', handleKeyDown); + document.addEventListener("keydown", handleKeyDown); + return () => document.removeEventListener("keydown", handleKeyDown); } }, [open, handleKeyDown]); @@ -264,12 +263,16 @@ export const CippVariableAutocomplete = React.memo( {filteredVariables.map((variable, index) => ( { - // Scroll selected item into view - if (el) { - el.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); - } - } : null} + ref={ + index === selectedIndex + ? (el) => { + // Scroll selected item into view + if (el) { + el.scrollIntoView({ block: "nearest", behavior: "smooth" }); + } + } + : null + } onClick={(e) => { e.stopPropagation(); e.preventDefault(); @@ -282,12 +285,12 @@ export const CippVariableAutocomplete = React.memo( py: 1, px: 2, borderBottom: `1px solid ${theme.palette.divider}`, - backgroundColor: index === selectedIndex - ? theme.palette.action.selected - : 'transparent', - borderLeft: index === selectedIndex - ? `3px solid ${theme.palette.primary.main}` - : '3px solid transparent', + backgroundColor: + index === selectedIndex ? theme.palette.action.selected : "transparent", + borderLeft: + index === selectedIndex + ? `3px solid ${theme.palette.primary.main}` + : "3px solid transparent", "&:hover": { backgroundColor: theme.palette.action.hover, }, From 52fa53ff0b91d5679830f1e8adef98e5fd8977c2 Mon Sep 17 00:00:00 2001 From: Peter Vive <95594418+PeterVive@users.noreply.github.com> Date: Sat, 4 Oct 2025 18:16:27 +0200 Subject: [PATCH 092/112] Implement Office custom XML support during deployment - FR #469 --- .../CippApplicationDeployDrawer.jsx | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/components/CippComponents/CippApplicationDeployDrawer.jsx b/src/components/CippComponents/CippApplicationDeployDrawer.jsx index 9fc0e50c4ee1..ac831f7ca3b3 100644 --- a/src/components/CippComponents/CippApplicationDeployDrawer.jsx +++ b/src/components/CippComponents/CippApplicationDeployDrawer.jsx @@ -751,6 +751,44 @@ export const CippApplicationDeployDrawer = ({ defaultValue={true} />
    + + + + + + + + Provide a custom Office Configuration XML. When using custom XML, all other + Office configuration options above will be ignored. See{" "} + + Office Customization Tool + {" "} + to generate XML. + + + {/* Assign To Options */} From d1214a5cf07ff72e83c7f2e74cd529feb282080b Mon Sep 17 00:00:00 2001 From: Peter Vive <95594418+PeterVive@users.noreply.github.com> Date: Sat, 4 Oct 2025 19:05:12 +0200 Subject: [PATCH 093/112] Implement custom chocolatey arguments #4683 --- .../CippComponents/CippApplicationDeployDrawer.jsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/components/CippComponents/CippApplicationDeployDrawer.jsx b/src/components/CippComponents/CippApplicationDeployDrawer.jsx index 9fc0e50c4ee1..0ed156484378 100644 --- a/src/components/CippComponents/CippApplicationDeployDrawer.jsx +++ b/src/components/CippComponents/CippApplicationDeployDrawer.jsx @@ -599,6 +599,14 @@ export const CippApplicationDeployDrawer = ({ formControl={formControl} /> + + + {/* Install Options */} From c239168fef426c331b086c6955253cbf2fc9f451 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Mon, 6 Oct 2025 15:25:19 -0400 Subject: [PATCH 094/112] fix pathing for tenant management --- .../tenant/administration/tenants/index.js | 4 +- .../applied-standards.js} | 51 +++++++++++++------ .../configuration-backup.js | 0 .../manage-drift/index.js => manage/drift.js} | 2 +- .../driftManagementActions.js | 0 .../edit-tenant.js => manage/edit.js} | 0 .../manage-drift => manage}/history.js | 2 +- .../policies-deployed.js | 21 +++++--- .../recover-policies.js | 0 src/pages/tenant/manage/tabOptions.json | 26 ++++++++++ .../list-standards/classic-standards/index.js | 2 +- .../list-standards/drift-alignment/index.js | 4 +- .../tenant/standards/list-standards/index.js | 4 +- .../standards/manage-drift/tabOptions.json | 26 ---------- src/pages/tenant/standards/template.jsx | 2 +- .../standards/tenant-alignment/index.js | 2 +- 16 files changed, 85 insertions(+), 61 deletions(-) rename src/pages/tenant/{standards/manage-drift/compare.js => manage/applied-standards.js} (97%) rename src/pages/tenant/{standards/manage-drift => manage}/configuration-backup.js (100%) rename src/pages/tenant/{standards/manage-drift/index.js => manage/drift.js} (99%) rename src/pages/tenant/{standards/manage-drift => manage}/driftManagementActions.js (100%) rename src/pages/tenant/{standards/manage-drift/edit-tenant.js => manage/edit.js} (100%) rename src/pages/tenant/{standards/manage-drift => manage}/history.js (99%) rename src/pages/tenant/{standards/manage-drift => manage}/policies-deployed.js (96%) rename src/pages/tenant/{standards/manage-drift => manage}/recover-policies.js (100%) create mode 100644 src/pages/tenant/manage/tabOptions.json delete mode 100644 src/pages/tenant/standards/manage-drift/tabOptions.json diff --git a/src/pages/tenant/administration/tenants/index.js b/src/pages/tenant/administration/tenants/index.js index c21587c72044..129b26fb1db2 100644 --- a/src/pages/tenant/administration/tenants/index.js +++ b/src/pages/tenant/administration/tenants/index.js @@ -26,12 +26,12 @@ const Page = () => { const actions = [ { label: "Edit Tenant", - link: "/tenant/standards/manage-drift/edit-tenant?tenantFilter=[defaultDomainName]", + link: "/tenant/manage/edit?tenantFilter=[defaultDomainName]", icon: , }, { label: "Configure Backup", - link: "/tenant/standards/manage-drift/configuration-backup?tenantFilter=[defaultDomainName]", + link: "/tenant/manage/configuration-backup?tenantFilter=[defaultDomainName]", icon: , }, { diff --git a/src/pages/tenant/standards/manage-drift/compare.js b/src/pages/tenant/manage/applied-standards.js similarity index 97% rename from src/pages/tenant/standards/manage-drift/compare.js rename to src/pages/tenant/manage/applied-standards.js index 1dc45c1a36b1..f54db6eaab63 100644 --- a/src/pages/tenant/standards/manage-drift/compare.js +++ b/src/pages/tenant/manage/applied-standards.js @@ -1,5 +1,5 @@ import React, { useState, useEffect, useMemo } from "react"; -import { CippAutoComplete } from "../../../../components/CippComponents/CippAutocomplete"; +import { CippAutoComplete } from "../../../components/CippComponents/CippAutocomplete"; import { Button, Card, @@ -30,13 +30,13 @@ import { Policy, } from "@mui/icons-material"; import standards from "/src/data/standards.json"; -import { CippApiDialog } from "../../../../components/CippComponents/CippApiDialog"; +import { CippApiDialog } from "../../../components/CippComponents/CippApiDialog"; import { SvgIcon } from "@mui/material"; import { useForm } from "react-hook-form"; -import { useSettings } from "../../../../hooks/use-settings"; -import { ApiGetCall, ApiPostCall } from "../../../../api/ApiCall"; +import { useSettings } from "../../../hooks/use-settings"; +import { ApiGetCall, ApiPostCall } from "../../../api/ApiCall"; import { useRouter } from "next/router"; -import { useDialog } from "../../../../hooks/use-dialog"; +import { useDialog } from "../../../hooks/use-dialog"; import { Grid } from "@mui/system"; import DOMPurify from "dompurify"; import { ClockIcon } from "@heroicons/react/24/outline"; @@ -172,7 +172,7 @@ const Page = () => { standardId, standardName: `Intune Template: ${ expandedTemplate.displayName || expandedTemplate.name || templateId - } (via ${templateItem['TemplateList-Tags'].value})`, + } (via ${templateItem["TemplateList-Tags"].value})`, currentTenantValue: standardObject !== undefined ? { @@ -316,7 +316,7 @@ const Page = () => { standardId, standardName: `Conditional Access Template: ${ expandedTemplate.displayName || expandedTemplate.name || templateId - } (via ${templateItem['TemplateList-Tags'].value})`, + } (via ${templateItem["TemplateList-Tags"].value})`, currentTenantValue: standardObject !== undefined ? { @@ -619,15 +619,20 @@ const Page = () => { // Simple filter for all templates (no type filtering) const templateOptions = templateDetails.data ? templateDetails.data.map((template) => ({ - label: template.displayName || template.templateName || template.name || `Template ${template.GUID}`, + label: + template.displayName || + template.templateName || + template.name || + `Template ${template.GUID}`, value: template.GUID, })) : []; // Find currently selected template - const selectedTemplateOption = templateId && templateOptions.length - ? templateOptions.find((option) => option.value === templateId) || null - : null; + const selectedTemplateOption = + templateId && templateOptions.length + ? templateOptions.find((option) => option.value === templateId) || null + : null; // Effect to refetch APIs when templateId changes (needed for shallow routing) useEffect(() => { @@ -1050,7 +1055,7 @@ const Page = () => { sx={{ wordBreak: "break-word", overflowWrap: "break-word", - hyphens: "auto" + hyphens: "auto", }} > {standard?.standardName} @@ -1307,7 +1312,8 @@ const Page = () => { // Format the display value let displayValue; if (typeof value === "object" && value !== null) { - displayValue = value?.label || JSON.stringify(value, null, 2); + displayValue = + value?.label || JSON.stringify(value, null, 2); } else if (value === true) { displayValue = "Enabled"; } else if (value === false) { @@ -1317,7 +1323,10 @@ const Page = () => { } return ( - + { whiteSpace: "pre-wrap", flex: 1, minWidth: 0, - fontFamily: typeof value === "object" && value !== null && !value?.label ? "monospace" : "inherit", - fontSize: typeof value === "object" && value !== null && !value?.label ? "0.75rem" : "inherit", + fontFamily: + typeof value === "object" && + value !== null && + !value?.label + ? "monospace" + : "inherit", + fontSize: + typeof value === "object" && + value !== null && + !value?.label + ? "0.75rem" + : "inherit", m: 0, }} > diff --git a/src/pages/tenant/standards/manage-drift/configuration-backup.js b/src/pages/tenant/manage/configuration-backup.js similarity index 100% rename from src/pages/tenant/standards/manage-drift/configuration-backup.js rename to src/pages/tenant/manage/configuration-backup.js diff --git a/src/pages/tenant/standards/manage-drift/index.js b/src/pages/tenant/manage/drift.js similarity index 99% rename from src/pages/tenant/standards/manage-drift/index.js rename to src/pages/tenant/manage/drift.js index 715aa7ee2b76..dd2b59c340be 100644 --- a/src/pages/tenant/standards/manage-drift/index.js +++ b/src/pages/tenant/manage/drift.js @@ -28,7 +28,7 @@ import tabOptions from "./tabOptions.json"; import standardsData from "/src/data/standards.json"; import { createDriftManagementActions } from "./driftManagementActions"; import { ExecutiveReportButton } from "/src/components/ExecutiveReportButton"; -import { CippAutoComplete } from "../../../../components/CippComponents/CippAutocomplete"; +import { CippAutoComplete } from "../../../components/CippComponents/CippAutocomplete"; const ManageDriftPage = () => { const router = useRouter(); diff --git a/src/pages/tenant/standards/manage-drift/driftManagementActions.js b/src/pages/tenant/manage/driftManagementActions.js similarity index 100% rename from src/pages/tenant/standards/manage-drift/driftManagementActions.js rename to src/pages/tenant/manage/driftManagementActions.js diff --git a/src/pages/tenant/standards/manage-drift/edit-tenant.js b/src/pages/tenant/manage/edit.js similarity index 100% rename from src/pages/tenant/standards/manage-drift/edit-tenant.js rename to src/pages/tenant/manage/edit.js diff --git a/src/pages/tenant/standards/manage-drift/history.js b/src/pages/tenant/manage/history.js similarity index 99% rename from src/pages/tenant/standards/manage-drift/history.js rename to src/pages/tenant/manage/history.js index 9dbac59c0708..5a3dd89eba72 100644 --- a/src/pages/tenant/standards/manage-drift/history.js +++ b/src/pages/tenant/manage/history.js @@ -33,7 +33,7 @@ import { ExpandMore, } from "@mui/icons-material"; import tabOptions from "./tabOptions.json"; -import { useSettings } from "../../../../hooks/use-settings"; +import { useSettings } from "../../../hooks/use-settings"; import { createDriftManagementActions } from "./driftManagementActions"; const Page = () => { diff --git a/src/pages/tenant/standards/manage-drift/policies-deployed.js b/src/pages/tenant/manage/policies-deployed.js similarity index 96% rename from src/pages/tenant/standards/manage-drift/policies-deployed.js rename to src/pages/tenant/manage/policies-deployed.js index 15752fb2f2a4..fede3bf0ac15 100644 --- a/src/pages/tenant/standards/manage-drift/policies-deployed.js +++ b/src/pages/tenant/manage/policies-deployed.js @@ -17,8 +17,8 @@ import { CippHead } from "/src/components/CippComponents/CippHead"; import { ApiGetCall } from "/src/api/ApiCall"; import standardsData from "/src/data/standards.json"; import { createDriftManagementActions } from "./driftManagementActions"; -import { useSettings } from "../../../../hooks/use-settings"; -import { CippAutoComplete } from "../../../../components/CippComponents/CippAutocomplete"; +import { useSettings } from "../../../hooks/use-settings"; +import { CippAutoComplete } from "../../../components/CippComponents/CippAutocomplete"; import { useEffect } from "react"; const PoliciesDeployedPage = () => { @@ -73,7 +73,7 @@ const PoliciesDeployedPage = () => { return "Deployed"; } else { // Check if there's drift data for this standard to get the deviation status - const driftData = driftApi.data || []; + const driftData = Array.isArray(driftApi.data) ? driftApi.data : []; // For templates, we need to match against the full template path let searchKeys = [standardKey, `standards.${standardKey}`]; @@ -107,7 +107,7 @@ const PoliciesDeployedPage = () => { // Helper function to get display name from drift data const getDisplayNameFromDrift = (standardKey, templateValue = null, templateType = null) => { - const driftData = driftApi.data || []; + const driftData = Array.isArray(driftApi.data) ? driftApi.data : []; // For templates, we need to match against the full template path let searchKeys = [standardKey, `standards.${standardKey}`]; @@ -328,15 +328,20 @@ const PoliciesDeployedPage = () => { // Simple filter for all templates (no type filtering) const templateOptions = standardsApi.data ? standardsApi.data.map((template) => ({ - label: template.displayName || template.templateName || template.name || `Template ${template.GUID}`, + label: + template.displayName || + template.templateName || + template.name || + `Template ${template.GUID}`, value: template.GUID, })) : []; // Find currently selected template - const selectedTemplateOption = templateId && templateOptions.length - ? templateOptions.find((option) => option.value === templateId) || null - : null; + const selectedTemplateOption = + templateId && templateOptions.length + ? templateOptions.find((option) => option.value === templateId) || null + : null; // Effect to refetch APIs when templateId changes (needed for shallow routing) useEffect(() => { diff --git a/src/pages/tenant/standards/manage-drift/recover-policies.js b/src/pages/tenant/manage/recover-policies.js similarity index 100% rename from src/pages/tenant/standards/manage-drift/recover-policies.js rename to src/pages/tenant/manage/recover-policies.js diff --git a/src/pages/tenant/manage/tabOptions.json b/src/pages/tenant/manage/tabOptions.json new file mode 100644 index 000000000000..681c61cad32e --- /dev/null +++ b/src/pages/tenant/manage/tabOptions.json @@ -0,0 +1,26 @@ +[ + { + "label": "Edit Tenant", + "path": "/tenant/manage/edit" + }, + { + "label": "Manage Drift", + "path": "/tenant/manage/drift" + }, + { + "label": "Configuration Backup", + "path": "/tenant/manage/configuration-backup" + }, + { + "label": "Applied Standards Report", + "path": "/tenant/manage/applied-standards" + }, + { + "label": "Policies and Settings Deployed", + "path": "/tenant/manage/policies-deployed" + }, + { + "label": "History", + "path": "/tenant/manage/history" + } +] \ No newline at end of file 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 ed718b46caab..60681e160b09 100644 --- a/src/pages/tenant/standards/list-standards/classic-standards/index.js +++ b/src/pages/tenant/standards/list-standards/classic-standards/index.js @@ -28,7 +28,7 @@ const Page = () => { const actions = [ { label: "View Tenant Report", - link: "/tenant/standards/manage-drift/compare?templateId=[GUID]", + link: "/tenant/manage/applied-standards/?tenantFilter=[tenantFilter]&templateId=[standardId]", icon: , color: "info", target: "_self", diff --git a/src/pages/tenant/standards/list-standards/drift-alignment/index.js b/src/pages/tenant/standards/list-standards/drift-alignment/index.js index 561a94daaeb6..55c408abf622 100644 --- a/src/pages/tenant/standards/list-standards/drift-alignment/index.js +++ b/src/pages/tenant/standards/list-standards/drift-alignment/index.js @@ -10,7 +10,7 @@ const Page = () => { const actions = [ { label: "View Tenant Report", - link: "/tenant/standards/compare?tenantFilter=[tenantFilter]&templateId=[standardId]", + link: "/tenant/manage/applied-standards/?tenantFilter=[tenantFilter]&templateId=[standardId]", icon: , color: "info", target: "_self", @@ -41,4 +41,4 @@ Page.getLayout = (page) => ( ); -export default Page; \ No newline at end of file +export default Page; diff --git a/src/pages/tenant/standards/list-standards/index.js b/src/pages/tenant/standards/list-standards/index.js index c4c95550bd02..28ad3abd2f4f 100644 --- a/src/pages/tenant/standards/list-standards/index.js +++ b/src/pages/tenant/standards/list-standards/index.js @@ -13,14 +13,14 @@ const Page = () => { const actions = [ { label: "View Tenant Report", - link: "/tenant/standards/manage-drift/compare?tenantFilter=[tenantFilter]&templateId=[standardId]", + link: "/tenant/manage/applied-standards/?tenantFilter=[tenantFilter]&templateId=[standardId]", icon: , color: "info", target: "_self", }, { label: "Manage Drift", - link: "/tenant/standards/manage-drift?templateId=[standardId]&tenantFilter=[tenantFilter]", + link: "/tenant/manage/drift?templateId=[standardId]&tenantFilter=[tenantFilter]", icon: , color: "info", target: "_self", diff --git a/src/pages/tenant/standards/manage-drift/tabOptions.json b/src/pages/tenant/standards/manage-drift/tabOptions.json deleted file mode 100644 index 53417621e322..000000000000 --- a/src/pages/tenant/standards/manage-drift/tabOptions.json +++ /dev/null @@ -1,26 +0,0 @@ -[ - { - "label": "Edit Tenant", - "path": "/tenant/standards/manage-drift/edit-tenant" - }, - { - "label": "Configuration Backup", - "path": "/tenant/standards/manage-drift/configuration-backup" - }, - { - "label": "Manage Drift", - "path": "/tenant/standards/manage-drift" - }, - { - "label": "Applied Standards Report", - "path": "/tenant/standards/manage-drift/compare" - }, - { - "label": "Policies and Settings Deployed", - "path": "/tenant/standards/manage-drift/policies-deployed" - }, - { - "label": "History", - "path": "/tenant/standards/manage-drift/history" - } -] diff --git a/src/pages/tenant/standards/template.jsx b/src/pages/tenant/standards/template.jsx index 78f0bf71b428..357b5d7edbab 100644 --- a/src/pages/tenant/standards/template.jsx +++ b/src/pages/tenant/standards/template.jsx @@ -16,7 +16,7 @@ import { ArrowLeftIcon } from "@mui/x-date-pickers"; import { useDialog } from "../../../hooks/use-dialog"; import { ApiGetCall } from "../../../api/ApiCall"; import _ from "lodash"; -import { createDriftManagementActions } from "./manage-drift/driftManagementActions"; +import { createDriftManagementActions } from "../manage/driftManagementActions"; import { ActionsMenu } from "/src/components/actions-menu"; import { useSettings } from "/src/hooks/use-settings"; diff --git a/src/pages/tenant/standards/tenant-alignment/index.js b/src/pages/tenant/standards/tenant-alignment/index.js index 34467ff6ea4b..e891f2a0576d 100644 --- a/src/pages/tenant/standards/tenant-alignment/index.js +++ b/src/pages/tenant/standards/tenant-alignment/index.js @@ -8,7 +8,7 @@ const Page = () => { const actions = [ { label: "View Tenant Report", - link: "/tenant/standards/compare?tenantFilter=[tenantFilter]&templateId=[standardId]", + link: "/tenant/manage/applied-standards/?tenantFilter=[tenantFilter]&templateId=[standardId]", icon: , color: "info", target: "_self", From 3b7159e0b2cd5f6e836981db70a35cab5966628f Mon Sep 17 00:00:00 2001 From: John Duprey Date: Mon, 6 Oct 2025 15:44:58 -0400 Subject: [PATCH 095/112] fix link --- .../standards/list-standards/classic-standards/index.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) 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 60681e160b09..a204fddb81b9 100644 --- a/src/pages/tenant/standards/list-standards/classic-standards/index.js +++ b/src/pages/tenant/standards/list-standards/classic-standards/index.js @@ -28,7 +28,7 @@ const Page = () => { const actions = [ { label: "View Tenant Report", - link: "/tenant/manage/applied-standards/?tenantFilter=[tenantFilter]&templateId=[standardId]", + link: "/tenant/manage/applied-standards/?templateId=[GUID]", icon: , color: "info", target: "_self", @@ -186,7 +186,12 @@ const Page = () => { - Date: Mon, 6 Oct 2025 17:23:53 -0400 Subject: [PATCH 096/112] fix autopilot issue with hardware hash boolean --- .../CippComponents/CippAutopilotProfileDrawer.jsx | 2 ++ src/pages/endpoint/autopilot/list-profiles/index.js | 9 +++++++-- src/utils/get-cipp-filter-variant.js | 8 +++++++- src/utils/get-cipp-formatting.js | 7 +++++-- 4 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/components/CippComponents/CippAutopilotProfileDrawer.jsx b/src/components/CippComponents/CippAutopilotProfileDrawer.jsx index 90a7f852dc3f..9ac21d1fd858 100644 --- a/src/components/CippComponents/CippAutopilotProfileDrawer.jsx +++ b/src/components/CippComponents/CippAutopilotProfileDrawer.jsx @@ -188,6 +188,8 @@ export const CippAutopilotProfileDrawer = ({ label="Hide Change Account Options" name="HideChangeAccount" formControl={formControl} + disabled={true} + helperText="This setting requires Hybrid Azure AD Join which is not supported in CIPP" /> { const simpleColumns = [ "displayName", - "Description", + "description", "language", "extractHardwareHash", "deviceNameTemplate", @@ -36,7 +36,12 @@ const Page = () => { return ( { //First key based filters switch (tailKey) { case "assignedLicenses": - // Extract unique licenses from the data if available let filterSelectOptions = []; if (isOptions && arg.dataArray && Array.isArray(arg.dataArray)) { @@ -116,4 +115,11 @@ export const getCippFilterVariant = (providedColumnKeys, arg) => { filterFn: "betweenInclusive", }; } + + // Default fallback for any remaining cases - use text filter to avoid localeCompare issues + return { + filterVariant: "text", + sortingFn: "alphanumeric", + filterFn: "includes", + }; }; diff --git a/src/utils/get-cipp-formatting.js b/src/utils/get-cipp-formatting.js index e2df26a72ffd..90902925e4d0 100644 --- a/src/utils/get-cipp-formatting.js +++ b/src/utils/get-cipp-formatting.js @@ -193,8 +193,11 @@ export const getCippFormatting = (data, cellName, type, canReceive, flatten = tr // Handle hardware hash fields const hardwareHashFields = ["hardwareHash", "Hardware Hash"]; - if (hardwareHashFields.includes(cellName) || cellNameLower.includes("hardware")) { - if (typeof data === "string" && data.length > 15) { + if ( + typeof data === "string" && + (hardwareHashFields.includes(cellName) || cellNameLower.includes("hardware")) + ) { + if (data.length > 15) { return isText ? data : `${data.substring(0, 15)}...`; } return isText ? data : data; From 19ebcfcb7e20edba107c63a31faa3f9a7f7c4a22 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Mon, 6 Oct 2025 17:43:32 -0400 Subject: [PATCH 097/112] Update index.js --- src/pages/endpoint/autopilot/list-profiles/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/endpoint/autopilot/list-profiles/index.js b/src/pages/endpoint/autopilot/list-profiles/index.js index 401fdd7a9ec4..63626a661e54 100644 --- a/src/pages/endpoint/autopilot/list-profiles/index.js +++ b/src/pages/endpoint/autopilot/list-profiles/index.js @@ -21,7 +21,7 @@ const Page = () => { ]; const offCanvas = { - children: (row) => , + children: (row) => , size: "xl", }; From 61acc9121406da51658e8f969622737578975d50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Tue, 7 Oct 2025 14:28:25 +0200 Subject: [PATCH 098/112] refactor: reorder actions and update confirm texts with CA policy names --- .../tenant/conditional/list-policies/index.js | 101 +++++++++--------- 1 file changed, 51 insertions(+), 50 deletions(-) diff --git a/src/pages/tenant/conditional/list-policies/index.js b/src/pages/tenant/conditional/list-policies/index.js index 9f22e22ce45d..5fd18aafeeba 100644 --- a/src/pages/tenant/conditional/list-policies/index.js +++ b/src/pages/tenant/conditional/list-policies/index.js @@ -1,12 +1,13 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; import { - Block as BlockIcon, - Check as CheckIcon, - Delete as DeleteIcon, - MenuBook as MenuBookIcon, - Visibility as VisibilityIcon, - Edit as EditIcon, + Block, + Check, + Delete, + MenuBook, + Visibility, + Edit, + VerifiedUser, } from "@mui/icons-material"; import { Box } from "@mui/material"; import CippJsonView from "../../../../components/CippFormPages/CippJSONView"; @@ -31,10 +32,36 @@ const Page = () => { return JSON.parse(data.rawjson); }, hideBulk: true, - confirmText: "Are you sure you want to create a template based on this policy?", - icon: , + confirmText: `Are you sure you want to create a template based on "[displayName]"?`, + icon: , color: "info", }, + { + label: "Change Display Name", + type: "POST", + url: "/api/EditCAPolicy", + data: { + GUID: "id", + }, + confirmText: `What do you want to change the display name of "[displayName]" to?`, + icon: , + color: "info", + hideBulk: true, + fields: [ + { + type: "textField", + name: "newDisplayName", + label: "New Display Name", + required: true, + validate: (value) => { + if (!value) { + return "Display name is required."; + } + return true; + }, + }, + ], + }, { label: "Enable policy", type: "POST", @@ -43,9 +70,9 @@ const Page = () => { GUID: "id", State: "!Enabled", }, - confirmText: "Are you sure you want to enable this policy?", + confirmText: `Are you sure you want to enable "[displayName]"?`, condition: (row) => row.state !== "enabled", - icon: , + icon: , color: "info", }, { @@ -56,9 +83,9 @@ const Page = () => { GUID: "id", State: "!Disabled", }, - confirmText: "Are you sure you want to disable this policy?", + confirmText: `Are you sure you want to disable "[displayName]"?`, condition: (row) => row.state !== "disabled", - icon: , + icon: , color: "info", }, { @@ -69,58 +96,32 @@ const Page = () => { GUID: "id", State: "!enabledForReportingButNotEnforced", }, - confirmText: "Are you sure you want to set this policy to report only?", + confirmText: `Are you sure you want to set "[displayName]" to report only?`, condition: (row) => row.state !== "enabledForReportingButNotEnforced", - icon: , + icon: , color: "info", }, { - label: "Delete policy", - type: "POST", - url: "/api/RemoveCAPolicy", - data: { - GUID: "id", - }, - confirmText: "Are you sure you want to delete this policy?", - icon: , - color: "danger", - }, - { - label: "Change Display Name", + label: "Add service provider exception to policy", type: "POST", - url: "/api/EditCAPolicy", + url: "/api/ExecCAServiceExclusion", data: { GUID: "id", }, - confirmText: "Are you sure you want to change the display name of this policy?", - icon: , - color: "info", - hideBulk: true, - fields: [ - { - type: "textField", - name: "newDisplayName", - label: "New Display Name", - required: true, - validate: (value) => { - if (!value) { - return "Display name is required."; - } - return true; - }, - }, - ], + confirmText: `Are you sure you want to add the service provider exception to "[displayName]"?`, + icon: , + color: "warning", }, { - label: "Add service provider exception to policy", + label: "Delete policy", type: "POST", - url: "/api/ExecCAServiceExclusion", + url: "/api/RemoveCAPolicy", data: { GUID: "id", }, - confirmText: "Are you sure you want to add the service provider exception to this policy?", - icon: , - color: "warning", + confirmText: `Are you sure you want to delete "[displayName]"?`, + icon: , + color: "danger", }, ]; From f09f56ad61f72066ca2769e8dac81ddde6679acf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Tue, 7 Oct 2025 14:52:45 +0200 Subject: [PATCH 099/112] fix: handle potential null values in invite and onboarding retrieval --- .../tenant/gdap-management/onboarding/start.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/pages/tenant/gdap-management/onboarding/start.js b/src/pages/tenant/gdap-management/onboarding/start.js index d52982666c0c..0e692528751a 100644 --- a/src/pages/tenant/gdap-management/onboarding/start.js +++ b/src/pages/tenant/gdap-management/onboarding/start.js @@ -134,13 +134,17 @@ const Page = () => { setInvalidRelationship(true); } } - const invite = currentInvites?.data?.pages?.[0]?.find( - (invite) => invite?.RowKey === formValue?.value - ); + const invite = + currentInvites?.data?.pages?.[0] && Array.isArray(currentInvites.data.pages[0]) + ? currentInvites.data.pages[0].find((invite) => invite?.RowKey === formValue?.value) + : null; - const onboarding = onboardingList.data?.pages?.[0]?.find( - (onboarding) => onboarding?.RowKey === formValue?.value - ); + const onboarding = + onboardingList.data?.pages?.[0] && Array.isArray(onboardingList.data.pages[0]) + ? onboardingList.data.pages[0].find( + (onboarding) => onboarding?.RowKey === formValue?.value + ) + : null; if (onboarding) { setCurrentOnboarding(onboarding); var stepCount = 0; From 467804c0beae542d1e5873616cb23943e3cba870 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Tue, 7 Oct 2025 15:50:45 +0200 Subject: [PATCH 100/112] feat: add option to exclude onboarded tenant from top-level standards --- .../tenant/gdap-management/onboarding/start.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/pages/tenant/gdap-management/onboarding/start.js b/src/pages/tenant/gdap-management/onboarding/start.js index 0e692528751a..e6ae11357c1a 100644 --- a/src/pages/tenant/gdap-management/onboarding/start.js +++ b/src/pages/tenant/gdap-management/onboarding/start.js @@ -251,6 +251,11 @@ const Page = () => { if (formControl.getValues("ignoreMissingRoles")) { data.ignoreMissingRoles = Boolean(formControl.getValues("ignoreMissingRoles")); } + if (formControl.getValues("standardsExcludeAllTenants")) { + data.standardsExcludeAllTenants = Boolean( + formControl.getValues("standardsExcludeAllTenants") + ); + } startOnboarding.mutate({ url: "/api/ExecOnboardTenant", @@ -275,6 +280,11 @@ const Page = () => { if (formControl.getValues("ignoreMissingRoles")) { data.IgnoreMissingRoles = Boolean(formControl.getValues("ignoreMissingRoles")); } + if (formControl.getValues("standardsExcludeAllTenants")) { + data.standardsExcludeAllTenants = Boolean( + formControl.getValues("standardsExcludeAllTenants") + ); + } startOnboarding.mutate({ url: "/api/ExecOnboardTenant", @@ -402,6 +412,13 @@ const Page = () => { /> )} + {currentRelationship?.value && ( <> {currentRelationship?.addedFields?.accessDetails?.unifiedRoles.some( From 5fa06a70563bb0f8a41a244ca74ba59dea25efe1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Tue, 7 Oct 2025 17:08:43 +0200 Subject: [PATCH 101/112] Feat: AllTenants support for listing CA policies --- src/pages/tenant/conditional/list-policies/index.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pages/tenant/conditional/list-policies/index.js b/src/pages/tenant/conditional/list-policies/index.js index 5fd18aafeeba..1612ed5d4bda 100644 --- a/src/pages/tenant/conditional/list-policies/index.js +++ b/src/pages/tenant/conditional/list-policies/index.js @@ -137,6 +137,7 @@ const Page = () => { // Columns for CippTablePage const simpleColumns = [ + "Tenant", "displayName", "state", "modifiedDateTime", @@ -164,6 +165,7 @@ const Page = () => { } title={pageTitle} apiUrl={apiUrl} + apiDataKey="Results" actions={actions} offCanvas={offCanvas} simpleColumns={simpleColumns} @@ -171,5 +173,5 @@ const Page = () => { ); }; -Page.getLayout = (page) => {page}; +Page.getLayout = (page) => {page}; export default Page; From 84cb07f64ed4274d1d0ab199a2ec1ee1e4c1eb01 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 7 Oct 2025 11:10:35 -0400 Subject: [PATCH 102/112] fix query key --- src/pages/cipp/settings/notifications.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/pages/cipp/settings/notifications.js b/src/pages/cipp/settings/notifications.js index dbb0727e0be4..408bdf6f5169 100644 --- a/src/pages/cipp/settings/notifications.js +++ b/src/pages/cipp/settings/notifications.js @@ -22,13 +22,10 @@ const Page = () => { formControl={formControl} resetForm={false} postUrl="/api/ExecNotificationConfig" - relatedQueryKeys={["ListNotificationConfig"]} + queryKey={"ListNotificationConfig"} > {/* Use the reusable notification form component */} - + ); }; From 21845cc99570f7f9087fe7a86517b07545e9f5b6 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 7 Oct 2025 11:31:36 -0400 Subject: [PATCH 103/112] fix all tenants dialog actions --- .../CippComponents/CippApiDialog.jsx | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/components/CippComponents/CippApiDialog.jsx b/src/components/CippComponents/CippApiDialog.jsx index 6d441e7412ce..e45fe1a22365 100644 --- a/src/components/CippComponents/CippApiDialog.jsx +++ b/src/components/CippComponents/CippApiDialog.jsx @@ -133,11 +133,16 @@ export const CippApiDialog = (props) => { return; } - const commonData = { - tenantFilter, - ...formData, - ...addedFieldData, + // Helper function to get the correct tenant filter for a row + const getRowTenantFilter = (rowData) => { + // If we're in AllTenants mode and the row has a Tenant property, use that + if (tenantFilter === "AllTenants" && rowData?.Tenant) { + return rowData.Tenant; + } + // Otherwise use the current tenant filter + return tenantFilter; }; + const processedActionData = processActionData(action.data, row, action.replacementBehaviour); if (!processedActionData || Object.keys(processedActionData).length === 0) { @@ -146,6 +151,11 @@ export const CippApiDialog = (props) => { // MULTI ROW CASES if (Array.isArray(row)) { const arrayData = row.map((singleRow) => { + const commonData = { + tenantFilter: getRowTenantFilter(singleRow), + ...formData, + ...addedFieldData, + }; const itemData = { ...commonData }; Object.keys(processedActionData).forEach((key) => { const rowValue = singleRow[processedActionData[key]]; @@ -173,6 +183,14 @@ export const CippApiDialog = (props) => { return; } } + + // SINGLE ROW CASE + const commonData = { + tenantFilter: getRowTenantFilter(row), + ...formData, + ...addedFieldData, + }; + // ✅ FIXED: DIRECT MERGE INSTEAD OF CORRUPT TRANSFORMATION finalData = { ...commonData, From cc28bd90157cca8a87103d49eef613206fdee707 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 7 Oct 2025 11:44:24 -0400 Subject: [PATCH 104/112] better handling of child intune objects null/undefined or non array for options --- src/components/CippFormPages/CippJSONView.jsx | 12 ++++++++---- src/pages/endpoint/MEM/list-templates/index.js | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/components/CippFormPages/CippJSONView.jsx b/src/components/CippFormPages/CippJSONView.jsx index 2cd814d427b7..5f3de20a24f7 100644 --- a/src/components/CippFormPages/CippJSONView.jsx +++ b/src/components/CippFormPages/CippJSONView.jsx @@ -286,9 +286,11 @@ function CippJsonView({ let value; if (child.choiceSettingValue && child.choiceSettingValue.value) { value = - childIntuneObj?.options?.find( - (option) => option.id === child.choiceSettingValue.value - )?.displayName || child.choiceSettingValue.value; + (Array.isArray(childIntuneObj?.options) && + childIntuneObj.options.find( + (option) => option.id === child.choiceSettingValue.value + )?.displayName) || + child.choiceSettingValue.value; } items.push( option.id === rawValue)?.displayName || rawValue; + (Array.isArray(intuneObj?.options) && + intuneObj.options.find((option) => option.id === rawValue)?.displayName) || + rawValue; // Check if optionValue is a GUID that we've resolved if (typeof optionValue === "string" && isGuid(optionValue) && guidMapping[optionValue]) { diff --git a/src/pages/endpoint/MEM/list-templates/index.js b/src/pages/endpoint/MEM/list-templates/index.js index a987aa3e1c7c..2c06faf5f4da 100644 --- a/src/pages/endpoint/MEM/list-templates/index.js +++ b/src/pages/endpoint/MEM/list-templates/index.js @@ -135,7 +135,7 @@ const Page = () => { ]; const offCanvas = { - children: (row) => , + children: (row) => , size: "lg", }; From 24b7f1336e7bc95ae8b4c0f46a042284af72bdc2 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 7 Oct 2025 11:56:27 -0400 Subject: [PATCH 105/112] fix layering issue with tables in dialogs fixes #4741 --- .../CippComponents/CippTableDialog.jsx | 58 +++++++------- .../CippTable/CIPPTableToptoolbar.js | 76 ++++++++++++++----- src/components/CippTable/CippDataTable.js | 2 + .../CippTable/CippDataTableButton.jsx | 1 + 4 files changed, 89 insertions(+), 48 deletions(-) diff --git a/src/components/CippComponents/CippTableDialog.jsx b/src/components/CippComponents/CippTableDialog.jsx index 59ec73436efd..e31d4485263b 100644 --- a/src/components/CippComponents/CippTableDialog.jsx +++ b/src/components/CippComponents/CippTableDialog.jsx @@ -1,28 +1,30 @@ -import { Button, Dialog, DialogActions, DialogContent, DialogTitle } from "@mui/material"; -import { Stack } from "@mui/system"; -import { CippDataTable } from "../CippTable/CippDataTable"; - -export const CippTableDialog = (props) => { - const { createDialog, title, fields, api, simpleColumns, ...other } = props; - return ( - - {title} - - - - - - - - - - ); -}; +import { Button, Dialog, DialogActions, DialogContent, DialogTitle } from "@mui/material"; +import { Stack } from "@mui/system"; +import { CippDataTable } from "../CippTable/CippDataTable"; + +export const CippTableDialog = (props) => { + const { createDialog, title, fields, api, simpleColumns, ...other } = props; + + return ( + + {title} + + + + + + + + + + ); +}; diff --git a/src/components/CippTable/CIPPTableToptoolbar.js b/src/components/CippTable/CIPPTableToptoolbar.js index a41d9e1b3785..f4374fbf64b5 100644 --- a/src/components/CippTable/CIPPTableToptoolbar.js +++ b/src/components/CippTable/CIPPTableToptoolbar.js @@ -14,6 +14,10 @@ import { Paper, Checkbox, SvgIcon, + Dialog, + DialogTitle, + DialogContent, + DialogActions, } from "@mui/material"; import { Search as SearchIcon, @@ -158,6 +162,7 @@ export const CIPPTableToptoolbar = ({ setGraphFilterData, setConfiguredSimpleColumns, queueMetadata, + isInDialog = false, }) => { const popover = usePopover(); const [filtersAnchor, setFiltersAnchor] = useState(null); @@ -172,6 +177,7 @@ export const CIPPTableToptoolbar = ({ const createDialog = useDialog(); const [actionData, setActionData] = useState({ data: {}, action: {}, ready: false }); const [offcanvasVisible, setOffcanvasVisible] = useState(false); + const [jsonDialogOpen, setJsonDialogOpen] = useState(false); // For dialog-based JSON view const [filterList, setFilterList] = useState(filters); const [currentEffectiveQueryKey, setCurrentEffectiveQueryKey] = useState(queryKey || title); const [originalSimpleColumns, setOriginalSimpleColumns] = useState(simpleColumns); @@ -983,7 +989,11 @@ export const CIPPTableToptoolbar = ({ { - setOffcanvasVisible(true); + if (isInDialog) { + setJsonDialogOpen(true); + } else { + setOffcanvasVisible(true); + } setExportAnchor(null); }} > @@ -1152,25 +1162,27 @@ export const CIPPTableToptoolbar = ({ ))} - {/* API Response Off-Canvas */} - { - setOffcanvasVisible(false); - }} - > - - API Response - - - + {/* API Response Off-Canvas - only show when not in dialog mode */} + {!isInDialog && ( + { + setOffcanvasVisible(false); + }} + > + + API Response + + + + )} {/* Action Dialog */} {actionData.ready && ( @@ -1217,6 +1229,30 @@ export const CIPPTableToptoolbar = ({ component="card" /> + + {/* JSON Dialog for when in dialog mode */} + {isInDialog && ( + setJsonDialogOpen(false)} + sx={{ zIndex: (theme) => theme.zIndex.modal + 1 }} + > + API Response + + + + + + + + )} ); }; diff --git a/src/components/CippTable/CippDataTable.js b/src/components/CippTable/CippDataTable.js index 898610c48149..2cfba0bc9640 100644 --- a/src/components/CippTable/CippDataTable.js +++ b/src/components/CippTable/CippDataTable.js @@ -56,6 +56,7 @@ export const CippDataTable = (props) => { filters, maxHeightOffset = "380px", defaultSorting = [], + isInDialog = false, } = props; const [columnVisibility, setColumnVisibility] = useState(initialColumnVisibility); const [configuredSimpleColumns, setConfiguredSimpleColumns] = useState(simpleColumns); @@ -439,6 +440,7 @@ export const CippDataTable = (props) => { setGraphFilterData={setGraphFilterData} setConfiguredSimpleColumns={setConfiguredSimpleColumns} queueMetadata={getRequestData.data?.pages?.[0]?.Metadata} + isInDialog={isInDialog} /> )} diff --git a/src/components/CippTable/CippDataTableButton.jsx b/src/components/CippTable/CippDataTableButton.jsx index b2a89b7fc927..79eec0f04bc5 100644 --- a/src/components/CippTable/CippDataTableButton.jsx +++ b/src/components/CippTable/CippDataTableButton.jsx @@ -58,6 +58,7 @@ const CippDataTableButton = ({ data, title, tableTitle = "Data" }) => { title={tableTitle} data={dialogData} simple={false} + isInDialog={true} /> From 73b0f838ddcfd94f6ebde75375ce64e0d64cf172 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 7 Oct 2025 12:39:31 -0400 Subject: [PATCH 106/112] better clipboard handling --- .../CippComponents/CippCodeBlock.jsx | 4 +- .../CippComponents/CippCopyToClipboard.jsx | 93 ++++++++++++------- .../CippTable/CippGraphExplorerFilter.js | 4 + src/components/property-list-item.js | 26 +----- 4 files changed, 68 insertions(+), 59 deletions(-) diff --git a/src/components/CippComponents/CippCodeBlock.jsx b/src/components/CippComponents/CippCodeBlock.jsx index 6dbf8f0e047f..507a26667bbd 100644 --- a/src/components/CippComponents/CippCodeBlock.jsx +++ b/src/components/CippComponents/CippCodeBlock.jsx @@ -16,7 +16,7 @@ const CodeContainer = styled("div")` padding-bottom: 1rem; .cipp-code-copy-button { position: absolute; - right: 0.5rem; + right: 1rem; /* Moved further left to avoid Monaco scrollbar */ top: 0.5rem; z-index: 1; /* Ensure the button is above the code block */ } @@ -54,7 +54,7 @@ export const CippCodeBlock = (props) => { options={{ wordWrap: true, lineNumbers: showLineNumbers ? "on" : "off", - minimap: { enabled: showLineNumbers}, + minimap: { enabled: showLineNumbers }, }} {...other} /> diff --git a/src/components/CippComponents/CippCopyToClipboard.jsx b/src/components/CippComponents/CippCopyToClipboard.jsx index cdc18f224f8b..2be6d6aa2880 100644 --- a/src/components/CippComponents/CippCopyToClipboard.jsx +++ b/src/components/CippComponents/CippCopyToClipboard.jsx @@ -1,11 +1,40 @@ import { CopyAll, Visibility, VisibilityOff } from "@mui/icons-material"; import { Chip, IconButton, SvgIcon, Tooltip } from "@mui/material"; import { useState } from "react"; -import CopyToClipboard from "react-copy-to-clipboard"; export const CippCopyToClipBoard = (props) => { - const { text, type = "button", visible = true, ...other } = props; + const { text, type = "button", visible = true, onClick, ...other } = props; const [showPassword, setShowPassword] = useState(false); + const [copied, setCopied] = useState(false); + + const handleCopy = async () => { + try { + await navigator.clipboard.writeText(text); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + if (onClick) onClick(); + } catch (err) { + console.error("Failed to copy text: ", err); + // Fallback for older browsers + try { + const textArea = document.createElement("textarea"); + textArea.value = text; + textArea.style.position = "fixed"; + textArea.style.left = "-999999px"; + textArea.style.top = "-999999px"; + document.body.appendChild(textArea); + textArea.focus(); + textArea.select(); + document.execCommand("copy"); + document.body.removeChild(textArea); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + if (onClick) onClick(); + } catch (fallbackErr) { + console.error("Fallback copy failed: ", fallbackErr); + } + } + }; const handleTogglePassword = () => { setShowPassword((prev) => !prev); @@ -15,32 +44,29 @@ export const CippCopyToClipBoard = (props) => { if (type === "button") { return ( - - - - - - - - - + + + + + + + ); } if (type === "chip") { return ( - - - - - + + + ); } @@ -52,17 +78,16 @@ export const CippCopyToClipBoard = (props) => { {showPassword ? : } - - - - - + + + ); } diff --git a/src/components/CippTable/CippGraphExplorerFilter.js b/src/components/CippTable/CippGraphExplorerFilter.js index a6b82ccc80ff..ad1315667b35 100644 --- a/src/components/CippTable/CippGraphExplorerFilter.js +++ b/src/components/CippTable/CippGraphExplorerFilter.js @@ -398,6 +398,9 @@ const CippGraphExplorerFilter = ({ Import / Export Graph Explorer Preset + + Copy the JSON below to export your preset, or paste a preset JSON to import it. + setEditorValues(JSON.parse(value))} @@ -412,6 +415,7 @@ const CippGraphExplorerFilter = ({ }} variant="contained" color="primary" + sx={{ mt: 2 }} > Import Template diff --git a/src/components/property-list-item.js b/src/components/property-list-item.js index aa61fa0b5d23..4249e975cef0 100644 --- a/src/components/property-list-item.js +++ b/src/components/property-list-item.js @@ -1,16 +1,6 @@ -import { - Box, - Button, - IconButton, - ListItem, - ListItemText, - SvgIcon, - Tooltip, - Typography, -} from "@mui/material"; +import { Box, Button, ListItem, ListItemText, Typography } from "@mui/material"; import { useState } from "react"; -import CopyToClipboard from "react-copy-to-clipboard"; -import { CopyAll } from "@mui/icons-material"; +import { CippCopyToClipBoard } from "./CippComponents/CippCopyToClipboard"; export const PropertyListItem = (props) => { const { @@ -57,17 +47,7 @@ export const PropertyListItem = (props) => { )} )} - {copyItems && ( - - - - - - - - - - )} + {copyItems && } )} From b3c4083675ba772fd844a50d7be1b26b21cb186b Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 7 Oct 2025 20:40:31 -0400 Subject: [PATCH 107/112] fix missing data keys --- .../CippComponents/CippFormUserSelector.jsx | 4 +- src/components/ExecutiveReportButton.js | 41 ++++++++++++------- .../conditional/deploy-vacation/add.jsx | 3 ++ 3 files changed, 32 insertions(+), 16 deletions(-) diff --git a/src/components/CippComponents/CippFormUserSelector.jsx b/src/components/CippComponents/CippFormUserSelector.jsx index 7e5b11c0f551..18a2e8d13fa9 100644 --- a/src/components/CippComponents/CippFormUserSelector.jsx +++ b/src/components/CippComponents/CippFormUserSelector.jsx @@ -13,6 +13,7 @@ export const CippFormUserSelector = ({ addedField, valueField, dataFilter = null, + showRefresh = false, ...other }) => { const currentTenant = useWatch({ control: formControl.control, name: "tenantFilter" }); @@ -47,7 +48,8 @@ export const CippFormUserSelector = ({ return options.filter(dataFilter); } return options; - } + }, + showRefresh: showRefresh, }} creatable={false} {...other} diff --git a/src/components/ExecutiveReportButton.js b/src/components/ExecutiveReportButton.js index da34559bf8d9..cbd26cc24968 100644 --- a/src/components/ExecutiveReportButton.js +++ b/src/components/ExecutiveReportButton.js @@ -1194,16 +1194,24 @@ const ExecutiveReportDocument = ({ const endAngle = currentAngle + angle; // Outer arc points - const outerStartX = centerX + outerRadius * Math.cos((startAngle * Math.PI) / 180); - const outerStartY = centerY + outerRadius * Math.sin((startAngle * Math.PI) / 180); - const outerEndX = centerX + outerRadius * Math.cos((endAngle * Math.PI) / 180); - const outerEndY = centerY + outerRadius * Math.sin((endAngle * Math.PI) / 180); + const outerStartX = + centerX + outerRadius * Math.cos((startAngle * Math.PI) / 180); + const outerStartY = + centerY + outerRadius * Math.sin((startAngle * Math.PI) / 180); + const outerEndX = + centerX + outerRadius * Math.cos((endAngle * Math.PI) / 180); + const outerEndY = + centerY + outerRadius * Math.sin((endAngle * Math.PI) / 180); // Inner arc points - const innerStartX = centerX + innerRadius * Math.cos((startAngle * Math.PI) / 180); - const innerStartY = centerY + innerRadius * Math.sin((startAngle * Math.PI) / 180); - const innerEndX = centerX + innerRadius * Math.cos((endAngle * Math.PI) / 180); - const innerEndY = centerY + innerRadius * Math.sin((endAngle * Math.PI) / 180); + const innerStartX = + centerX + innerRadius * Math.cos((startAngle * Math.PI) / 180); + const innerStartY = + centerY + innerRadius * Math.sin((startAngle * Math.PI) / 180); + const innerEndX = + centerX + innerRadius * Math.cos((endAngle * Math.PI) / 180); + const innerEndY = + centerY + innerRadius * Math.sin((endAngle * Math.PI) / 180); const largeArcFlag = angle > 180 ? 1 : 0; @@ -1213,8 +1221,8 @@ const ExecutiveReportDocument = ({ `A ${outerRadius} ${outerRadius} 0 ${largeArcFlag} 1 ${outerEndX} ${outerEndY}`, `L ${innerEndX} ${innerEndY}`, `A ${innerRadius} ${innerRadius} 0 ${largeArcFlag} 0 ${innerStartX} ${innerStartY}`, - 'Z' - ].join(' '); + "Z", + ].join(" "); currentAngle += angle; @@ -1256,15 +1264,17 @@ const ExecutiveReportDocument = ({ .map((value, index) => ({ value, index, - label: chartLabels[index].replace(" Deviations", "").replace(" Policies", ""), - color: chartColors[index] + label: chartLabels[index] + .replace(" Deviations", "") + .replace(" Policies", ""), + color: chartColors[index], })) - .filter(item => item.value > 0); - + .filter((item) => item.value > 0); + return visibleItems.map((item, displayIndex) => { const legendX = 30 + displayIndex * 90; const legendY = 175; - + return ( { data: { tenantFilter: settings.currentTenant, }, + dataKey: "Results", queryKey: `ca-policies-report-${settings.currentTenant}`, waiting: previewOpen, }); diff --git a/src/pages/tenant/conditional/deploy-vacation/add.jsx b/src/pages/tenant/conditional/deploy-vacation/add.jsx index b4fa24353a71..afa926d47db2 100644 --- a/src/pages/tenant/conditional/deploy-vacation/add.jsx +++ b/src/pages/tenant/conditional/deploy-vacation/add.jsx @@ -74,6 +74,7 @@ const Page = () => { validators={{ required: "Picking a user is required" }} required={true} disabled={!tenantDomain} + showRefresh={true} /> @@ -93,8 +94,10 @@ const Page = () => { queryKey: `ListConditionalAccessPolicies-${tenantDomain}`, url: "/api/ListConditionalAccessPolicies", data: { tenantFilter: tenantDomain }, + dataKey: "Results", labelField: (option) => `${option.displayName}`, valueField: "id", + showRefresh: true, } : null } From 6a7cb8c4b679ad342c52fd686576ecdb9a60117d Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 7 Oct 2025 21:02:03 -0400 Subject: [PATCH 108/112] update to MUI components --- .../administration/restricted-users/index.js | 75 +++++++++++++------ 1 file changed, 54 insertions(+), 21 deletions(-) diff --git a/src/pages/email/administration/restricted-users/index.js b/src/pages/email/administration/restricted-users/index.js index 3f21e1f50d0a..62129b05d6b0 100644 --- a/src/pages/email/administration/restricted-users/index.js +++ b/src/pages/email/administration/restricted-users/index.js @@ -1,6 +1,6 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; -import { Alert } from "@mui/material"; +import { Alert, Link, Typography, List, ListItem, ListItemText } from "@mui/material"; import { Block as BlockIcon } from "@mui/icons-material"; const Page = () => { @@ -32,26 +32,59 @@ const Page = () => { return ( <> - - Users in this list have been restricted from sending email due to exceeding outbound spam - limits. - -
    - This typically indicates a compromised account.{" "} - - Before unblocking, ensure you have properly secured the account. - - Recommended actions include: -
      -
    • Checked for suspicious sign-ins and activities
    • -
    • Reviewed their mailbox rules and forwarding settings
    • -
    • - Investigated any unusual mailbox activity, such as unexpected sent items via message - trace -
    • -
    • Reset the user's password if compromised
    • -
    • Enabled MFA on the account if not already enabled
    • -
    + + + Users in this list have been restricted from sending email due to exceeding outbound spam + limits. + + + + This typically indicates a compromised account.{" "} + + Before unblocking, ensure you have properly secured the account. + + + + Recommended actions include: + + + + + + + + + + + + + + + + + +
    Date: Tue, 7 Oct 2025 21:16:35 -0400 Subject: [PATCH 109/112] formatting --- .../administration/restricted-users/index.js | 26 +++++++------------ 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/src/pages/email/administration/restricted-users/index.js b/src/pages/email/administration/restricted-users/index.js index 62129b05d6b0..faa3b3792daa 100644 --- a/src/pages/email/administration/restricted-users/index.js +++ b/src/pages/email/administration/restricted-users/index.js @@ -34,8 +34,8 @@ const Page = () => { - Users in this list have been restricted from sending email due to exceeding outbound spam - limits. + Users in this list have been restricted from sending email due to exceeding outbound + spam limits. @@ -53,32 +53,26 @@ const Page = () => { Recommended actions include: - - - + + + - + - + - - + + - + Date: Tue, 7 Oct 2025 21:16:43 -0400 Subject: [PATCH 110/112] remove invalid link --- src/layouts/config.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/layouts/config.js b/src/layouts/config.js index e23ff8a95dba..115de1795d46 100644 --- a/src/layouts/config.js +++ b/src/layouts/config.js @@ -726,11 +726,6 @@ export const nativeMenuItems = [ path: "/email/reports/malware-filters", permissions: ["Exchange.SpamFilter.*"], }, - { - title: "Safe Links Filters", - path: "/email/reports/safelinks-filters", - permissions: ["Exchange.SafeLinks.*"], - }, { title: "Safe Attachments Filters", path: "/email/reports/safeattachments-filters", From 12aba14bba35d78dd9d9c291faa8a546d722c7b3 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 7 Oct 2025 23:09:12 -0400 Subject: [PATCH 111/112] mailbox restore tweaks --- src/api/ApiCall.jsx | 12 - .../CippMailboxRestoreDrawer.jsx | 538 ++++++++++++++++++ .../email/tools/mailbox-restores/index.js | 19 +- 3 files changed, 543 insertions(+), 26 deletions(-) create mode 100644 src/components/CippComponents/CippMailboxRestoreDrawer.jsx diff --git a/src/api/ApiCall.jsx b/src/api/ApiCall.jsx index 8b07fdcf99da..c069208c2dc4 100644 --- a/src/api/ApiCall.jsx +++ b/src/api/ApiCall.jsx @@ -177,8 +177,6 @@ export function ApiPostCall({ relatedQueryKeys, onResult }) { } }, onSuccess: () => { - console.log("ApiPostCall onSuccess triggered with relatedQueryKeys:", relatedQueryKeys); - if (relatedQueryKeys) { const clearKeys = Array.isArray(relatedQueryKeys) ? relatedQueryKeys : [relatedQueryKeys]; setTimeout(() => { @@ -192,15 +190,8 @@ export function ApiPostCall({ relatedQueryKeys, onResult }) { .map((key) => key.slice(0, -1)); const exactKeys = clearKeys.filter((key) => !key.endsWith("*")); - console.log("ApiPostCall Invalidation Debug:", { - clearKeys, - wildcardPatterns, - exactKeys, - }); - // Use single predicate call for all wildcard patterns if (wildcardPatterns.length > 0) { - console.log("Running predicate invalidation for patterns:", wildcardPatterns); queryClient.invalidateQueries({ predicate: (query) => { if (!query.queryKey || !query.queryKey[0]) return false; @@ -227,13 +218,10 @@ export function ApiPostCall({ relatedQueryKeys, onResult }) { // Handle exact keys exactKeys.forEach((key) => { - console.log("Invalidating exact key:", key); queryClient.invalidateQueries({ queryKey: [key] }); }); } }, 1000); - } else { - console.log("No relatedQueryKeys provided to ApiPostCall"); } }, }); diff --git a/src/components/CippComponents/CippMailboxRestoreDrawer.jsx b/src/components/CippComponents/CippMailboxRestoreDrawer.jsx new file mode 100644 index 000000000000..81e03bb4c659 --- /dev/null +++ b/src/components/CippComponents/CippMailboxRestoreDrawer.jsx @@ -0,0 +1,538 @@ +import { useEffect, useState } from "react"; +import { useForm, useWatch, useFormState } from "react-hook-form"; +import { + Button, + Drawer, + Box, + Typography, + IconButton, + Alert, + Divider, + CircularProgress, + Card, + CardContent, + Chip, + Tooltip, +} from "@mui/material"; +import { Grid } from "@mui/system"; +import { + Close as CloseIcon, + RestoreFromTrash, + DeleteForever, + Archive, + Storage, + AccountBox, +} from "@mui/icons-material"; +import { useSettings } from "../../hooks/use-settings"; +import { getCippTranslation } from "../../utils/get-cipp-translation"; +import CippFormComponent from "./CippFormComponent"; +import { CippApiResults } from "./CippApiResults"; +import { ApiPostCall } from "../../api/ApiCall"; + +const wellKnownFolders = [ + "Inbox", + "SentItems", + "DeletedItems", + "Calendar", + "Contacts", + "Drafts", + "Journal", + "Tasks", + "Notes", + "JunkEmail", + "CommunicationHistory", + "Voicemail", + "Fax", + "Conflicts", + "SyncIssues", + "LocalFailures", + "ServerFailures", +].map((folder) => ({ value: `#${folder}#`, label: getCippTranslation(folder) })); + +export const CippMailboxRestoreDrawer = ({ + buttonText = "New Restore Job", + requiredPermissions = [], + PermissionButton = Button, +}) => { + const [drawerVisible, setDrawerVisible] = useState(false); + const userSettingsDefaults = useSettings(); + const tenantDomain = userSettingsDefaults.currentTenant; + + const formControl = useForm({ + mode: "onBlur", + defaultValues: { + tenantFilter: tenantDomain, + }, + }); + + const createRestore = ApiPostCall({ + relatedQueryKeys: ["MailboxRestores*"], + datafromurl: true, + }); + + const { isValid, isDirty } = useFormState({ control: formControl.control }); + + const sourceMailbox = useWatch({ control: formControl.control, name: "SourceMailbox" }); + const targetMailbox = useWatch({ control: formControl.control, name: "TargetMailbox" }); + + // Helper function to check if archive is active (GUID exists and is not all zeros) + const hasActiveArchive = (mailbox) => { + const archiveGuid = mailbox?.addedFields?.ArchiveGuid; + return ( + archiveGuid && + archiveGuid !== "00000000-0000-0000-0000-000000000000" && + archiveGuid.replace(/0/g, "").replace(/-/g, "") !== "" + ); + }; + + useEffect(() => { + if (sourceMailbox && targetMailbox) { + const sourceUPN = sourceMailbox.value; + const targetUPN = targetMailbox.value; + const randomGUID = crypto.randomUUID(); + formControl.setValue("RequestName", `Restore ${sourceUPN} to ${targetUPN} (${randomGUID})`, { + shouldDirty: true, + shouldValidate: true, + }); + } + }, [sourceMailbox?.value, targetMailbox?.value]); + + useEffect(() => { + if (createRestore.isSuccess) { + formControl.reset(); + } + }, [createRestore.isSuccess]); + + const handleSubmit = () => { + const values = formControl.getValues(); + const shippedValues = { + TenantFilter: tenantDomain, + RequestName: values.RequestName, + SourceMailbox: values.SourceMailbox?.addedFields?.ExchangeGuid ?? values.SourceMailbox?.value, + TargetMailbox: values.TargetMailbox?.addedFields?.ExchangeGuid ?? values.TargetMailbox?.value, + BadItemLimit: values.BadItemLimit, + LargeItemLimit: values.LargeItemLimit, + AcceptLargeDataLoss: values.AcceptLargeDataLoss, + AssociatedMessagesCopyOption: values.AssociatedMessagesCopyOption, + ExcludeFolders: values.ExcludeFolders, + IncludeFolders: values.IncludeFolders, + BatchName: values.BatchName, + CompletedRequestAgeLimit: values.CompletedRequestAgeLimit, + ConflictResolutionOption: values.ConflictResolutionOption, + SourceRootFolder: values.SourceRootFolder, + TargetRootFolder: values.TargetRootFolder, + TargetType: values.TargetType, + ExcludeDumpster: values.ExcludeDumpster, + SourceIsArchive: values.SourceIsArchive, + TargetIsArchive: values.TargetIsArchive, + }; + + createRestore.mutate({ + url: "/api/ExecMailboxRestore", + data: shippedValues, + }); + }; + + const handleCloseDrawer = () => { + formControl.reset(); + setDrawerVisible(false); + }; + + return ( + <> + } + onClick={() => setDrawerVisible(true)} + requiredPermissions={requiredPermissions} + > + {buttonText} + + + + + + New Mailbox Restore + + + + + + + + + + Use this form to restore a mailbox from a soft-deleted state to the target + mailbox. Use the optional settings to tailor the restore request for your needs. + + + + + Restore Settings + + + + `${option.displayName} (${option.UPN})`, + valueField: "UPN", + addedField: { + displayName: "displayName", + ExchangeGuid: "ExchangeGuid", + recipientTypeDetails: "recipientTypeDetails", + ArchiveStatus: "ArchiveStatus", + ArchiveGuid: "ArchiveGuid", + ProhibitSendQuota: "ProhibitSendQuota", + TotalItemSize: "TotalItemSize", + ItemCount: "ItemCount", + WhenSoftDeleted: "WhenSoftDeleted", + }, + url: "/api/ListMailboxes?SoftDeletedMailbox=true", + queryKey: `ListMailboxes-${tenantDomain}-SoftDeleted`, + showRefresh: true, + }} + validators={{ + validate: (value) => (value ? true : "Please select a source mailbox."), + }} + /> + + + {sourceMailbox && ( + + + {sourceMailbox.addedFields?.recipientTypeDetails && ( + + } + label={sourceMailbox.addedFields.recipientTypeDetails} + size="small" + color="info" + variant="outlined" + /> + + )} + + } + label={ + hasActiveArchive(sourceMailbox) + ? "Archive Active" + : "Archive Not Available" + } + size="small" + color={hasActiveArchive(sourceMailbox) ? "success" : "warning"} + variant="outlined" + /> + + + + )} + + + `${option.displayName} (${option.UPN})`, + valueField: "UPN", + addedField: { + displayName: "displayName", + ExchangeGuid: "ExchangeGuid", + recipientTypeDetails: "recipientTypeDetails", + ArchiveStatus: "ArchiveStatus", + ArchiveGuid: "ArchiveGuid", + ProhibitSendQuota: "ProhibitSendQuota", + TotalItemSize: "TotalItemSize", + ItemCount: "ItemCount", + }, + url: "/api/ListMailboxes", + showRefresh: true, + }} + validators={{ + validate: (value) => (value ? true : "Please select a target mailbox."), + }} + /> + + + {targetMailbox && ( + + + {targetMailbox.addedFields?.recipientTypeDetails && ( + + } + label={targetMailbox.addedFields.recipientTypeDetails} + size="small" + color="info" + variant="outlined" + /> + + )} + + } + label={ + hasActiveArchive(targetMailbox) + ? "Archive Active" + : "Archive Not Available" + } + size="small" + color={hasActiveArchive(targetMailbox) ? "success" : "warning"} + variant="outlined" + /> + + {targetMailbox.addedFields?.TotalItemSize && ( + + } + label={targetMailbox.addedFields.TotalItemSize} + size="small" + color="info" + variant="outlined" + /> + + )} + + + )} + + + + + + + + + + + Optional Settings + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/src/pages/email/tools/mailbox-restores/index.js b/src/pages/email/tools/mailbox-restores/index.js index b63d572a32f3..0cd211b5aa4d 100644 --- a/src/pages/email/tools/mailbox-restores/index.js +++ b/src/pages/email/tools/mailbox-restores/index.js @@ -1,13 +1,13 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; -import { Button } from "@mui/material"; -import Link from "next/link"; import { RestoreFromTrash, PlayArrow, Pause, Delete } from "@mui/icons-material"; import MailboxRestoreDetails from "../../../../components/CippComponents/MailboxRestoreDetails"; +import { CippMailboxRestoreDrawer } from "../../../../components/CippComponents/CippMailboxRestoreDrawer"; +import { useSettings } from "/src/hooks/use-settings"; const Page = () => { const pageTitle = "Mailbox Restores"; - + const tenantDomain = useSettings().currentTenant; const actions = [ { label: "Resume Restore Request", @@ -62,17 +62,8 @@ const Page = () => { actions={actions} offCanvas={offCanvas} simpleColumns={simpleColumns} - cardButton={ - <> - - - } + cardButton={} + queryKey={`MailboxRestores-${tenantDomain}`} /> ); }; From dd181bdc5ca7ad8ddeaa72040872a5979269ff3d Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 8 Oct 2025 20:36:40 +1100 Subject: [PATCH 112/112] up version --- package.json | 2 +- public/version.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 2ade75d4e6c0..1a5926d62bfe 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cipp", - "version": "8.4.2", + "version": "8.5.0", "author": "CIPP Contributors", "homepage": "https://cipp.app/", "bugs": { diff --git a/public/version.json b/public/version.json index 6a8d059b3176..5cf63fb43067 100644 --- a/public/version.json +++ b/public/version.json @@ -1,3 +1,3 @@ { - "version": "8.4.2" + "version": "8.5.0" }