Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
"@tiptap/extension-table": "^3.4.1",
"@tiptap/pm": "^3.4.1",
"@tiptap/react": "^3.4.1",
"@tiptap/starter-kit": "^3.4.1",
"@tiptap/starter-kit": "^3.19.0",
"@uiw/react-json-view": "^2.0.0-alpha.30",
"@vvo/tzdb": "^6.198.0",
"apexcharts": "5.3.5",
Expand All @@ -62,15 +62,15 @@
"formik": "2.4.6",
"gray-matter": "4.0.3",
"i18next": "25.5.2",
"javascript-time-ago": "^2.5.11",
"javascript-time-ago": "^2.6.2",
"jspdf": "^4.1.0",
"jspdf-autotable": "^5.0.2",
"leaflet": "^1.9.4",
"leaflet-defaulticon-compatibility": "^0.1.2",
"leaflet.markercluster": "^1.5.3",
"lodash.isequal": "4.5.0",
"material-react-table": "^3.0.1",
"monaco-editor": "^0.53.0",
"monaco-editor": "^0.55.1",
"mui-tiptap": "^1.14.0",
"next": "^16.1.2",
"nprogress": "0.2.0",
Expand Down
165 changes: 165 additions & 0 deletions src/components/CippComponents/CippReusableSettingsDeployDrawer.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import { useEffect, useState } from "react";
import { Button, Stack } from "@mui/material";
import { RocketLaunch } from "@mui/icons-material";
import { useForm, useWatch, useFormState } from "react-hook-form";
import { CippOffCanvas } from "./CippOffCanvas";
import { ApiGetCall, ApiPostCall } from "../../api/ApiCall";
import CippFormComponent from "./CippFormComponent";
import CippJsonView from "../CippFormPages/CippJSONView";
import { Grid } from "@mui/system";
import { CippApiResults } from "./CippApiResults";
import { useSettings } from "../../hooks/use-settings";
import { CippFormTenantSelector } from "./CippFormTenantSelector";
import { PermissionButton as PermissionAwareButton } from "../../utils/permissions";

export const CippReusableSettingsDeployDrawer = ({
buttonText = "Deploy Reusable Settings",
requiredPermissions = [],
PermissionButton = PermissionAwareButton,
}) => {
const [drawerVisible, setDrawerVisible] = useState(false);
const formControl = useForm({ mode: "onChange" });
const { isValid } = useFormState({ control: formControl.control });
const tenantFilter = useSettings()?.tenantFilter;
const selectedTemplate = useWatch({ control: formControl.control, name: "TemplateList" });
const rawJson = useWatch({ control: formControl.control, name: "rawJSON" });
const selectedTenants = useWatch({ control: formControl.control, name: "tenantFilter" });

const templates = ApiGetCall({ url: "/api/ListIntuneReusableSettingTemplates", queryKey: "ListIntuneReusableSettingTemplates" });

useEffect(() => {
if (templates.isSuccess && selectedTemplate?.value) {
const match = templates.data?.find((t) => t.GUID === selectedTemplate.value);
if (match) {
formControl.setValue("rawJSON", match.RawJSON || "");
formControl.setValue("TemplateId", match.GUID);
}
}
}, [templates.isSuccess, templates.data, selectedTemplate, formControl]);

const effectiveTenants = Array.isArray(selectedTenants) && selectedTenants.length > 0
? selectedTenants
: tenantFilter
? [tenantFilter]
: [];

const deploy = ApiPostCall({
urlFromData: true,
relatedQueryKeys: [
"ListIntuneReusableSettingTemplates",
`ListIntuneReusableSettings-${effectiveTenants.join(",")}`,
],
});

const handleSubmit = async () => {
const isFormValid = await formControl.trigger();
if (!isFormValid) {
return;
}
const values = formControl.getValues();
deploy.mutate({
url: "/api/AddIntuneReusableSetting",
data: {
tenantFilter: effectiveTenants,
TemplateId: values?.TemplateList?.value,
rawJSON: values?.rawJSON,
},
});
};

const handleCloseDrawer = () => {
setDrawerVisible(false);
formControl.reset();
deploy.reset();
};

const safeJson = () => {
if (!rawJson) return null;
try {
return JSON.parse(rawJson);
} catch (e) {
return null;
}
};

return (
<>
<PermissionButton
requiredPermissions={requiredPermissions}
onClick={() => setDrawerVisible(true)}
startIcon={<RocketLaunch />}
>
{buttonText}
</PermissionButton>
<CippOffCanvas
title="Deploy Reusable Settings"
visible={drawerVisible}
onClose={handleCloseDrawer}
size="lg"
footer={
<Stack direction="row" spacing={2}>
<Button
variant="contained"
color="primary"
onClick={handleSubmit}
disabled={deploy.isLoading || !isValid}
>
{deploy.isLoading ? "Deploying..." : "Deploy"}
</Button>
<Button variant="outlined" onClick={handleCloseDrawer}>
Close
</Button>
</Stack>
}
>
<Stack spacing={3}>
<CippFormTenantSelector
formControl={formControl}
name="tenantFilter"
required={true}
disableClearable={false}
allTenants={true}
preselectedEnabled={true}
type="multiple"
/>
<CippFormComponent
type="autoComplete"
name="TemplateList"
label="Choose a Reusable Settings Template"
isFetching={templates.isLoading}
multiple={false}
formControl={formControl}
options={
templates.isSuccess && Array.isArray(templates.data)
? templates.data.map((t) => ({
label:
t.displayName ||
t.DisplayName ||
t.templateName ||
t.TemplateName ||
t.name ||
`Template ${t.GUID}`,
value: t.GUID,
}))
: []
}
validators={{ required: { value: true, message: "Template selection is required" } }}
/>
<Grid size={{ xs: 12 }}>
<CippFormComponent
type="json"
name="rawJSON"
label="Raw JSON"
formControl={formControl}
required
validators={{ required: "Raw JSON is required" }}
rows={12}
/>
</Grid>
<CippJsonView object={safeJson()} type="intune" />
<CippApiResults apiObject={deploy} />
</Stack>
</CippOffCanvas>
</>
);
};
99 changes: 92 additions & 7 deletions src/components/CippComponents/CippTemplateFieldRenderer.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from "react";
import React, { useMemo } from "react";
import { Typography, Divider } from "@mui/material";
import { Grid } from "@mui/system";
import CippFormComponent from "./CippFormComponent";
Expand All @@ -10,6 +10,15 @@ const CippTemplateFieldRenderer = ({
formControl,
templateType = "conditionalAccess",
}) => {
const intuneDefinitionMap = useMemo(() => {
const map = new Map();
(intuneCollection || []).forEach((def) => {
if (def?.id) {
map.set(def.id, def);
}
});
return map;
}, []);
// Default blacklisted fields with wildcard support
const defaultBlacklistedFields = [
"id",
Expand Down Expand Up @@ -253,6 +262,86 @@ const CippTemplateFieldRenderer = ({
return null;
}

// Render Intune group setting collections with child-friendly fields instead of raw [object Object]
if (
templateType === "intune" &&
key.toLowerCase() === "groupsettingcollectionvalue" &&
Array.isArray(value)
) {
return (
<Grid size={{ xs: 12 }} key={fieldPath}>
<Typography variant="h6" sx={{ mt: 2, mb: 1 }}>
{getCippTranslation(key)}
</Typography>
<Divider sx={{ mb: 2 }} />
<Grid container spacing={2}>
{value.map((groupEntry, groupIndex) => (
<Grid size={{ xs: 12 }} key={`${fieldPath}.${groupIndex}`}>
<Typography variant="subtitle1" sx={{ mt: 1, mb: 1 }}>
{`Entry ${groupIndex + 1}`}
</Typography>
<Grid container spacing={2}>
{(groupEntry?.children || []).map((child, childIndex) => {
const childPath = `${fieldPath}.${groupIndex}.children.${childIndex}`;
const intuneDefinition = intuneDefinitionMap.get(child?.settingDefinitionId);
const childLabel =
intuneDefinition?.displayName || child?.settingDefinitionId || `Child ${
childIndex + 1
}`;

if (child?.simpleSettingValue) {
return (
<Grid size={{ xs: 12, md: 6 }} key={childPath}>
<CippFormComponent
type="textField"
label={childLabel}
name={`${childPath}.simpleSettingValue.value`}
formControl={formControl}
includeSystemVariables={true}
helperText={child?.settingDefinitionId}
/>
</Grid>
);
}

if (child?.choiceSettingValue) {
const options =
intuneDefinition?.options?.map((option) => ({
label: option.displayName || option.id,
value: option.id,
})) || [];

return (
<Grid size={{ xs: 12, md: 6 }} key={childPath}>
<CippFormComponent
type="autoComplete"
label={childLabel}
name={`${childPath}.choiceSettingValue.value`}
formControl={formControl}
options={options}
multiple={false}
helperText={child?.settingDefinitionId}
/>
</Grid>
);
}

return (
<Grid size={{ xs: 12, md: 6 }} key={childPath}>
<Typography variant="body2" color="text.secondary" sx={{ fontStyle: "italic" }}>
Unsupported group entry type — edit in JSON if needed.
</Typography>
</Grid>
);
})}
</Grid>
</Grid>
))}
</Grid>
</Grid>
);
}

// Check for custom schema handling
const schemaField = schemaFields[key.toLowerCase()];
if (schemaField) {
Expand Down Expand Up @@ -299,9 +388,7 @@ const CippTemplateFieldRenderer = ({
// Handle different setting types
if (settingInstance.choiceSettingValue) {
// Find the setting definition in the intune collection
const intuneObj = intuneCollection.find(
(item) => item.id === settingInstance.settingDefinitionId
);
const intuneObj = intuneDefinitionMap.get(settingInstance.settingDefinitionId);

const label = intuneObj?.displayName || `Setting ${index + 1}`;
const options =
Expand All @@ -327,9 +414,7 @@ const CippTemplateFieldRenderer = ({

if (settingInstance.simpleSettingValue) {
// Find the setting definition in the intune collection
const intuneObj = intuneCollection.find(
(item) => item.id === settingInstance.settingDefinitionId
);
const intuneObj = intuneDefinitionMap.get(settingInstance.settingDefinitionId);

const label = intuneObj?.displayName || `Setting ${index + 1}`;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Grid } from "@mui/system";
import CippFormComponent from "/src/components/CippComponents/CippFormComponent";

const CippAddIntuneReusableSettingTemplateForm = ({ formControl }) => {
return (
<Grid container spacing={2}>
<CippFormComponent type="hidden" name="GUID" formControl={formControl} />

<Grid size={{ md: 6, xs: 12 }}>
<CippFormComponent
type="textField"
label="Display Name"
name="displayName"
required
formControl={formControl}
validators={{ required: "Display Name is required" }}
/>
</Grid>
<Grid size={{ md: 6, xs: 12 }}>
<CippFormComponent
type="textField"
label="Description"
name="description"
formControl={formControl}
/>
</Grid>

<Grid size={{ md: 6, xs: 12 }}>
<CippFormComponent
type="textField"
label="Package (optional)"
name="package"
formControl={formControl}
helperText="Optional metadata to group templates (e.g., internal package name)."
/>
</Grid>

<Grid size={{ xs: 12 }}>
<CippFormComponent
type="json"
label="Raw JSON"
name="rawJSON"
formControl={formControl}
required
validators={{ required: "Raw JSON is required" }}
helperText="Paste the reusablePolicySetting payload (Graph beta: deviceManagement/reusablePolicySettings)."
rows={12}
/>
</Grid>
</Grid>
);
};

export default CippAddIntuneReusableSettingTemplateForm;
Loading