From f88e0442df3dc3d73db1ff56231a8ad756ce3c0c Mon Sep 17 00:00:00 2001 From: James Tarran Date: Tue, 24 Mar 2026 13:13:34 +0000 Subject: [PATCH 1/4] Update licenses.js - Added form dropdown to license exclusion flow --- src/pages/cipp/settings/licenses.js | 111 +++++++++++++++++++++++----- 1 file changed, 94 insertions(+), 17 deletions(-) diff --git a/src/pages/cipp/settings/licenses.js b/src/pages/cipp/settings/licenses.js index b5816cadde66..e2d34db85c9c 100644 --- a/src/pages/cipp/settings/licenses.js +++ b/src/pages/cipp/settings/licenses.js @@ -2,11 +2,15 @@ import tabOptions from "./tabOptions"; import { TabbedLayout } from "../../../layouts/TabbedLayout"; import { Layout as DashboardLayout } from "../../../layouts/index.js"; import { CippTablePage } from "../../../components/CippComponents/CippTablePage.jsx"; -import { Button, SvgIcon, Stack } from "@mui/material"; +import { Button, SvgIcon, Stack, Box } from "@mui/material"; import { TrashIcon } from "@heroicons/react/24/outline"; import { Add, RestartAlt } from "@mui/icons-material"; import { CippApiDialog } from "../../../components/CippComponents/CippApiDialog"; import { useDialog } from "../../../hooks/use-dialog"; +import CippFormComponent from "../../../components/CippComponents/CippFormComponent"; +import { CippFormCondition } from "../../../components/CippComponents/CippFormCondition"; +import M365Licenses from "../../../data/M365Licenses.json"; +import { useMemo } from "react"; const Page = () => { const pageTitle = "Excluded Licenses"; @@ -15,6 +19,22 @@ const Page = () => { const resetDialog = useDialog(); const simpleColumns = ["Product_Display_Name", "GUID"]; + // Deduplicate licenses by GUID and create autocomplete options + const licenseOptions = useMemo(() => { + const uniqueLicenses = new Map(); + M365Licenses.forEach((license) => { + if (!uniqueLicenses.has(license.GUID)) { + uniqueLicenses.set(license.GUID, { + label: license.Product_Display_Name, + value: license.GUID, + }); + } + }); + return Array.from(uniqueLicenses.values()).sort((a, b) => + a.label.localeCompare(b.label) + ); + }, []); + const actions = [ { label: "Delete Exclusion", @@ -81,30 +101,87 @@ const Page = () => { { + if (formData.advancedMode) { + return { + Action: "AddExclusion", + GUID: formData.GUID, + SKUName: formData.SKUName, + }; + } else { + return { + Action: "AddExclusion", + GUID: formData.selectedLicense?.value, + SKUName: formData.selectedLicense?.label, + }; + } + }, }} - /> + > + {({ formHook }) => ( + <> + + + + + + + + + + + + + + + + )} + Date: Tue, 24 Mar 2026 13:49:01 +0000 Subject: [PATCH 2/4] Update licenses.js --- src/pages/cipp/settings/licenses.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/pages/cipp/settings/licenses.js b/src/pages/cipp/settings/licenses.js index e2d34db85c9c..f2754636bfb7 100644 --- a/src/pages/cipp/settings/licenses.js +++ b/src/pages/cipp/settings/licenses.js @@ -9,6 +9,7 @@ import { CippApiDialog } from "../../../components/CippComponents/CippApiDialog" import { useDialog } from "../../../hooks/use-dialog"; import CippFormComponent from "../../../components/CippComponents/CippFormComponent"; import { CippFormCondition } from "../../../components/CippComponents/CippFormCondition"; +import { ApiGetCall } from "../../../api/ApiCall"; import M365Licenses from "../../../data/M365Licenses.json"; import { useMemo } from "react"; @@ -19,7 +20,16 @@ const Page = () => { const resetDialog = useDialog(); const simpleColumns = ["Product_Display_Name", "GUID"]; - // Deduplicate licenses by GUID and create autocomplete options + const excludedLicenses = ApiGetCall({ + url: "/api/ListExcludedLicenses", + queryKey: "ExcludedLicenses", + }); + + const excludedGuids = useMemo( + () => excludedLicenses.data?.Results?.map((license) => license.GUID) || [], + [excludedLicenses.data] + ); + const licenseOptions = useMemo(() => { const uniqueLicenses = new Map(); M365Licenses.forEach((license) => { @@ -148,6 +158,7 @@ const Page = () => { name="selectedLicense" label="Select License" options={licenseOptions} + removeOptions={excludedGuids} formControl={formHook} multiple={false} validators={{ required: "Please select a license" }} From fe4fd23a39c7041994abf9e6c69ae75cfcae595d Mon Sep 17 00:00:00 2001 From: James Tarran Date: Tue, 24 Mar 2026 14:10:18 +0000 Subject: [PATCH 3/4] Update licenses.js --- src/pages/cipp/settings/licenses.js | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/pages/cipp/settings/licenses.js b/src/pages/cipp/settings/licenses.js index f2754636bfb7..b785792fc97f 100644 --- a/src/pages/cipp/settings/licenses.js +++ b/src/pages/cipp/settings/licenses.js @@ -25,15 +25,13 @@ const Page = () => { queryKey: "ExcludedLicenses", }); - const excludedGuids = useMemo( - () => excludedLicenses.data?.Results?.map((license) => license.GUID) || [], - [excludedLicenses.data] - ); - const licenseOptions = useMemo(() => { + const excludedGuids = new Set( + excludedLicenses.data?.Results?.map((license) => license.GUID) || [] + ); const uniqueLicenses = new Map(); M365Licenses.forEach((license) => { - if (!uniqueLicenses.has(license.GUID)) { + if (!uniqueLicenses.has(license.GUID) && !excludedGuids.has(license.GUID)) { uniqueLicenses.set(license.GUID, { label: license.Product_Display_Name, value: license.GUID, @@ -43,7 +41,7 @@ const Page = () => { return Array.from(uniqueLicenses.values()).sort((a, b) => a.label.localeCompare(b.label) ); - }, []); + }, [excludedLicenses.data]); const actions = [ { @@ -158,7 +156,6 @@ const Page = () => { name="selectedLicense" label="Select License" options={licenseOptions} - removeOptions={excludedGuids} formControl={formHook} multiple={false} validators={{ required: "Please select a license" }} From ff1f20537c7e5a494ca37f3a42e57380319916cc Mon Sep 17 00:00:00 2001 From: James Tarran Date: Wed, 25 Mar 2026 10:43:29 +0000 Subject: [PATCH 4/4] Update licenses.js --- src/pages/cipp/settings/licenses.js | 83 ++++++++++++++++------------- 1 file changed, 45 insertions(+), 38 deletions(-) diff --git a/src/pages/cipp/settings/licenses.js b/src/pages/cipp/settings/licenses.js index b785792fc97f..d734f7eac437 100644 --- a/src/pages/cipp/settings/licenses.js +++ b/src/pages/cipp/settings/licenses.js @@ -9,9 +9,9 @@ import { CippApiDialog } from "../../../components/CippComponents/CippApiDialog" import { useDialog } from "../../../hooks/use-dialog"; import CippFormComponent from "../../../components/CippComponents/CippFormComponent"; import { CippFormCondition } from "../../../components/CippComponents/CippFormCondition"; -import { ApiGetCall } from "../../../api/ApiCall"; -import M365Licenses from "../../../data/M365Licenses.json"; -import { useMemo } from "react"; +import M365LicensesDefault from "../../../data/M365Licenses.json"; +import M365LicensesAdditional from "../../../data/M365Licenses-additional.json"; +import { useMemo, useCallback } from "react"; const Page = () => { const pageTitle = "Excluded Licenses"; @@ -20,28 +20,33 @@ const Page = () => { const resetDialog = useDialog(); const simpleColumns = ["Product_Display_Name", "GUID"]; - const excludedLicenses = ApiGetCall({ - url: "/api/ListExcludedLicenses", - queryKey: "ExcludedLicenses", - }); - - const licenseOptions = useMemo(() => { - const excludedGuids = new Set( - excludedLicenses.data?.Results?.map((license) => license.GUID) || [] - ); + const allLicenseOptions = useMemo(() => { + const allLicenses = [...M365LicensesDefault, ...M365LicensesAdditional]; const uniqueLicenses = new Map(); - M365Licenses.forEach((license) => { - if (!uniqueLicenses.has(license.GUID) && !excludedGuids.has(license.GUID)) { - uniqueLicenses.set(license.GUID, { - label: license.Product_Display_Name, - value: license.GUID, - }); + + allLicenses.forEach((license) => { + if (license.GUID && license.Product_Display_Name) { + if (!uniqueLicenses.has(license.GUID)) { + uniqueLicenses.set(license.GUID, { + label: license.Product_Display_Name, + value: license.GUID, + }); + } } }); - return Array.from(uniqueLicenses.values()).sort((a, b) => - a.label.localeCompare(b.label) - ); - }, [excludedLicenses.data]); + + const options = Array.from(uniqueLicenses.values()); + const nameCounts = {}; + options.forEach((opt) => { + nameCounts[opt.label] = (nameCounts[opt.label] || 0) + 1; + }); + + return options + .map((opt) => + nameCounts[opt.label] > 1 ? { ...opt, label: `${opt.label} (${opt.value})` } : opt + ) + .sort((a, b) => a.label.localeCompare(b.label)); + }, []); const actions = [ { @@ -93,6 +98,21 @@ const Page = () => { actions: actions, }; + const addExclusionFormatter = useCallback((row, action, formData) => { + if (formData.advancedMode) { + return { + Action: "AddExclusion", + GUID: formData.GUID, + SKUName: formData.SKUName, + }; + } + return { + Action: "AddExclusion", + GUID: formData.selectedLicense?.value, + SKUName: formData.selectedLicense?.label, + }; + }, []); + return ( <> { data: { Action: "!AddExclusion" }, replacementBehaviour: "removeNulls", relatedQueryKeys: ["ExcludedLicenses"], - customDataformatter: (row, action, formData) => { - if (formData.advancedMode) { - return { - Action: "AddExclusion", - GUID: formData.GUID, - SKUName: formData.SKUName, - }; - } else { - return { - Action: "AddExclusion", - GUID: formData.selectedLicense?.value, - SKUName: formData.selectedLicense?.label, - }; - } - }, + customDataformatter: addExclusionFormatter, }} > {({ formHook }) => ( @@ -155,9 +161,10 @@ const Page = () => { type="autoComplete" name="selectedLicense" label="Select License" - options={licenseOptions} + options={allLicenseOptions} formControl={formHook} multiple={false} + creatable={false} validators={{ required: "Please select a license" }} />