From c91817dd42310427bc018fd7846c82c0c6d022d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Mon, 24 Nov 2025 21:52:31 +0100 Subject: [PATCH 01/10] Feat: Add custom group assignment option and update assignment intent handling --- src/pages/endpoint/applications/list/index.js | 95 ++++++++++++++++++- 1 file changed, 93 insertions(+), 2 deletions(-) diff --git a/src/pages/endpoint/applications/list/index.js b/src/pages/endpoint/applications/list/index.js index 9822447c7151..9321135e954b 100644 --- a/src/pages/endpoint/applications/list/index.js +++ b/src/pages/endpoint/applications/list/index.js @@ -1,13 +1,33 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; import { CippApiDialog } from "/src/components/CippComponents/CippApiDialog.jsx"; -import { GlobeAltIcon, TrashIcon, UserIcon } from "@heroicons/react/24/outline"; +import { GlobeAltIcon, TrashIcon, UserIcon, UserGroupIcon } from "@heroicons/react/24/outline"; import { LaptopMac, Sync } from "@mui/icons-material"; import { CippApplicationDeployDrawer } from "/src/components/CippComponents/CippApplicationDeployDrawer"; import { Button, Box } from "@mui/material"; import { useSettings } from "/src/hooks/use-settings.js"; import { useDialog } from "/src/hooks/use-dialog.js"; +const assignmentIntentOptions = [ + { label: "Required", value: "Required" }, + { label: "Available", value: "Available" }, + { label: "Available without enrollment", value: "AvailableWithoutEnrollment" }, + { label: "Uninstall", value: "Uninstall" }, +]; + +const assignmentModeOptions = [ + { label: "Replace existing assignments", value: "replace" }, + { label: "Append to existing assignments", value: "append" }, +]; + +const getAppAssignmentSettingsType = (odataType) => { + if (!odataType || typeof odataType !== "string") { + return undefined; + } + + return odataType.replace("#microsoft.graph.", "").replace(/App$/i, ""); +}; + const Page = () => { const pageTitle = "Applications"; const syncDialog = useDialog(); @@ -43,13 +63,84 @@ const Page = () => { type: "POST", url: "/api/ExecAssignApp", data: { - AssignTo: "!Both", + AssignTo: "!AllDevicesAndUsers", ID: "id", }, confirmText: "Are you sure you want to assign this app to all users and devices?", icon: , color: "info", }, + { + label: "Assign to Custom Group", + type: "POST", + url: "/api/ExecAssignApp", + icon: , + color: "info", + confirmText: + "Select the target groups and intent for this application assignment. Assignments are scoped to the current tenant context.", + fields: [ + { + type: "autoComplete", + name: "groupTargets", + label: "Group(s)", + multiple: true, + creatable: false, + allowResubmit: true, + validators: { required: "Please select at least one group" }, + api: { + url: "/api/ListGraphRequest", + dataKey: "Results", + queryKey: `ListAppAssignmentGroups-${tenant}`, + labelField: (group) => + group.id ? `${group.displayName} (${group.id})` : group.displayName, + valueField: "id", + addedField: { + description: "description", + }, + data: { + Endpoint: "groups", + manualPagination: true, + $select: "id,displayName,description", + $orderby: "displayName", + $top: 999, + $count: true, + }, + }, + }, + { + type: "radio", + name: "assignmentIntent", + label: "Assignment intent", + options: assignmentIntentOptions, + defaultValue: "Required", + validators: { required: "Select an assignment intent" }, + helperText: + "Available assigns to Company Portal, Required installs automatically, Uninstall removes the app, Available without enrollment exposes it without device enrollment.", + }, + { + type: "radio", + name: "assignmentMode", + label: "Assignment mode", + options: assignmentModeOptions, + defaultValue: "replace", + helperText: + "Replace will overwrite existing assignments. Append keeps current assignments and adds the selected groups/intents.", + }, + ], + customDataformatter: (row, action, formData) => { + const selectedGroups = Array.isArray(formData?.groupTargets) ? formData.groupTargets : []; + const tenantFilterValue = tenant === "AllTenants" && row?.Tenant ? row.Tenant : tenant; + return { + tenantFilter: tenantFilterValue, + ID: row?.id, + GroupIds: selectedGroups.map((group) => group.value).filter(Boolean), + GroupNames: selectedGroups.map((group) => group.label).filter(Boolean), + Intent: formData?.assignmentIntent || "Required", + AssignmentMode: formData?.assignmentMode || "replace", + AppType: getAppAssignmentSettingsType(row?.["@odata.type"]), + }; + }, + }, { label: "Delete Application", type: "POST", From c08f43dfe5f089a42903b8fae0b3090defb81f19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Mon, 24 Nov 2025 23:15:00 +0100 Subject: [PATCH 02/10] Feat: Enhance app assignment dialogs with intent selection and updated confirmation messages --- src/pages/endpoint/applications/list/index.js | 44 +++++++++++++++++-- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/src/pages/endpoint/applications/list/index.js b/src/pages/endpoint/applications/list/index.js index 9321135e954b..60ce25dd9f83 100644 --- a/src/pages/endpoint/applications/list/index.js +++ b/src/pages/endpoint/applications/list/index.js @@ -42,7 +42,19 @@ const Page = () => { AssignTo: "!AllUsers", ID: "id", }, - confirmText: "Are you sure you want to assign this app to all users?", + fields: [ + { + type: "radio", + name: "Intent", + label: "Assignment intent", + options: assignmentIntentOptions, + defaultValue: "Required", + validators: { required: "Select an assignment intent" }, + helperText: + "Available assigns to Company Portal, Required installs automatically, Uninstall removes the app, Available without enrollment exposes it without device enrollment.", + }, + ], + confirmText: 'Are you sure you want to assign "[displayName]" to all users?', icon: , color: "info", }, @@ -54,7 +66,19 @@ const Page = () => { AssignTo: "!AllDevices", ID: "id", }, - confirmText: "Are you sure you want to assign this app to all devices?", + fields: [ + { + type: "radio", + name: "Intent", + label: "Assignment intent", + options: assignmentIntentOptions, + defaultValue: "Required", + validators: { required: "Select an assignment intent" }, + helperText: + "Available assigns to Company Portal, Required installs automatically, Uninstall removes the app, Available without enrollment exposes it without device enrollment.", + }, + ], + confirmText: 'Are you sure you want to assign "[displayName]" to all devices?', icon: , color: "info", }, @@ -66,7 +90,19 @@ const Page = () => { AssignTo: "!AllDevicesAndUsers", ID: "id", }, - confirmText: "Are you sure you want to assign this app to all users and devices?", + fields: [ + { + type: "radio", + name: "Intent", + label: "Assignment intent", + options: assignmentIntentOptions, + defaultValue: "Required", + validators: { required: "Select an assignment intent" }, + helperText: + "Available assigns to Company Portal, Required installs automatically, Uninstall removes the app, Available without enrollment exposes it without device enrollment.", + }, + ], + confirmText: 'Are you sure you want to assign "[displayName]" to all users and devices?', icon: , color: "info", }, @@ -148,7 +184,7 @@ const Page = () => { data: { ID: "id", }, - confirmText: "Are you sure you want to delete this application?", + confirmText: 'Are you sure you want to delete "[displayName]"?', icon: , color: "danger", }, From a11aa1fe9adb1b88e6ffcc361f8c33a629fc3a21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Tue, 25 Nov 2025 00:28:40 +0100 Subject: [PATCH 03/10] Feat: Add assignment mode radio options to app assignment dialogs --- src/pages/endpoint/applications/list/index.js | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/pages/endpoint/applications/list/index.js b/src/pages/endpoint/applications/list/index.js index 60ce25dd9f83..b562c998540b 100644 --- a/src/pages/endpoint/applications/list/index.js +++ b/src/pages/endpoint/applications/list/index.js @@ -53,6 +53,15 @@ const Page = () => { helperText: "Available assigns to Company Portal, Required installs automatically, Uninstall removes the app, Available without enrollment exposes it without device enrollment.", }, + { + type: "radio", + name: "assignmentMode", + label: "Assignment mode", + options: assignmentModeOptions, + defaultValue: "replace", + helperText: + "Replace will overwrite existing assignments. Append keeps current assignments and adds/overwrites only for the selected groups/intents.", + }, ], confirmText: 'Are you sure you want to assign "[displayName]" to all users?', icon: , @@ -77,6 +86,15 @@ const Page = () => { helperText: "Available assigns to Company Portal, Required installs automatically, Uninstall removes the app, Available without enrollment exposes it without device enrollment.", }, + { + type: "radio", + name: "assignmentMode", + label: "Assignment mode", + options: assignmentModeOptions, + defaultValue: "replace", + helperText: + "Replace will overwrite existing assignments. Append keeps current assignments and adds/overwrites only for the selected groups/intents.", + }, ], confirmText: 'Are you sure you want to assign "[displayName]" to all devices?', icon: , @@ -101,6 +119,15 @@ const Page = () => { helperText: "Available assigns to Company Portal, Required installs automatically, Uninstall removes the app, Available without enrollment exposes it without device enrollment.", }, + { + type: "radio", + name: "assignmentMode", + label: "Assignment mode", + options: assignmentModeOptions, + defaultValue: "replace", + helperText: + "Replace will overwrite existing assignments. Append keeps current assignments and adds/overwrites only for the selected groups/intents.", + }, ], confirmText: 'Are you sure you want to assign "[displayName]" to all users and devices?', icon: , @@ -160,7 +187,7 @@ const Page = () => { options: assignmentModeOptions, defaultValue: "replace", helperText: - "Replace will overwrite existing assignments. Append keeps current assignments and adds the selected groups/intents.", + "Replace will overwrite existing assignments. Append keeps current assignments and adds/overwrites only for the selected groups/intents.", }, ], customDataformatter: (row, action, formData) => { From 5015340d2ae2df0753d57e75c265a1820822a066 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Tue, 25 Nov 2025 00:32:15 +0100 Subject: [PATCH 04/10] Feat: Update confirmation message with displayName --- src/pages/endpoint/applications/list/index.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pages/endpoint/applications/list/index.js b/src/pages/endpoint/applications/list/index.js index b562c998540b..4de87bd7d89d 100644 --- a/src/pages/endpoint/applications/list/index.js +++ b/src/pages/endpoint/applications/list/index.js @@ -139,8 +139,7 @@ const Page = () => { url: "/api/ExecAssignApp", icon: , color: "info", - confirmText: - "Select the target groups and intent for this application assignment. Assignments are scoped to the current tenant context.", + confirmText: 'Select the target groups and intent for "[displayName]".', fields: [ { type: "autoComplete", From abcd498514518ef02ef22c12eb209e6b668bfb19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= <31723128+kris6673@users.noreply.github.com> Date: Tue, 25 Nov 2025 12:43:37 +0100 Subject: [PATCH 05/10] Add new columns to simpleColumns array --- src/pages/endpoint/applications/list/index.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/pages/endpoint/applications/list/index.js b/src/pages/endpoint/applications/list/index.js index 4de87bd7d89d..6205f1a8f633 100644 --- a/src/pages/endpoint/applications/list/index.js +++ b/src/pages/endpoint/applications/list/index.js @@ -236,8 +236,10 @@ const Page = () => { const simpleColumns = [ "displayName", "publishingState", - "installCommandLine", - "uninstallCommandLine", + "isAssigned", + "lastModifiedDateTime", + "createdDateTime", + "applicableDeviceType", ]; return ( From 0243afc7e1d8dfa7e11a5bff96ff6b4617c5ac04 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 25 Nov 2025 12:06:39 -0500 Subject: [PATCH 06/10] fix group template display --- src/pages/tenant/manage/applied-standards.js | 99 +++++++++++++++++++- 1 file changed, 97 insertions(+), 2 deletions(-) diff --git a/src/pages/tenant/manage/applied-standards.js b/src/pages/tenant/manage/applied-standards.js index e19e38b00bbc..d863bda714fd 100644 --- a/src/pages/tenant/manage/applied-standards.js +++ b/src/pages/tenant/manage/applied-standards.js @@ -408,6 +408,97 @@ const Page = () => { } } }); + } else if (standardKey === "GroupTemplate") { + // GroupTemplate structure has groupTemplate array and action array at the top level + const groupTemplates = standardConfig.groupTemplate || []; + const actions = standardConfig.action || []; + const standardId = `standards.GroupTemplate`; + const standardInfo = standards.find((s) => s.name === standardId); + + // Find the tenant's value for this template + const currentTenantStandard = currentTenantData.find( + (s) => s.standardId === standardId + ); + const standardObject = currentTenantObj?.[standardId]; + const directStandardValue = standardObject?.Value; + let isCompliant = false; + + // For GroupTemplate, the value is true if compliant + if (directStandardValue === true) { + isCompliant = true; + } else if ( + directStandardValue !== undefined && + typeof directStandardValue !== "object" + ) { + isCompliant = true; + } else if (currentTenantStandard) { + isCompliant = currentTenantStandard.value === true; + } + + // Build a list of all group names with their types + const groupList = groupTemplates + .map((groupTemplate) => { + const rawGroupType = ( + groupTemplate.rawData?.groupType || "generic" + ).toLowerCase(); + let prettyGroupType = "Generic"; + + if (rawGroupType.includes("dynamicdistribution")) { + prettyGroupType = "Dynamic Distribution Group"; + } else if (rawGroupType.includes("dynamic")) { + prettyGroupType = "Dynamic Security Group"; + } else if (rawGroupType.includes("azurerole")) { + prettyGroupType = "Azure Role-Assignable Group"; + } else if ( + rawGroupType.includes("m365") || + rawGroupType.includes("unified") || + rawGroupType.includes("microsoft") + ) { + prettyGroupType = "Microsoft 365 Group"; + } else if ( + rawGroupType.includes("distribution") || + rawGroupType.includes("mail") + ) { + prettyGroupType = "Distribution Group"; + } else if ( + rawGroupType.includes("security") || + rawGroupType === "mail-enabled security" + ) { + prettyGroupType = "Security Group"; + } else if (rawGroupType.includes("generic")) { + prettyGroupType = "Security Group"; + } + + const groupName = + groupTemplate.label || groupTemplate.rawData?.displayName || "Unknown Group"; + return `- ${groupName} (${prettyGroupType})`; + }) + .join("\n"); + + // Create a single standard entry for all groups + const templateSettings = { + Groups: groupList, + }; + + allStandards.push({ + standardId, + standardName: `Group Templates`, + currentTenantValue: + standardObject !== undefined + ? { + Value: directStandardValue, + LastRefresh: standardObject?.LastRefresh, + } + : currentTenantStandard?.value, + standardValue: templateSettings, + complianceStatus: isCompliant ? "Compliant" : "Non-Compliant", + complianceDetails: standardInfo?.docsDescription || standardInfo?.helpText || "", + standardDescription: standardInfo?.helpText || "", + standardImpact: standardInfo?.impact || "Medium Impact", + standardImpactColour: standardInfo?.impactColour || "warning", + templateName: selectedTemplate?.templateName || "Standard Template", + templateActions: actions, + }); } else { // Regular handling for other standards const standardId = `standards.${standardKey}`; @@ -1129,14 +1220,18 @@ const Page = () => { typeof standard.standardValue === "object" && Object.keys(standard.standardValue).length > 0 ? ( Object.entries(standard.standardValue).map(([key, value]) => ( - + {key}: - + {typeof value === "object" && value !== null ? value?.label || JSON.stringify(value) : value === true From ce352c61290fc4ac2bba2e10d3dc3c703b8d12fc Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 25 Nov 2025 13:12:49 -0500 Subject: [PATCH 07/10] Refine template API call and compliance check logic Pass templateId in the API call to list standard templates and update the queryKey to include templateId for better cache management. Simplify compliance determination logic by checking currentTenantStandard.value directly. --- src/pages/tenant/manage/applied-standards.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/pages/tenant/manage/applied-standards.js b/src/pages/tenant/manage/applied-standards.js index d863bda714fd..3c86e87632c3 100644 --- a/src/pages/tenant/manage/applied-standards.js +++ b/src/pages/tenant/manage/applied-standards.js @@ -64,7 +64,10 @@ const Page = () => { const templateDetails = ApiGetCall({ url: `/api/listStandardTemplates`, - queryKey: `listStandardTemplates-reports`, + data: { + templateId: templateId, + }, + queryKey: `listStandardTemplates-reports-${templateId}`, }); // Normalize template data structure to always work with an array @@ -426,12 +429,7 @@ const Page = () => { // For GroupTemplate, the value is true if compliant if (directStandardValue === true) { isCompliant = true; - } else if ( - directStandardValue !== undefined && - typeof directStandardValue !== "object" - ) { - isCompliant = true; - } else if (currentTenantStandard) { + } else if (currentTenantStandard?.value) { isCompliant = currentTenantStandard.value === true; } From aa3cd115721d3217b7eb3ada5b052d585791905a Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Tue, 25 Nov 2025 22:13:47 +0100 Subject: [PATCH 08/10] Fix menu searching to only use permissions and not role. --- src/components/CippComponents/CippCentralSearch.jsx | 8 -------- src/layouts/index.js | 8 -------- 2 files changed, 16 deletions(-) diff --git a/src/components/CippComponents/CippCentralSearch.jsx b/src/components/CippComponents/CippCentralSearch.jsx index 11e4b1e67a62..523615e7c659 100644 --- a/src/components/CippComponents/CippCentralSearch.jsx +++ b/src/components/CippComponents/CippCentralSearch.jsx @@ -88,14 +88,6 @@ async function loadTabOptions() { */ function filterItemsByPermissionsAndRoles(items, userPermissions, userRoles) { return items.filter((item) => { - // Check roles if specified - if (item.roles && item.roles.length > 0) { - const hasRole = item.roles.some((requiredRole) => userRoles.includes(requiredRole)); - if (!hasRole) { - return false; - } - } - // Check permissions with pattern matching support if (item.permissions && item.permissions.length > 0) { const hasPermission = userPermissions?.some((userPerm) => { diff --git a/src/layouts/index.js b/src/layouts/index.js index 8508f200f287..e719e62772e8 100644 --- a/src/layouts/index.js +++ b/src/layouts/index.js @@ -114,14 +114,6 @@ export const Layout = (props) => { const filterItemsByRole = (items) => { return items .map((item) => { - // role - if (item.roles && item.roles.length > 0) { - const hasRole = item.roles.some((requiredRole) => userRoles.includes(requiredRole)); - if (!hasRole) { - return null; - } - } - // Check permission with pattern matching support if (item.permissions && item.permissions.length > 0) { const hasPermission = userPermissions?.some((userPerm) => { From 7e5a48bdf95dddceebc89d278ae73191a7f2a0af Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Tue, 25 Nov 2025 22:18:48 +0100 Subject: [PATCH 09/10] menu fix --- src/layouts/config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/layouts/config.js b/src/layouts/config.js index ce0edeb5cdd9..a335b5be498a 100644 --- a/src/layouts/config.js +++ b/src/layouts/config.js @@ -744,6 +744,7 @@ export const nativeMenuItems = [ "Tenant.Application.*", "Tenant.DomainAnalyser.*", "Exchange.Mailbox.*", + "CIPP.Scheduler.*", ], items: [ { From 41ff35e2111c8f4c569ab5084966d56194ff11db Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Tue, 25 Nov 2025 22:22:36 +0100 Subject: [PATCH 10/10] version up --- public/version.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/version.json b/public/version.json index 1194adaf2480..f3db8faafeda 100644 --- a/public/version.json +++ b/public/version.json @@ -1,3 +1,3 @@ { - "version": "8.7.1" -} \ No newline at end of file + "version": "8.7.2" +}