diff --git a/.github/workflows/cipp_dev_build.yml b/.github/workflows/cipp_dev_build.yml
index 52758378212b..8f9def9f61b8 100644
--- a/.github/workflows/cipp_dev_build.yml
+++ b/.github/workflows/cipp_dev_build.yml
@@ -54,7 +54,7 @@ jobs:
# Upload to Azure Blob Storage
- name: Azure Blob Upload
- uses: LanceMcCarthy/Action-AzureBlobUpload@v3.8.0
+ uses: LanceMcCarthy/Action-AzureBlobUpload@v3.9.0
with:
connection_string: ${{ secrets.AZURE_CONNECTION_STRING }}
container_name: cipp
diff --git a/.github/workflows/cipp_frontend_build.yml b/.github/workflows/cipp_frontend_build.yml
index bf86646e19cd..900a1adb0992 100644
--- a/.github/workflows/cipp_frontend_build.yml
+++ b/.github/workflows/cipp_frontend_build.yml
@@ -47,7 +47,7 @@ jobs:
# Upload to Azure Blob Storage
- name: Azure Blob Upload
- uses: LanceMcCarthy/Action-AzureBlobUpload@v3.8.0
+ uses: LanceMcCarthy/Action-AzureBlobUpload@v3.9.0
with:
connection_string: ${{ secrets.AZURE_CONNECTION_STRING }}
container_name: cipp
diff --git a/package.json b/package.json
index 0b146367a28c..295eea0297e5 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "cipp",
- "version": "10.2.2",
+ "version": "10.2.4",
"author": "CIPP Contributors",
"homepage": "https://cipp.app/",
"bugs": {
@@ -51,7 +51,7 @@
"@tiptap/extension-table": "^3.19.0",
"@tiptap/pm": "^3.4.1",
"@tiptap/react": "^3.4.1",
- "@tiptap/starter-kit": "^3.20.0",
+ "@tiptap/starter-kit": "^3.20.1",
"@uiw/react-json-view": "^2.0.0-alpha.41",
"@vvo/tzdb": "^6.198.0",
"apexcharts": "5.3.5",
@@ -61,7 +61,7 @@
"export-to-csv": "^1.3.0",
"formik": "2.4.9",
"gray-matter": "4.0.3",
- "i18next": "25.8.13",
+ "i18next": "25.8.18",
"javascript-time-ago": "^2.6.2",
"jspdf": "^4.2.0",
"jspdf-autotable": "^5.0.7",
@@ -98,7 +98,7 @@
"react-redux": "9.2.0",
"react-syntax-highlighter": "^16.1.0",
"react-time-ago": "^7.3.3",
- "react-virtuoso": "^4.18.1",
+ "react-virtuoso": "^4.18.3",
"react-window": "^2.2.5",
"recharts": "^3.7.0",
"redux": "5.0.1",
@@ -110,7 +110,7 @@
"simplebar": "6.3.3",
"simplebar-react": "3.3.2",
"stylis-plugin-rtl": "2.1.1",
- "typescript": "5.9.2",
+ "typescript": "5.9.3",
"yup": "1.7.1"
},
"devDependencies": {
diff --git a/public/version.json b/public/version.json
index 2550102c4f0b..0e0e9967ccb6 100644
--- a/public/version.json
+++ b/public/version.json
@@ -1,3 +1,3 @@
{
- "version": "10.2.2"
+ "version": "10.2.4"
}
\ No newline at end of file
diff --git a/src/components/CippComponents/CippApiDialog.jsx b/src/components/CippComponents/CippApiDialog.jsx
index 4ba2060a9410..da48eb7e4ccd 100644
--- a/src/components/CippComponents/CippApiDialog.jsx
+++ b/src/components/CippComponents/CippApiDialog.jsx
@@ -52,7 +52,7 @@ export const CippApiDialog = (props) => {
useEffect(() => {
if (createDialog.open) {
setIsFormSubmitted(false);
- formHook.reset(defaultvalues || {});
+ formHook.reset(typeof defaultvalues === "function" ? defaultvalues(row) : defaultvalues || {});
}
}, [createDialog.open, defaultvalues]);
diff --git a/src/components/CippComponents/CippAutopilotProfileDrawer.jsx b/src/components/CippComponents/CippAutopilotProfileDrawer.jsx
index 7edc95b46b60..fa1ac63baa52 100644
--- a/src/components/CippComponents/CippAutopilotProfileDrawer.jsx
+++ b/src/components/CippComponents/CippAutopilotProfileDrawer.jsx
@@ -109,8 +109,8 @@ export const CippAutopilotProfileDrawer = ({
{createProfile.isLoading
? "Creating..."
: createProfile.isSuccess
- ? "Create Another"
- : "Create Profile"}
+ ? "Create Another"
+ : "Create Profile"}
+
}
>
- Choose your password style. Classic passwords are a combination of letters and symbols.
- Correct-Battery-Horse style is a passphrase, which is easier to remember and more secure
- than classic passwords.
+ Configure password generation settings including type, length, character sets, and
+ passphrase options.
+
+ {hasError && !isLoading && (
+
+ {getErrorMessage()}
+
+ )}
);
};
diff --git a/src/components/CippStandards/CippStandardDialog.jsx b/src/components/CippStandards/CippStandardDialog.jsx
index ffe959653259..6ebe6362930c 100644
--- a/src/components/CippStandards/CippStandardDialog.jsx
+++ b/src/components/CippStandards/CippStandardDialog.jsx
@@ -83,7 +83,7 @@ const StandardCard = memo(
observer.disconnect();
}
},
- { threshold: 0.1 }
+ { threshold: 0.1 },
);
const currentRef = document.getElementById(`standard-card-${standard.name}`);
@@ -233,8 +233,8 @@ const StandardCard = memo(
standard.impact === "High Impact"
? "error"
: standard.impact === "Medium Impact"
- ? "warning"
- : "info"
+ ? "warning"
+ : "info"
}
/>
{expanded && standard.recommendedBy?.length > 0 && (
@@ -334,7 +334,7 @@ const StandardCard = memo(
// If we get here, nothing important changed, skip re-render
return true;
- }
+ },
);
StandardCard.displayName = "StandardCard";
@@ -342,7 +342,7 @@ StandardCard.displayName = "StandardCard";
// Virtualized grid to handle large numbers of standards efficiently
const VirtualizedStandardGrid = memo(({ items, renderItem }) => {
const [itemsPerRow, setItemsPerRow] = useState(() =>
- window.innerWidth > 960 ? 4 : window.innerWidth > 600 ? 2 : 1
+ window.innerWidth > 960 ? 4 : window.innerWidth > 600 ? 2 : 1,
);
// Handle window resize for responsive grid
@@ -470,8 +470,8 @@ const CompactStandardList = memo(
standard.impact === "High Impact"
? "error"
: standard.impact === "Medium Impact"
- ? "warning"
- : "info"
+ ? "warning"
+ : "info"
}
/>
@@ -618,7 +618,7 @@ const CompactStandardList = memo(
})}
);
- }
+ },
);
CompactStandardList.displayName = "CompactStandardList";
@@ -851,7 +851,7 @@ const CippStandardDialog = ({
showOnlyNew,
statusFilter,
selectedStandards,
- ]
+ ],
);
// Enhanced sort function
@@ -899,7 +899,7 @@ const CippStandardDialog = ({
return 0;
});
},
- [sortBy, sortOrder]
+ [sortBy, sortOrder],
);
// Optimize handleAddClick to be more performant
@@ -914,7 +914,7 @@ const CippStandardDialog = ({
}, 100);
});
},
- [handleAddMultipleStandard]
+ [handleAddMultipleStandard],
);
// Optimize search debounce with a higher timeout for better performance
@@ -922,7 +922,7 @@ const CippStandardDialog = ({
debounce((query) => {
setSearchQuery(query.trim());
}, 350), // Increased debounce time for better performance
- [setSearchQuery]
+ [setSearchQuery],
);
// Only process visible categories on demand to improve performance
@@ -935,7 +935,7 @@ const CippStandardDialog = ({
setLocalSearchQuery(value);
handleSearchQueryChange(value);
},
- [handleSearchQueryChange]
+ [handleSearchQueryChange],
);
// Clear all filters
@@ -992,7 +992,7 @@ const CippStandardDialog = ({
(standard) => {
const item = allItems.find((item) => item.standard.name === standard.name);
return item;
- }
+ },
);
setProcessedItems(sortedAllItems);
@@ -1028,7 +1028,7 @@ const CippStandardDialog = ({
isButtonDisabled={isButtonDisabled}
/>
),
- [selectedStandards, handleToggleSingleStandard, handleAddClick, isButtonDisabled]
+ [selectedStandards, handleToggleSingleStandard, handleAddClick, isButtonDisabled],
);
// Count active filters
@@ -1047,6 +1047,7 @@ const CippStandardDialog = ({
onClose={handleClose}
maxWidth="xxl"
fullWidth
+ fullScreen
keepMounted={false}
TransitionProps={{
onExited: () => {
@@ -1056,15 +1057,12 @@ const CippStandardDialog = ({
}}
PaperProps={{
sx: {
- minWidth: "720px",
- maxHeight: "90vh",
- height: "90vh",
display: "flex",
flexDirection: "column",
},
}}
>
- Select a Standard to Add
+ Select a Standard to Add
{
setSavedItem(id);
@@ -271,289 +285,308 @@ const CippStandardsSideBar = ({
};
return (
-
-
-
-
-
- {isDriftMode ? "About Drift Templates" : "About Standard Templates"}
-
- {isDriftMode ? (
-
-
- Drift templates provide continuous monitoring of tenant configurations to detect
- unauthorized changes. Each tenant can only have one drift template applied at a time.
-
-
- Remediation Options:
-
-
- • Automatic Remediation: Immediately reverts unauthorized changes
- back to the template configuration
-
• Manual Remediation: Sends email notifications for review,
- allowing you to accept or deny detected changes
-
-
- Key Features:
-
-
- • Monitors all security standards, Conditional Access policies, and Intune policies
-
- • Detects changes made outside of CIPP
-
- • Configurable webhook and email notifications
-
• Granular control over deviation acceptance
-
-
- ) : (
-
-
- Standard templates can be applied to multiple tenants and allow overlapping
- configurations with intelligent merging based on specificity and timing.
-
-
-
- Merge Priority (Specificity):
-
-
- 1. Individual Tenant - Highest priority, overrides all others
-
- 2. Tenant Group - Overrides "All Tenants" settings
-
- 3. All Tenants - Lowest priority, default baseline
-
-
-
- Conflict Resolution:
-
-
- When multiple standards target the same scope (e.g., two tenant-specific templates),
- the most recently created template takes precedence.
-
-
-
- Example: An "All Tenants" template enables audit log retention for 90
- days, but you need 365 days for one specific tenant. Create a tenant-specific template
- with 365-day retention - it will override the global setting for that tenant only.
-
-
- )}
-
-
- {/* Hidden field to mark drift templates */}
- {isDriftMode && (
-
- )}
-
-
-
-
-
-
- {/* Show drift error */}
- {isDriftMode && driftError && {driftError}}
-
- {(watchForm.tenantFilter?.some(
- (tenant) => tenant.value === "AllTenants" || tenant.type === "Group",
- ) ||
- (watchForm.excludedTenants && watchForm.excludedTenants.length > 0)) && (
- <>
-
-
- >
- )}
- {/* Drift-specific fields */}
- {isDriftMode && (
- <>
-
-
-
-
-
- When enabled, all drift alert notifications (email, webhook, and PSA) will be
- disabled.
+ <>
+ setAboutOpen(false)}
+ size="sm"
+ >
+
+ {isDriftMode ? (
+
+
+ Drift templates provide continuous monitoring of tenant configurations to detect
+ unauthorized changes. Each tenant can only have one drift template applied at a
+ time.
+
+
+ Remediation Options:
+
+
+ • Automatic Remediation: Immediately reverts unauthorized changes
+ back to the template configuration
+
• Manual Remediation: Sends email notifications for review,
+ allowing you to accept or deny detected changes
+
+
+ Key Features:
- >
+
+ • Monitors all security standards, Conditional Access policies, and Intune policies
+
+ • Detects changes made outside of CIPP
+
+ • Configurable webhook and email notifications
+
• Granular control over deviation acceptance
+
+
+ ) : (
+
+
+ Standard templates can be applied to multiple tenants and allow overlapping
+ configurations with intelligent merging based on specificity and timing.
+
+
+ Merge Priority (Specificity):
+
+
+ 1. Individual Tenant - Highest priority, overrides all others
+
+ 2. Tenant Group - Overrides "All Tenants" settings
+
+ 3. All Tenants - Lowest priority, default baseline
+
+
+ Conflict Resolution:
+
+
+ When multiple standards target the same scope (e.g., two tenant-specific templates),
+ the most recently created template takes precedence.
+
+
+ Example: An "All Tenants" template enables audit log retention for
+ 90 days, but you need 365 days for one specific tenant. Create a tenant-specific
+ template with 365-day retention - it will override the global setting for that
+ tenant only.
+
+
)}
- {/* Hide schedule options in drift mode */}
- {!isDriftMode && (
- <>
- {updatedAt.date && (
- <>
-
- Last Updated by {updatedAt?.user}
-
- >
- )}
+
+
+
+
+ setAboutOpen(true)} color="primary">
+
+
+
+ }
+ />
+
+
+
+ {/* Hidden field to mark drift templates */}
+ {isDriftMode && (
-
+
+
+
+
+
+ {/* Show drift error */}
+ {isDriftMode && driftError && {driftError}}
+
+ {(watchForm.tenantFilter?.some(
+ (tenant) => tenant.value === "AllTenants" || tenant.type === "Group",
+ ) ||
+ (watchForm.excludedTenants && watchForm.excludedTenants.length > 0)) && (
+ <>
+
+
+ >
+ )}
+ {/* Drift-specific fields */}
+ {isDriftMode && (
+ <>
+
+
+
+
+
+ When enabled, all drift alert notifications (email, webhook, and PSA) will be
+ disabled.
+
+ >
+ )}
+ {/* Hide schedule options in drift mode */}
+ {!isDriftMode && (
+ <>
+ {updatedAt.date && (
+ <>
+
+ Last Updated by {updatedAt?.user}
+
+ >
+ )}
+
+
+ This setting allows you to create this template and run it only by using "Run
+ Now".
+
+ >
+ )}
+
+
+ {/* Hide timeline/ticker in drift mode */}
+ {!isDriftMode && (
+ <>
+
+
+
- This setting allows you to create this template and run it only by using "Run Now".
-
- >
- )}
-
-
- {/* Hide timeline/ticker in drift mode */}
- {!isDriftMode && (
- <>
-
-
-
- {steps.map((step, index) => (
-
-
-
- {index < steps.length - 1 && }
-
- {step}
-
- ))}
-
-
- >
- )}
-
-
- {actions.map((action, index) => (
- {action.icon}}
- label={action.label}
- onClick={action.handler}
- disabled={
- !(watchForm.tenantFilter && watchForm.tenantFilter.length > 0) ||
- currentStep < 3 ||
- (isDriftMode && driftError)
- }
- />
- ))}
-
-
- dialogAfterEffect(data.id)}
- createDialog={createDialog}
- title="Add Standard"
- api={{
- confirmText: isDriftMode
- ? "This template will run automatically every 12 hours to detect drift. Are you sure you want to apply this Drift Template?"
- : watchForm.runManually
- ? "Are you sure you want to apply this standard? This template has been set to never run on a schedule. After saving the template you will have to run it manually."
- : "Are you sure you want to apply this standard? This will apply the template and run every 12 hours.",
- url: "/api/AddStandardsTemplate",
- type: "POST",
- replacementBehaviour: "removeNulls",
- data: {
- tenantFilter: "tenantFilter",
- excludedTenants: "excludedTenants",
- description: "description",
- templateName: "templateName",
- standards: "standards",
- ...(edit ? { GUID: "GUID" } : {}),
- ...(savedItem ? { GUID: savedItem } : {}),
- runManually: isDriftMode ? false : "runManually",
- isDriftTemplate: "isDriftTemplate",
- ...(isDriftMode
- ? {
- type: "drift",
- driftAlertWebhook: "driftAlertWebhook",
- driftAlertEmail: "driftAlertEmail",
- driftAlertDisableEmail: "driftAlertDisableEmail",
- }
- : {}),
- },
- }}
- row={formControl.getValues()}
- formControl={formControl}
- relatedQueryKeys={[
- "listStandardTemplates",
- "listStandards",
- `listStandardTemplates-${watchForm.GUID}`,
- "ListTenantAlignment-drift-validation",
- "ListTenantGroups-drift-validation",
- ]}
- />
-
+ {steps.map((step, index) => (
+
+
+
+ {index < steps.length - 1 && }
+
+ {step}
+
+ ))}
+
+
+ >
+ )}
+
+
+ {actions.map((action, index) => (
+ {action.icon}}
+ label={action.label}
+ onClick={action.handler}
+ disabled={
+ !(watchForm.tenantFilter && watchForm.tenantFilter.length > 0) ||
+ currentStep < 3 ||
+ (isDriftMode && driftError)
+ }
+ />
+ ))}
+
+
+ dialogAfterEffect(data.id)}
+ createDialog={createDialog}
+ title="Add Standard"
+ api={{
+ confirmText: isDriftMode
+ ? "This template will run automatically every 12 hours to detect drift. Are you sure you want to apply this Drift Template?"
+ : watchForm.runManually
+ ? "Are you sure you want to apply this standard? This template has been set to never run on a schedule. After saving the template you will have to run it manually."
+ : "Are you sure you want to apply this standard? This will apply the template and run every 12 hours.",
+ url: "/api/AddStandardsTemplate",
+ type: "POST",
+ replacementBehaviour: "removeNulls",
+ data: {
+ tenantFilter: "tenantFilter",
+ excludedTenants: "excludedTenants",
+ description: "description",
+ templateName: "templateName",
+ standards: "standards",
+ ...(edit ? { GUID: "GUID" } : {}),
+ ...(savedItem ? { GUID: savedItem } : {}),
+ runManually: isDriftMode ? false : "runManually",
+ isDriftTemplate: "isDriftTemplate",
+ ...(isDriftMode
+ ? {
+ type: "drift",
+ driftAlertWebhook: "driftAlertWebhook",
+ driftAlertEmail: "driftAlertEmail",
+ driftAlertDisableEmail: "driftAlertDisableEmail",
+ }
+ : {}),
+ },
+ }}
+ row={formControl.getValues()}
+ formControl={formControl}
+ relatedQueryKeys={[
+ "listStandardTemplates",
+ "listStandards",
+ `listStandardTemplates-${watchForm.GUID}`,
+ "ListTenantAlignment-drift-validation",
+ "ListTenantGroups-drift-validation",
+ ]}
+ />
+
+ >
);
};
diff --git a/src/components/CippWizard/CippWizardDialogContext.js b/src/components/CippWizard/CippWizardDialogContext.js
new file mode 100644
index 000000000000..2ff78f19fe1b
--- /dev/null
+++ b/src/components/CippWizard/CippWizardDialogContext.js
@@ -0,0 +1,10 @@
+import { createContext, useContext } from "react";
+
+/**
+ * When CippWizardPage is used in dialogMode, it provides this context with a
+ * reference to the DialogActions DOM node. CippWizardStepButtons checks for
+ * it and portals its navigation buttons there, keeping the main content area
+ * clean while anchoring controls at the bottom of the dialog.
+ */
+export const CippWizardDialogContext = createContext(null);
+export const useCippWizardDialog = () => useContext(CippWizardDialogContext);
diff --git a/src/components/CippWizard/CippWizardGroupTemplates.jsx b/src/components/CippWizard/CippWizardGroupTemplates.jsx
index 16e1bd61ef0d..2e1679808723 100644
--- a/src/components/CippWizard/CippWizardGroupTemplates.jsx
+++ b/src/components/CippWizard/CippWizardGroupTemplates.jsx
@@ -4,11 +4,12 @@ import CippFormComponent from "../CippComponents/CippFormComponent";
import { CippFormCondition } from "../CippComponents/CippFormCondition";
import { Grid } from "@mui/system";
import { useWatch } from "react-hook-form";
-import { useEffect } from "react";
+import { useEffect, useRef } from "react";
export const CippWizardGroupTemplates = (props) => {
const { postUrl, formControl, onPreviousStep, onNextStep, currentStep } = props;
const watcher = useWatch({ control: formControl.control, name: "TemplateList" });
+ const lastAppliedTemplate = useRef(null);
const groupOptions = [
{ label: "Dynamic Group", value: "dynamic" },
{ label: "Dynamic Distribution Group", value: "dynamicDistribution" },
@@ -18,19 +19,28 @@ export const CippWizardGroupTemplates = (props) => {
{ label: "Mail Enabled Security Group", value: "security" },
];
useEffect(() => {
- if (watcher?.value) {
+ if (watcher?.value && watcher.value !== lastAppliedTemplate.current) {
+ lastAppliedTemplate.current = watcher.value;
console.log("Loading template:", watcher);
// Set groupType first to ensure conditional fields are visible
- formControl.setValue("groupType", watcher.addedFields.groupType);
+ formControl.setValue("groupType", watcher.addedFields.groupType, { shouldValidate: true });
// Use setTimeout to ensure the DOM updates with the groupType before setting other fields
setTimeout(() => {
- formControl.setValue("displayName", watcher.addedFields.displayName);
- formControl.setValue("description", watcher.addedFields.description);
- formControl.setValue("username", watcher.addedFields.username);
- formControl.setValue("allowExternal", watcher.addedFields.allowExternal);
- formControl.setValue("membershipRules", watcher.addedFields.membershipRules);
+ formControl.setValue("displayName", watcher.addedFields.displayName, {
+ shouldValidate: true,
+ });
+ formControl.setValue("description", watcher.addedFields.description, {
+ shouldValidate: true,
+ });
+ formControl.setValue("username", watcher.addedFields.username, { shouldValidate: true });
+ formControl.setValue("allowExternal", watcher.addedFields.allowExternal, {
+ shouldValidate: true,
+ });
+ formControl.setValue("membershipRules", watcher.addedFields.membershipRules, {
+ shouldValidate: true,
+ });
console.log("Set membershipRules to:", watcher.addedFields.membershipRules);
}, 100);
diff --git a/src/components/CippWizard/CippWizardOffboarding.jsx b/src/components/CippWizard/CippWizardOffboarding.jsx
index f407a962b628..34d8b37b2bf5 100644
--- a/src/components/CippWizard/CippWizardOffboarding.jsx
+++ b/src/components/CippWizard/CippWizardOffboarding.jsx
@@ -358,7 +358,7 @@ export const CippWizardOffboarding = (props) => {
compareType="is"
compareValue={true}
>
-
+
Scheduled Offboarding Date
{
fullWidth
/>
+
-
- Send results to:
-
-
-
+
+ Send results to:
+
+
+
{
/>
-
+
-
-
-
-
+
+
+
diff --git a/src/components/CippWizard/CippWizardPage.jsx b/src/components/CippWizard/CippWizardPage.jsx
index 6266a3ec1c4b..cda2b0cf5c58 100644
--- a/src/components/CippWizard/CippWizardPage.jsx
+++ b/src/components/CippWizard/CippWizardPage.jsx
@@ -1,8 +1,24 @@
-import { Box, Button, Container, Stack, SvgIcon } from "@mui/material";
+import {
+ Box,
+ Button,
+ Container,
+ Dialog,
+ DialogActions,
+ DialogContent,
+ DialogTitle,
+ Divider,
+ IconButton,
+ Stack,
+ SvgIcon,
+ useMediaQuery,
+} from "@mui/material";
+import { Close } from "@mui/icons-material";
import { CippWizard } from "./CippWizard";
import { useRouter } from "next/router";
import { ArrowLeftIcon } from "@mui/x-date-pickers";
import { CippHead } from "../CippComponents/CippHead";
+import { CippWizardDialogContext } from "./CippWizardDialogContext";
+import { useState, useCallback } from "react";
const CippWizardPage = (props) => {
const router = useRouter();
@@ -14,8 +30,62 @@ const CippWizardPage = (props) => {
backButton = true,
wizardOrientation = "horizontal",
maxWidth = "xl",
+ dialogMode = false,
+ open = false,
+ onClose,
+ dialogIcon,
+ relatedQueryKeys,
...other
} = props;
+
+ const mdDown = useMediaQuery((theme) => theme.breakpoints.down("md"));
+ const [actionsEl, setActionsEl] = useState(null);
+ const actionsRef = useCallback((el) => setActionsEl(el), []);
+
+ const wizardNode = (
+
+ );
+
+ if (dialogMode) {
+ return (
+
+ );
+ }
+
return (
<>
@@ -30,12 +100,9 @@ const CippWizardPage = (props) => {
-
+
+ {wizardNode}
+
diff --git a/src/components/CippWizard/CippWizardStepButtons.jsx b/src/components/CippWizard/CippWizardStepButtons.jsx
index bf0e7f3a7918..64b8f0e23703 100644
--- a/src/components/CippWizard/CippWizardStepButtons.jsx
+++ b/src/components/CippWizard/CippWizardStepButtons.jsx
@@ -1,7 +1,9 @@
import { Button, Stack } from "@mui/material";
import { useFormState } from "react-hook-form";
+import { createPortal } from "react-dom";
import { ApiPostCall } from "../../api/ApiCall";
import { CippApiResults } from "../CippComponents/CippApiResults";
+import { useCippWizardDialog } from "./CippWizardDialogContext";
export const CippWizardStepButtons = (props) => {
const {
@@ -19,7 +21,18 @@ export const CippWizardStepButtons = (props) => {
...other
} = props;
const { isValid, isSubmitted, isSubmitting } = useFormState({ control: formControl.control });
- const sendForm = ApiPostCall({ relatedQueryKeys: queryKeys });
+ const dialogContext = useCippWizardDialog();
+ const mergedQueryKeys = [
+ ...(Array.isArray(queryKeys) ? queryKeys : queryKeys ? [queryKeys] : []),
+ ...(Array.isArray(dialogContext?.relatedQueryKeys)
+ ? dialogContext.relatedQueryKeys
+ : dialogContext?.relatedQueryKeys
+ ? [dialogContext.relatedQueryKeys]
+ : []),
+ ];
+ const sendForm = ApiPostCall({
+ relatedQueryKeys: mergedQueryKeys.length ? mergedQueryKeys : undefined,
+ });
const handleSubmit = () => {
const values = formControl.getValues();
const newData = {};
@@ -33,40 +46,55 @@ export const CippWizardStepButtons = (props) => {
sendForm.mutate({ url: postUrl, data: newData });
};
+ const buttonStack = (
+
+ {dialogContext?.onClose && (
+
+ Close
+
+ )}
+ {currentStep > 0 && (
+
+ Back
+
+ )}
+ {!noNextButton && currentStep !== lastStep && (
+
+ Next Step
+
+ )}
+ {!noSubmitButton && currentStep === lastStep && (
+
+ )}
+
+ );
+
return (
<>
-
- {currentStep > 0 && (
-
- Back
-
- )}
- {!noNextButton && currentStep !== lastStep && (
-
- Next Step
-
- )}
- {!noSubmitButton && currentStep === lastStep && (
-
- )}
-
+ {dialogContext?.actionsEl ? createPortal(buttonStack, dialogContext.actionsEl) : buttonStack}
>
);
};
diff --git a/src/contexts/settings-context.js b/src/contexts/settings-context.js
index f6f61ffc8649..156403b0c472 100644
--- a/src/contexts/settings-context.js
+++ b/src/contexts/settings-context.js
@@ -67,6 +67,15 @@ const storeSettings = (value) => {
storage.setItem(STORAGE_KEY, JSON.stringify(value));
};
+const stripServerManagedSettings = (settings) => {
+ if (!settings || typeof settings !== "object") {
+ return settings;
+ }
+
+ const { bookmarks, ...cleanedSettings } = settings;
+ return cleanedSettings;
+};
+
const initialSettings = {
direction: "ltr",
paletteMode: "light",
@@ -83,6 +92,7 @@ const initialSettings = {
breadcrumbMode: "hierarchical",
bookmarkSidebar: true,
bookmarkPopover: false,
+ compactNav: false,
};
const initialState = {
@@ -106,13 +116,20 @@ export const SettingsProvider = (props) => {
const restored = restoreSettings();
if (restored) {
- if (!restored.currentTheme && restored.paletteMode) {
- restored.currentTheme = { value: restored.paletteMode, label: restored.paletteMode };
+ const cleanedRestored = stripServerManagedSettings(restored);
+
+ if (!cleanedRestored.currentTheme && cleanedRestored.paletteMode) {
+ cleanedRestored.currentTheme = {
+ value: cleanedRestored.paletteMode,
+ label: cleanedRestored.paletteMode,
+ };
}
+ storeSettings(cleanedRestored);
+
setState((prevState) => ({
...prevState,
- ...restored,
+ ...cleanedRestored,
isInitialized: true,
}));
} else {
@@ -142,16 +159,20 @@ export const SettingsProvider = (props) => {
setState((prevState) => {
// Filter out null and undefined values to prevent resetting settings
const filteredSettings = Object.entries(settings).reduce((acc, [key, value]) => {
- if (value !== null && value !== undefined) {
+ if (key !== "bookmarks" && value !== null && value !== undefined) {
acc[key] = value;
}
return acc;
}, {});
- return {
+ const updatedSettings = stripServerManagedSettings({
...prevState,
...filteredSettings,
- };
+ });
+
+ storeSettings(updatedSettings);
+
+ return updatedSettings;
});
}, []);
@@ -172,13 +193,17 @@ export const SettingsProvider = (props) => {
handleUpdate,
isCustom,
setLastUsedFilter: (page, filter) => {
- setState((prevState) => ({
- ...prevState,
- lastUsedFilters: {
- ...prevState.lastUsedFilters,
- [page]: filter,
- },
- }));
+ setState((prevState) => {
+ const updated = stripServerManagedSettings({
+ ...prevState,
+ lastUsedFilters: {
+ ...prevState.lastUsedFilters,
+ [page]: filter,
+ },
+ });
+ storeSettings(updated);
+ return updated;
+ });
},
}}
>
diff --git a/src/data/standards.json b/src/data/standards.json
index 33d28185ebe6..a776d7f0c943 100644
--- a/src/data/standards.json
+++ b/src/data/standards.json
@@ -1343,6 +1343,70 @@
"powershellEquivalent": "Update-MgUser -UserId user@domain.com -BodyParameter @{preferredLanguage='en-US'}",
"recommendedBy": []
},
+ {
+ "name": "standards.AppManagementPolicy",
+ "cat": "Entra (AAD) Standards",
+ "tag": [],
+ "helpText": "Configures the default app management policy to control application and service principal credential restrictions such as password and key credential lifetimes.",
+ "docsDescription": "Configures the default app management policy to control application and service principal credential restrictions. This includes password addition restrictions, custom password addition, symmetric key addition, and credential lifetime limits for both applications and service principals.",
+ "executiveText": "Enforces credential restrictions on application registrations and service principals to limit how secrets and certificates are created and how long they remain valid. This reduces the risk of long-lived or unmanaged credentials being used to access your tenant.",
+ "addedComponent": [
+ {
+ "type": "autoComplete",
+ "multiple": false,
+ "creatable": false,
+ "required": false,
+ "name": "standards.AppManagementPolicy.passwordCredentialsPasswordAddition",
+ "label": "Password Addition",
+ "options": [
+ {
+ "label": "Enabled",
+ "value": "enabled"
+ },
+ {
+ "label": "Disabled",
+ "value": "disabled"
+ }
+ ]
+ },
+ {
+ "type": "autoComplete",
+ "multiple": false,
+ "creatable": false,
+ "required": false,
+ "name": "standards.AppManagementPolicy.passwordCredentialsCustomPasswordAddition",
+ "label": "Custom Password",
+ "options": [
+ {
+ "label": "Enabled",
+ "value": "enabled"
+ },
+ {
+ "label": "Disabled",
+ "value": "disabled"
+ }
+ ]
+ },
+ {
+ "type": "number",
+ "required": false,
+ "name": "standards.AppManagementPolicy.passwordCredentialsMaxLifetime",
+ "label": "Password Credentials Max Lifetime (Days)"
+ },
+ {
+ "type": "number",
+ "required": false,
+ "name": "standards.AppManagementPolicy.keyCredentialsMaxLifetime",
+ "label": "Key Credentials Max Lifetime (Days)"
+ }
+ ],
+ "label": "Set Default App Management Policy",
+ "impact": "Medium Impact",
+ "impactColour": "warning",
+ "addedDate": "2026-03-13",
+ "powershellEquivalent": "Graph API",
+ "recommendedBy": []
+ },
{
"name": "standards.OutBoundSpamAlert",
"cat": "Exchange Standards",
diff --git a/src/hooks/use-securescore.js b/src/hooks/use-securescore.js
index 66287c8f87bd..542b983e618d 100644
--- a/src/hooks/use-securescore.js
+++ b/src/hooks/use-securescore.js
@@ -5,15 +5,7 @@ import standards from "../data/standards.json";
export function useSecureScore({ waiting = true } = {}) {
const currentTenant = useSettings().currentTenant;
- if (currentTenant === "AllTenants") {
- return {
- controlScore: { isFetching: false, isSuccess: false, data: { Results: [] } },
- secureScore: { isFetching: false, isSuccess: false, data: { Results: [] } },
- translatedData: [],
- isFetching: true,
- isSuccess: false,
- };
- }
+ const isAllTenants = currentTenant === 'AllTenants';
const [translatedData, setTranslatedData] = useState([]);
const [isSuccess, setIsSuccess] = useState(false);
@@ -27,7 +19,7 @@ export function useSecureScore({ waiting = true } = {}) {
$top: 999,
},
queryKey: `controlScore-${currentTenant}`,
- waiting: waiting,
+ waiting: waiting || isAllTenants,
});
const secureScore = ApiGetCall({
@@ -40,18 +32,25 @@ export function useSecureScore({ waiting = true } = {}) {
$top: 7,
},
queryKey: `secureScore-${currentTenant}`,
- waiting: waiting,
+ waiting: waiting || isAllTenants,
});
useEffect(() => {
+ if (isAllTenants) {
+ setIsFetching(false);
+ setIsSuccess(false);
+ setTranslatedData([]);
+ return;
+ }
if (controlScore.isFetching || secureScore.isFetching) {
setIsFetching(true);
} else {
- setIsFetching(false);
+ setIsSuccess(false);
}
- }, [controlScore.isFetching, secureScore.isFetching]);
+ }, [controlScore.isFetching, secureScore.isFetching, isAllTenants]);
useEffect(() => {
+ if (isAllTenants) return;
if (controlScore.isSuccess && secureScore.isSuccess) {
const secureScoreData = secureScore.data.Results[0];
const updatedControlScores = secureScoreData.controlScores.map((control) => {
@@ -100,7 +99,7 @@ export function useSecureScore({ waiting = true } = {}) {
});
setIsSuccess(true);
}
- }, [controlScore.isSuccess, secureScore.isSuccess, controlScore.data, secureScore.data]);
+ }, [controlScore.isSuccess, secureScore.isSuccess, controlScore.data, secureScore.data, isAllTenants]);
return {
controlScore,
diff --git a/src/hooks/use-user-bookmarks.js b/src/hooks/use-user-bookmarks.js
new file mode 100644
index 000000000000..0e6b1512bb35
--- /dev/null
+++ b/src/hooks/use-user-bookmarks.js
@@ -0,0 +1,187 @@
+import { useCallback, useEffect, useMemo, useRef } from "react";
+import { useQueryClient } from "@tanstack/react-query";
+import { ApiGetCall, ApiPostCall } from "../api/ApiCall";
+
+const SETTINGS_STORAGE_KEY = "app.settings";
+
+const normalizeBookmarks = (value) => {
+ if (Array.isArray(value)) {
+ return value;
+ }
+
+ if (
+ value &&
+ typeof value === "object" &&
+ typeof value.path === "string" &&
+ typeof value.label === "string"
+ ) {
+ return [value];
+ }
+
+ return [];
+};
+
+const getLocalStoredBookmarks = () => {
+ if (typeof window === "undefined") {
+ return [];
+ }
+
+ try {
+ const restored = window.localStorage.getItem(SETTINGS_STORAGE_KEY);
+ if (!restored) {
+ return [];
+ }
+
+ const parsed = JSON.parse(restored);
+ return normalizeBookmarks(parsed?.bookmarks);
+ } catch {
+ return [];
+ }
+};
+
+const clearLocalStoredBookmarks = () => {
+ if (typeof window === "undefined") {
+ return;
+ }
+
+ try {
+ const restored = window.localStorage.getItem(SETTINGS_STORAGE_KEY);
+ if (!restored) {
+ return;
+ }
+
+ const parsed = JSON.parse(restored);
+ if (!parsed || typeof parsed !== "object" || !Object.prototype.hasOwnProperty.call(parsed, "bookmarks")) {
+ return;
+ }
+
+ delete parsed.bookmarks;
+ window.localStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(parsed));
+ } catch {
+ return;
+ }
+};
+
+const getBookmarksFromSettings = (settingsData) => {
+ if (!settingsData) {
+ return [];
+ }
+
+ if (settingsData.UserBookmarks) {
+ return normalizeBookmarks(settingsData.UserBookmarks);
+ }
+
+ if (settingsData.bookmarks) {
+ return normalizeBookmarks(settingsData.bookmarks);
+ }
+
+ return [];
+};
+
+export const useUserBookmarks = () => {
+ const queryClient = useQueryClient();
+ const localMigrationComplete = useRef(false);
+ const localMigrationInFlight = useRef(false);
+
+ const userSettings = ApiGetCall({
+ url: "/api/ListUserSettings",
+ queryKey: "userSettings",
+ });
+
+ const auth = ApiGetCall({
+ url: "/api/me",
+ queryKey: "authmecipp",
+ });
+
+ const saveBookmarksPost = ApiPostCall({
+ relatedQueryKeys: "userSettings",
+ });
+
+ const bookmarks = useMemo(() => {
+ return getBookmarksFromSettings(userSettings.data);
+ }, [userSettings.data]);
+
+ const persistBookmarks = useCallback(
+ (nextBookmarks, callbacks = {}) => {
+ const safeBookmarks = Array.isArray(nextBookmarks) ? nextBookmarks : [];
+
+ queryClient.setQueryData(["userSettings"], (previous) => ({
+ ...(previous || {}),
+ UserBookmarks: safeBookmarks,
+ bookmarks: safeBookmarks,
+ }));
+
+ const user = auth.data?.clientPrincipal?.userDetails;
+ if (!user) {
+ return false;
+ }
+
+ saveBookmarksPost.mutate(
+ {
+ url: "/api/ExecUserBookmarks",
+ data: {
+ user,
+ currentSettings: {
+ bookmarks: safeBookmarks,
+ },
+ },
+ },
+ callbacks
+ );
+
+ return true;
+ },
+ [auth.data?.clientPrincipal?.userDetails, queryClient, saveBookmarksPost]
+ );
+
+ const setBookmarks = useCallback(
+ (nextBookmarks) => {
+ persistBookmarks(nextBookmarks);
+ },
+ [persistBookmarks]
+ );
+
+ useEffect(() => {
+ if (localMigrationComplete.current || localMigrationInFlight.current) {
+ return;
+ }
+
+ if (!auth.data?.clientPrincipal?.userDetails) {
+ return;
+ }
+
+ if (bookmarks.length > 0) {
+ localMigrationComplete.current = true;
+ return;
+ }
+
+ const localBookmarks = getLocalStoredBookmarks();
+ if (localBookmarks.length === 0) {
+ localMigrationComplete.current = true;
+ return;
+ }
+
+ localMigrationInFlight.current = true;
+ const didPost = persistBookmarks(localBookmarks, {
+ onSuccess: () => {
+ clearLocalStoredBookmarks();
+ localMigrationInFlight.current = false;
+ localMigrationComplete.current = true;
+ },
+ onError: () => {
+ localMigrationInFlight.current = false;
+ },
+ });
+
+ if (!didPost) {
+ localMigrationInFlight.current = false;
+ }
+ }, [auth.data?.clientPrincipal?.userDetails, bookmarks.length, persistBookmarks]);
+
+ return {
+ bookmarks,
+ setBookmarks,
+ isLoading: userSettings.isLoading,
+ isSaving: saveBookmarksPost.isPending,
+ };
+};
\ No newline at end of file
diff --git a/src/layouts/index.js b/src/layouts/index.js
index b3c6ea7ba694..b3ef2244cd1f 100644
--- a/src/layouts/index.js
+++ b/src/layouts/index.js
@@ -86,7 +86,6 @@ export const Layout = (props) => {
const mdDown = useMediaQuery((theme) => theme.breakpoints.down("md"));
const settings = useSettings();
const mobileNav = useMobileNav();
- const [userSettingsComplete, setUserSettingsComplete] = useState(false);
const [fetchingVisible, setFetchingVisible] = useState([]);
const [menuItems, setMenuItems] = useState(nativeMenuItems);
const lastUserSettingsUpdate = useRef(null);
@@ -216,44 +215,27 @@ export const Layout = (props) => {
// Only update if the data has actually changed (using dataUpdatedAt as a proxy)
const dataUpdatedAt = userSettingsAPI.dataUpdatedAt;
if (dataUpdatedAt && dataUpdatedAt !== lastUserSettingsUpdate.current) {
+ const { bookmarks: _bookmarks, ...serverSettings } = userSettingsAPI.data || {};
//if userSettingsAPI.data contains offboardingDefaults.user, delete that specific key.
- if (userSettingsAPI.data.offboardingDefaults?.user) {
- delete userSettingsAPI.data.offboardingDefaults.user;
+ if (serverSettings.offboardingDefaults?.user) {
+ delete serverSettings.offboardingDefaults.user;
}
- if (userSettingsAPI.data.offboardingDefaults?.keepCopy) {
- delete userSettingsAPI.data.offboardingDefaults.keepCopy;
+ if (serverSettings.offboardingDefaults?.keepCopy) {
+ delete serverSettings.offboardingDefaults.keepCopy;
}
- if (userSettingsAPI?.data?.currentTheme) {
- delete userSettingsAPI.data.currentTheme;
+ if (serverSettings?.currentTheme) {
+ delete serverSettings.currentTheme;
}
- // get current devtools settings
+ // get current devtools settings (device-local only)
var showDevtools = settings.showDevtools;
- // get current bookmarks and navigation settings (device-local only)
- var bookmarks = settings.bookmarks;
- var bookmarkSidebar = settings.bookmarkSidebar;
- var bookmarkPopover = settings.bookmarkPopover;
- var bookmarkReorderMode = settings.bookmarkReorderMode;
- var bookmarkLocked = settings.bookmarkLocked;
- var bookmarkSortOrder = settings.bookmarkSortOrder;
- var bookmarksOpen = settings.bookmarksOpen;
- var compactNav = settings.compactNav;
settings.handleUpdate({
- ...userSettingsAPI.data,
- bookmarks,
- bookmarkSidebar,
- bookmarkPopover,
- bookmarkReorderMode,
- bookmarkLocked,
- bookmarkSortOrder,
- bookmarksOpen,
- compactNav,
+ ...serverSettings,
showDevtools,
});
// Track this update and set completion status
lastUserSettingsUpdate.current = dataUpdatedAt;
- setUserSettingsComplete(true);
}
}
}, [
diff --git a/src/layouts/side-nav-bookmarks.js b/src/layouts/side-nav-bookmarks.js
index 229a86c88bbf..027513c34635 100644
--- a/src/layouts/side-nav-bookmarks.js
+++ b/src/layouts/side-nav-bookmarks.js
@@ -14,11 +14,14 @@ import LockOpenIcon from "@mui/icons-material/LockOpen";
import ChevronRightIcon from "@heroicons/react/24/outline/ChevronRightIcon";
import ChevronDownIcon from "@heroicons/react/24/outline/ChevronDownIcon";
import { useSettings } from "../hooks/use-settings";
+import { useUserBookmarks } from "../hooks/use-user-bookmarks";
export const SideNavBookmarks = ({ collapse = false }) => {
const settings = useSettings();
const compactNav = settings.compactNav ?? false;
const navItemPy = compactNav ? "6px" : "12px";
+ const emptyStatePy = compactNav ? "4px" : "8px";
+ const { bookmarks, setBookmarks } = useUserBookmarks();
const [open, setOpen] = useState(settings.bookmarksOpen ?? false);
const reorderMode = settings.bookmarkReorderMode || "arrows";
const locked = settings.bookmarkLocked ?? false;
@@ -42,38 +45,37 @@ export const SideNavBookmarks = ({ collapse = false }) => {
const moveBookmarkUp = useCallback(
(index) => {
if (index <= 0) return;
- const updatedBookmarks = [...(settings.bookmarks || [])];
+ const updatedBookmarks = [...bookmarks];
const temp = updatedBookmarks[index];
updatedBookmarks[index] = updatedBookmarks[index - 1];
updatedBookmarks[index - 1] = temp;
- settings.handleUpdate({ bookmarks: updatedBookmarks });
+ setBookmarks(updatedBookmarks);
},
- [settings],
+ [bookmarks, setBookmarks]
);
const moveBookmarkDown = useCallback(
(index) => {
- const bookmarks = settings.bookmarks || [];
if (index >= bookmarks.length - 1) return;
const updatedBookmarks = [...bookmarks];
const temp = updatedBookmarks[index];
updatedBookmarks[index] = updatedBookmarks[index + 1];
updatedBookmarks[index + 1] = temp;
- settings.handleUpdate({ bookmarks: updatedBookmarks });
+ setBookmarks(updatedBookmarks);
},
- [settings],
+ [bookmarks, setBookmarks]
);
const removeBookmark = useCallback(
(path) => {
- const updatedBookmarks = [...(settings.bookmarks || [])];
+ const updatedBookmarks = [...bookmarks];
const origIdx = updatedBookmarks.findIndex((b) => b.path === path);
if (origIdx !== -1) {
updatedBookmarks.splice(origIdx, 1);
- settings.handleUpdate({ bookmarks: updatedBookmarks });
+ setBookmarks(updatedBookmarks);
}
},
- [settings],
+ [bookmarks, setBookmarks]
);
const animatedMoveUp = useCallback(
@@ -97,7 +99,6 @@ export const SideNavBookmarks = ({ collapse = false }) => {
const animatedMoveDown = useCallback(
(index) => {
- const bookmarks = settings.bookmarks || [];
if (index >= bookmarks.length - 1 || animatingPair) return;
const el1 = itemRefs.current[index];
const el2 = itemRefs.current[index + 1];
@@ -112,7 +113,7 @@ export const SideNavBookmarks = ({ collapse = false }) => {
setAnimatingPair(null);
}, 250);
},
- [animatingPair, settings.bookmarks, moveBookmarkDown],
+ [animatingPair, bookmarks, moveBookmarkDown]
);
const triggerSortFlash = useCallback(() => {
@@ -146,15 +147,14 @@ export const SideNavBookmarks = ({ collapse = false }) => {
setDragOverIndex(null);
return;
}
- const items = [...(settings.bookmarks || [])];
+ const items = [...bookmarks];
const [reordered] = items.splice(dragIndex, 1);
items.splice(dropIndex, 0, reordered);
- settings.handleUpdate({ bookmarks: items, bookmarkSortOrder: "custom" });
- setSortOrder("custom");
+ setBookmarks(items);
setDragIndex(null);
setDragOverIndex(null);
},
- [dragIndex, settings],
+ [dragIndex, bookmarks, setBookmarks]
);
const handleDragEnd = useCallback(() => {
@@ -169,13 +169,12 @@ export const SideNavBookmarks = ({ collapse = false }) => {
}, [sortOrder, settings]);
const displayBookmarks = useMemo(() => {
- const bookmarks = settings.bookmarks || [];
if (sortOrder === "custom") return bookmarks;
return [...bookmarks].sort((a, b) => {
const cmp = (a.label || "").localeCompare(b.label || "");
return sortOrder === "asc" ? cmp : -cmp;
});
- }, [settings.bookmarks, sortOrder]);
+ }, [bookmarks, sortOrder]);
return (
@@ -191,7 +190,7 @@ export const SideNavBookmarks = ({ collapse = false }) => {
fontWeight: 500,
justifyContent: "flex-start",
px: "6px",
- py: "12px",
+ py: navItemPy,
textAlign: "left",
whiteSpace: "nowrap",
width: "100%",
@@ -311,7 +310,7 @@ export const SideNavBookmarks = ({ collapse = false }) => {
sx={{
pl: "42px",
pr: "8px",
- py: "8px",
+ py: emptyStatePy,
color: "text.secondary",
fontSize: 13,
}}
@@ -401,11 +400,7 @@ export const SideNavBookmarks = ({ collapse = false }) => {
const li = el?.closest("[data-bookmark-index]");
if (li) {
const overIdx = parseInt(li.dataset.bookmarkIndex, 10);
- if (
- !isNaN(overIdx) &&
- overIdx >= 0 &&
- overIdx < (settings.bookmarks || []).length
- ) {
+ if (!isNaN(overIdx) && overIdx >= 0 && overIdx < bookmarks.length) {
touchDragRef.current.overIdx = overIdx;
setDragOverIndex(overIdx);
}
@@ -414,11 +409,10 @@ export const SideNavBookmarks = ({ collapse = false }) => {
onTouchEnd={() => {
const { startIdx, overIdx } = touchDragRef.current;
if (startIdx !== null && overIdx !== null && startIdx !== overIdx) {
- const items = [...(settings.bookmarks || [])];
+ const items = [...bookmarks];
const [reordered] = items.splice(startIdx, 1);
items.splice(overIdx, 0, reordered);
- settings.handleUpdate({ bookmarks: items, bookmarkSortOrder: "custom" });
- setSortOrder("custom");
+ setBookmarks(items);
}
touchDragRef.current = { startIdx: null, overIdx: null };
setDragIndex(null);
diff --git a/src/layouts/side-nav-item.js b/src/layouts/side-nav-item.js
index 2a741dfe4ee1..ca6bbdd812ea 100644
--- a/src/layouts/side-nav-item.js
+++ b/src/layouts/side-nav-item.js
@@ -7,6 +7,7 @@ import ArrowTopRightOnSquareIcon from "@heroicons/react/24/outline/ArrowTopRight
import { Box, ButtonBase, Collapse, SvgIcon, Stack } from "@mui/material";
import BookmarkBorderIcon from "@mui/icons-material/BookmarkBorder";
import BookmarkIcon from "@mui/icons-material/Bookmark";
+import { useUserBookmarks } from "../hooks/use-user-bookmarks";
import { useSettings } from "../hooks/use-settings";
export const SideNavItem = (props) => {
@@ -24,7 +25,9 @@ export const SideNavItem = (props) => {
const [open, setOpen] = useState(openImmediately);
const [hovered, setHovered] = useState(false);
- const { handleUpdate, bookmarks = [], compactNav = false } = useSettings();
+ const { bookmarks, setBookmarks } = useUserBookmarks();
+ const settings = useSettings();
+ const compactNav = settings.compactNav ?? false;
const isBookmarked = bookmarks.some((bookmark) => bookmark.path === path);
const handleToggle = useCallback(() => {
@@ -34,13 +37,15 @@ export const SideNavItem = (props) => {
const handleBookmarkToggle = useCallback(
(event) => {
event.stopPropagation();
- handleUpdate({
- bookmarks: isBookmarked
+ setBookmarks(
+ isBookmarked
? bookmarks.filter((bookmark) => bookmark.path !== path)
- : bookmarks.length >= 50 ? bookmarks : [...bookmarks, { label: title, path }],
- });
+ : bookmarks.length >= 50
+ ? bookmarks
+ : [...bookmarks, { label: title, path }]
+ );
},
- [isBookmarked, bookmarks, handleUpdate, path, title]
+ [isBookmarked, bookmarks, setBookmarks, path, title]
);
// Dynamic spacing and font sizing based on depth
diff --git a/src/layouts/top-nav.js b/src/layouts/top-nav.js
index 370fd0f7ddbd..3c8eb4c65dd2 100644
--- a/src/layouts/top-nav.js
+++ b/src/layouts/top-nav.js
@@ -29,6 +29,7 @@ import {
} from "@mui/material";
import { Logo } from "../components/logo";
import { useSettings } from "../hooks/use-settings";
+import { useUserBookmarks } from "../hooks/use-user-bookmarks";
import { paths } from "../paths";
import { AccountPopover } from "./account-popover";
import { CippTenantSelector } from "../components/CippComponents/CippTenantSelector";
@@ -43,6 +44,7 @@ export const TopNav = (props) => {
const searchDialog = useDialog();
const { onNavOpen } = props;
const settings = useSettings();
+ const { bookmarks, setBookmarks } = useUserBookmarks();
const mdDown = useMediaQuery((theme) => theme.breakpoints.down("md"));
const showPopoverBookmarks = settings.bookmarkPopover === true;
const reorderMode = settings.bookmarkReorderMode || "arrows";
@@ -90,10 +92,10 @@ export const TopNav = (props) => {
setDragOverIndex(null);
return;
}
- const items = [...(settings.bookmarks || [])];
+ const items = [...bookmarks];
const [reordered] = items.splice(dragIndex, 1);
items.splice(dropIndex, 0, reordered);
- settings.handleUpdate({ bookmarks: items });
+ setBookmarks(items);
setDragIndex(null);
setDragOverIndex(null);
};
@@ -105,29 +107,28 @@ export const TopNav = (props) => {
const moveBookmarkUp = (index) => {
if (index <= 0) return;
- const updatedBookmarks = [...(settings.bookmarks || [])];
+ const updatedBookmarks = [...bookmarks];
const temp = updatedBookmarks[index];
updatedBookmarks[index] = updatedBookmarks[index - 1];
updatedBookmarks[index - 1] = temp;
- settings.handleUpdate({ bookmarks: updatedBookmarks });
+ setBookmarks(updatedBookmarks);
};
const moveBookmarkDown = (index) => {
- const bookmarks = settings.bookmarks || [];
if (index >= bookmarks.length - 1) return;
const updatedBookmarks = [...bookmarks];
const temp = updatedBookmarks[index];
updatedBookmarks[index] = updatedBookmarks[index + 1];
updatedBookmarks[index + 1] = temp;
- settings.handleUpdate({ bookmarks: updatedBookmarks });
+ setBookmarks(updatedBookmarks);
};
const removeBookmark = (path) => {
- const updatedBookmarks = [...(settings.bookmarks || [])];
+ const updatedBookmarks = [...bookmarks];
const origIdx = updatedBookmarks.findIndex((b) => b.path === path);
if (origIdx !== -1) {
updatedBookmarks.splice(origIdx, 1);
- settings.handleUpdate({ bookmarks: updatedBookmarks });
+ setBookmarks(updatedBookmarks);
}
};
@@ -162,7 +163,6 @@ export const TopNav = (props) => {
};
const animatedMoveDown = (index) => {
- const bookmarks = settings.bookmarks || [];
if (index >= bookmarks.length - 1 || animatingPair) return;
const el1 = itemRefs.current[index];
const el2 = itemRefs.current[index + 1];
@@ -185,13 +185,12 @@ export const TopNav = (props) => {
};
const displayBookmarks = useMemo(() => {
- const bookmarks = settings.bookmarks || [];
if (sortOrder === "custom") return bookmarks;
return [...bookmarks].sort((a, b) => {
const cmp = (a.label || "").localeCompare(b.label || "");
return sortOrder === "asc" ? cmp : -cmp;
});
- }, [settings.bookmarks, sortOrder]);
+ }, [bookmarks, sortOrder]);
const popoverOpen = Boolean(anchorEl);
const popoverId = popoverOpen ? "bookmark-popover" : undefined;
@@ -454,11 +453,7 @@ export const TopNav = (props) => {
const li = el?.closest("[data-bookmark-index]");
if (li) {
const overIdx = parseInt(li.dataset.bookmarkIndex, 10);
- if (
- !isNaN(overIdx) &&
- overIdx >= 0 &&
- overIdx < (settings.bookmarks || []).length
- ) {
+ if (!isNaN(overIdx) && overIdx >= 0 && overIdx < bookmarks.length) {
touchDragRef.current.overIdx = overIdx;
setDragOverIndex(overIdx);
}
@@ -467,10 +462,10 @@ export const TopNav = (props) => {
onTouchEnd={() => {
const { startIdx, overIdx } = touchDragRef.current;
if (startIdx !== null && overIdx !== null && startIdx !== overIdx) {
- const items = [...(settings.bookmarks || [])];
+ const items = [...bookmarks];
const [reordered] = items.splice(startIdx, 1);
items.splice(overIdx, 0, reordered);
- settings.handleUpdate({ bookmarks: items });
+ setBookmarks(items);
}
touchDragRef.current = { startIdx: null, overIdx: null };
setDragIndex(null);
diff --git a/src/pages/cipp/preferences.js b/src/pages/cipp/preferences.js
index fb575a84aae1..f6bbde2e7704 100644
--- a/src/pages/cipp/preferences.js
+++ b/src/pages/cipp/preferences.js
@@ -99,54 +99,6 @@ const Page = () => {
name: "user",
});
- // Watch navigation settings and apply immediately (device-local, no server save needed)
- const watchedBookmarkSidebar = useWatch({
- control: formcontrol.control,
- name: "bookmarkSidebar",
- });
- const watchedBookmarkPopover = useWatch({
- control: formcontrol.control,
- name: "bookmarkPopover",
- });
- const watchedBookmarkReorderMode = useWatch({
- control: formcontrol.control,
- name: "bookmarkReorderMode",
- });
- const watchedCompactNav = useWatch({ control: formcontrol.control, name: "compactNav" });
-
- useEffect(() => {
- const updates = {};
- if (
- watchedBookmarkSidebar !== undefined &&
- watchedBookmarkSidebar !== settings.bookmarkSidebar
- ) {
- updates.bookmarkSidebar = watchedBookmarkSidebar;
- }
- if (
- watchedBookmarkPopover !== undefined &&
- watchedBookmarkPopover !== settings.bookmarkPopover
- ) {
- updates.bookmarkPopover = watchedBookmarkPopover;
- }
- if (
- watchedBookmarkReorderMode !== undefined &&
- watchedBookmarkReorderMode !== settings.bookmarkReorderMode
- ) {
- updates.bookmarkReorderMode = watchedBookmarkReorderMode;
- }
- if (watchedCompactNav !== undefined && watchedCompactNav !== settings.compactNav) {
- updates.compactNav = watchedCompactNav;
- }
- if (Object.keys(updates).length > 0) {
- settings.handleUpdate(updates);
- }
- }, [
- watchedBookmarkSidebar,
- watchedBookmarkPopover,
- watchedBookmarkReorderMode,
- watchedCompactNav,
- ]);
-
// Update form when initial user type is determined
useEffect(() => {
if (initialUserType !== null && auth.data?.clientPrincipal?.userDetails) {
@@ -362,7 +314,7 @@ const Page = () => {
{
const [editTaskId, setEditTaskId] = useState(null);
const [cloneTaskId, setCloneTaskId] = useState(null);
+ const currentTenant = useSettings().currentTenant;
const drawerHandlers = {
openEditDrawer: (row) => {
@@ -67,9 +69,13 @@ const Page = () => {
tenantInTitle={false}
title="Scheduled Tasks"
apiUrl={
- showHiddenJobs ? "/api/ListScheduledItems?ShowHidden=true" : "/api/ListScheduledItems"
+ showHiddenJobs ? `/api/ListScheduledItems?ShowHidden=true` : `/api/ListScheduledItems`
+ }
+ queryKey={
+ showHiddenJobs
+ ? `ListScheduledItems-hidden-${currentTenant}`
+ : `ListScheduledItems-${currentTenant}`
}
- queryKey={showHiddenJobs ? `ListScheduledItems-hidden` : `ListScheduledItems`}
simpleColumns={[
"ExecutedTime",
"TaskState",
diff --git a/src/pages/cipp/settings/password-config/index.js b/src/pages/cipp/settings/password-config/index.js
new file mode 100644
index 000000000000..540dd2853b2f
--- /dev/null
+++ b/src/pages/cipp/settings/password-config/index.js
@@ -0,0 +1,329 @@
+import { useEffect, useState, useCallback } from "react";
+import {
+ Alert,
+ Box,
+ Button,
+ Card,
+ CardContent,
+ Container,
+ Divider,
+ FormControlLabel,
+ Stack,
+ SvgIcon,
+ Switch,
+ TextField,
+ ToggleButton,
+ ToggleButtonGroup,
+ Typography,
+} from "@mui/material";
+import { Grid } from "@mui/system";
+import { Layout as DashboardLayout } from "../../../../layouts/index.js";
+import { useRouter } from "next/router";
+import { ApiGetCall, ApiPostCall } from "../../../../api/ApiCall";
+import { ArrowLeftIcon, CheckIcon } from "@heroicons/react/24/outline";
+import { CippHead } from "../../../../components/CippComponents/CippHead";
+import { CippApiResults } from "../../../../components/CippComponents/CippApiResults";
+
+// Password configuration constants
+const PASSWORD_TYPES = {
+ CLASSIC: 'Classic',
+ PASSPHRASE: 'Passphrase'
+};
+
+const DEFAULT_VALUES = {
+ CHAR_COUNT: 14,
+ WORD_COUNT: 4,
+ SPECIAL_CHAR_SET: '$%&*#',
+ SEPARATOR: '-'
+};
+
+function normalizeConfigForBackend(config) {
+ return {
+ passwordType: String(config.passwordType || PASSWORD_TYPES.CLASSIC),
+ charCount: String(parseInt(config.charCount, 10) || DEFAULT_VALUES.CHAR_COUNT),
+ includeUppercase: String(Boolean(config.includeUppercase)),
+ includeLowercase: String(Boolean(config.includeLowercase)),
+ includeDigits: String(Boolean(config.includeDigits)),
+ includeSpecialChars: String(Boolean(config.includeSpecialChars)),
+ specialCharSet: String(config.specialCharSet || DEFAULT_VALUES.SPECIAL_CHAR_SET),
+ wordCount: String(parseInt(config.wordCount, 10) || DEFAULT_VALUES.WORD_COUNT),
+ separator: config.separator !== undefined && config.separator !== null ? String(config.separator) : DEFAULT_VALUES.SEPARATOR,
+ capitalizeWords: String(Boolean(config.capitalizeWords)),
+ appendNumber: String(Boolean(config.appendNumber)),
+ appendSpecialChar: String(Boolean(config.appendSpecialChar)),
+ };
+}
+
+
+const DEFAULT_CONFIG = {
+ passwordType: PASSWORD_TYPES.CLASSIC,
+ charCount: String(DEFAULT_VALUES.CHAR_COUNT),
+ includeUppercase: true,
+ includeLowercase: true,
+ includeDigits: true,
+ includeSpecialChars: true,
+ specialCharSet: DEFAULT_VALUES.SPECIAL_CHAR_SET,
+ wordCount: String(DEFAULT_VALUES.WORD_COUNT),
+ separator: DEFAULT_VALUES.SEPARATOR,
+ capitalizeWords: false,
+ appendNumber: false,
+ appendSpecialChar: false,
+};
+
+// ── Page ──────────────────────────────────────────────────────────────────────
+
+const Page = () => {
+ const router = useRouter();
+ const [config, setConfig] = useState(DEFAULT_CONFIG);
+
+ const passwordSetting = ApiGetCall({ url: "/api/ExecPasswordConfig?list=true", queryKey: "PasswordSettings" });
+ const passwordSave = ApiPostCall({ datafromUrl: true, relatedQueryKeys: "PasswordSettings" });
+
+ useEffect(() => {
+ if (passwordSetting.isSuccess && passwordSetting.data) {
+ const r = passwordSetting.data.Results;
+ const toBool = (v, def) => {
+ if (v === undefined || v === null) return def;
+ if (typeof v === 'boolean') return v;
+ if (typeof v === 'string') return v.toLowerCase() === 'true';
+ if (typeof v === 'number') return v === 1;
+ return def;
+ };
+
+ setConfig({
+ passwordType: r.passwordType || DEFAULT_CONFIG.passwordType,
+ charCount: String(parseInt(r.charCount, 10) || DEFAULT_CONFIG.charCount),
+ includeUppercase: toBool(r.includeUppercase, DEFAULT_CONFIG.includeUppercase),
+ includeLowercase: toBool(r.includeLowercase, DEFAULT_CONFIG.includeLowercase),
+ includeDigits: toBool(r.includeDigits, DEFAULT_CONFIG.includeDigits),
+ includeSpecialChars: toBool(r.includeSpecialChars, DEFAULT_CONFIG.includeSpecialChars),
+ specialCharSet: r.specialCharSet || DEFAULT_CONFIG.specialCharSet,
+ wordCount: String(parseInt(r.wordCount, 10) || DEFAULT_CONFIG.wordCount),
+ separator: r.separator !== undefined ? r.separator : DEFAULT_CONFIG.separator,
+ capitalizeWords: toBool(r.capitalizeWords, DEFAULT_CONFIG.capitalizeWords),
+ appendNumber: toBool(r.appendNumber, DEFAULT_CONFIG.appendNumber),
+ appendSpecialChar: toBool(r.appendSpecialChar, DEFAULT_CONFIG.appendSpecialChar),
+ });
+ }
+ }, [passwordSetting.isSuccess, passwordSetting.data]);
+
+ const set = useCallback((field, value) => {
+ setConfig((p) => ({ ...p, [field]: value }));
+ }, []);
+
+ const isClassic = config.passwordType === PASSWORD_TYPES.CLASSIC;
+
+ const handleSave = () => {
+ const normalizedConfig = normalizeConfigForBackend(config);
+
+ passwordSave.mutate(
+ {
+ url: "/api/ExecPasswordConfig",
+ data: normalizedConfig,
+ queryKey: "PasswordSettingsPost",
+ }
+ );
+ };
+
+ const handleBackToSettings = () => {
+ router.push("/cipp/settings");
+ };
+
+ return (
+ <>
+
+
+
+
+
+
+
Password Configuration
+
}
+ onClick={handleBackToSettings}
+ >
+ Settings
+
+
+
+
+
+
+
+
+ Type
+
+ v && set("passwordType", v)}
+ size="small"
+ color="primary"
+ >
+ Classic
+ Passphrase
+
+ }
+ >
+ {passwordSave.isPending ? "Saving..." : "Save"}
+
+
+
+
+ {isClassic
+ ? "Random characters from the selected classes. Good for systems requiring specific character types. 16+ characters recommended for strong security."
+ : "Random dictionary words joined together. Easier to remember and typically stronger at equal length. 5+ words recommended for high security."}
+
+
+
+ {isClassic ? (
+ <>
+
+ {
+ const value = e.target.value;
+ if (value === '' || /^\d+$/.test(value)) {
+ set("charCount", value);
+ }
+ }}
+ size="small"
+ sx={{ width: 120 }}
+ inputProps={{
+ style: { height: "40px" },
+ min: 8,
+ max: 256
+ }}
+ error={config.charCount === ''}
+ helperText={config.charCount === '' ? "Length cannot be empty" : ""}
+ />
+
+
+
+ set("includeUppercase", e.target.checked)} />}
+ label={Uppercase (A-Z)}
+ />
+
+
+ set("includeLowercase", e.target.checked)} />}
+ label={Lowercase (a-z)}
+ />
+
+
+ set("includeDigits", e.target.checked)} />}
+ label={Digits (0-9)}
+ />
+
+
+ set("includeSpecialChars", e.target.checked)} />}
+ label={Special Characters}
+ />
+
+
+ {config.includeSpecialChars && (
+ set("specialCharSet", e.target.value)}
+ size="small"
+ fullWidth
+ helperText="Allowed: !@#$%^&*()-_=+/"
+ />
+ )}
+ >
+ ) : (
+ <>
+
+ {
+ const value = e.target.value;
+ if (value === '' || /^\d+$/.test(value)) {
+ set("wordCount", value);
+ }
+ }}
+ size="small"
+ sx={{ width: 120, maxWidth: 160 }}
+ inputProps={{
+ style: { height: "40px" },
+ min: 2,
+ max: 10
+ }}
+ error={config.wordCount === ''}
+ helperText={config.wordCount === '' ? "Word count cannot be empty" : ""}
+ />
+ set("separator", e.target.value)}
+ size="small"
+ sx={{ maxWidth: 120 }}
+ />
+
+ Allowed: single space, empty, or !@#$%^&*()-_=+/
+
+
+
+
+ set("capitalizeWords", e.target.checked)} />}
+ label={Capitalize words}
+ />
+
+
+ set("appendNumber", e.target.checked)} />}
+ label={Append number}
+ />
+
+
+ set("appendSpecialChar", e.target.checked)} />}
+ label={Append Special Character}
+ />
+
+
+ {config.appendSpecialChar && (
+ set("specialCharSet", e.target.value)}
+ size="small"
+ fullWidth
+ helperText="Allowed: !@#$%^&*()-_=+/"
+ />
+ )}
+ >
+ )}
+
+
+
+
+
+
+
+ >
+ );
+};
+
+Page.getLayout = (page) => {page};
+
+export default Page;
diff --git a/src/pages/cipp/settings/tenants.js b/src/pages/cipp/settings/tenants.js
index 3ce10da19c2d..c6cd965fb2ac 100644
--- a/src/pages/cipp/settings/tenants.js
+++ b/src/pages/cipp/settings/tenants.js
@@ -1,12 +1,201 @@
+import { Button, SvgIcon } from "@mui/material";
+import { CippTablePage } from "../../../components/CippComponents/CippTablePage.jsx";
+import { CippApiDialog } from "../../../components/CippComponents/CippApiDialog";
+import { useCippGDAPTrace } from "../../../components/CippSettings/CippGDAP/CippGDAPTrace";
import { Layout as DashboardLayout } from "../../../layouts/index.js";
import { TabbedLayout } from "../../../layouts/TabbedLayout";
import tabOptions from "./tabOptions";
-import { CippTenantTable } from "../../../components/CippWizard/CippTenantTable";
+import { useDialog } from "../../../hooks/use-dialog";
+import {
+ Sync,
+ Block,
+ PlayArrow,
+ RestartAlt,
+ Delete,
+ Add,
+ Refresh,
+} from "@mui/icons-material";
+import cacheTypes from "../../../data/CIPPDBCacheTypes.json";
const Page = () => {
const pageTitle = "Tenants - Backend";
+ const createDialog = useDialog();
+ const { ref: gdapRef, traceGdapAction, CippGDAPTrace } = useCippGDAPTrace();
- return ;
+ const actions = [
+ {
+ label: "Exclude Tenants",
+ type: "POST",
+ url: "/api/ExecExcludeTenant?AddExclusion=true",
+ icon: ,
+ data: { value: "customerId" },
+ confirmText: "Are you sure you want to exclude [displayName]?",
+ multiPost: false,
+ condition: (row) => row.displayName !== "*Partner Tenant",
+ },
+ {
+ label: "Include Tenants",
+ type: "POST",
+ url: "/api/ExecExcludeTenant?RemoveExclusion=true",
+ icon: ,
+ data: { value: "customerId" },
+ confirmText: "Are you sure you want to include [displayName]?",
+ multiPost: false,
+ condition: (row) => row.displayName !== "*Partner Tenant",
+ },
+ {
+ label: "Refresh CPV Permissions",
+ type: "POST",
+ url: "/api/ExecCPVPermissions",
+ icon: ,
+ data: { tenantFilter: "customerId" },
+ confirmText: "Are you sure you want to refresh the CPV permissions for [displayName]?",
+ multiPost: false,
+ },
+ {
+ label: "Reset CPV Permissions",
+ type: "POST",
+ url: "/api/ExecCPVPermissions?&ResetSP=true",
+ icon: ,
+ data: { tenantFilter: "customerId" },
+ confirmText:
+ "Are you sure you want to reset the CPV permissions for [displayName]? (This will delete the Service Principal and re-add it.)",
+ multiPost: false,
+ condition: (row) =>
+ row.displayName !== "*Partner Tenant" && row.delegatedPrivilegeStatus !== "directTenant",
+ },
+ {
+ label: "Remove Tenant",
+ type: "POST",
+ url: "/api/ExecRemoveTenant",
+ icon: ,
+ data: { TenantID: "customerId" },
+ confirmText:
+ "Are you sure you want to remove [displayName]? If this is a Direct Tenant, this will no longer be accessible until you add it via the Setup Wizard.",
+ multiPost: false,
+ condition: (row) => row.displayName !== "*Partner Tenant",
+ },
+ {
+ label: "Refresh CIPPDB Cache",
+ type: "GET",
+ url: "/api/ExecCIPPDBCache",
+ icon: ,
+ data: { Name: "Name", TenantFilter: "customerId" },
+ confirmText: "Select the cache type to refresh for [displayName]:",
+ multiPost: false,
+ hideBulk: true,
+ fields: [
+ {
+ type: "autoComplete",
+ name: "Name",
+ label: "Cache Type",
+ placeholder: "Select a cache type",
+ options: cacheTypes.map((cacheType) => ({
+ label: cacheType.friendlyName,
+ value: cacheType.type,
+ description: cacheType.description,
+ })),
+ multiple: false,
+ creatable: false,
+ required: true,
+ },
+ ],
+ customDataformatter: (rowData, actionData, formData) => {
+ const tenantFilter = rowData?.customerId || rowData?.defaultDomainName || "";
+ const cacheTypeName = formData.Name?.value || formData.Name || "";
+ return {
+ Name: cacheTypeName,
+ TenantFilter: tenantFilter,
+ };
+ },
+ },
+ traceGdapAction,
+ ];
+
+ const offCanvas = {
+ extendedInfoFields: [
+ "displayName",
+ "defaultDomainName",
+ "delegatedPrivilegeStatus",
+ "Excluded",
+ "ExcludeDate",
+ "ExcludeUser",
+ ],
+ actions: actions,
+ };
+
+ const simpleColumns = [
+ "displayName",
+ "defaultDomainName",
+ "delegatedPrivilegeStatus",
+ "Excluded",
+ "ExcludeDate",
+ "ExcludeUser",
+ ];
+
+ const filters = [
+ {
+ filterName: "Included tenants",
+ value: [{ id: "Excluded", value: "No" }],
+ type: "column",
+ },
+ {
+ filterName: "Excluded tenants",
+ value: [{ id: "Excluded", value: "Yes" }],
+ type: "column",
+ },
+ ];
+
+ return (
+ <>
+
+
+
+
+ Force Refresh
+
+ }
+ />
+
+
+ >
+ );
};
Page.getLayout = (page) => (
diff --git a/src/pages/dashboardv2/index.js b/src/pages/dashboardv2/index.js
index eeb351d6f3a2..fcb983b4e36d 100644
--- a/src/pages/dashboardv2/index.js
+++ b/src/pages/dashboardv2/index.js
@@ -100,8 +100,12 @@ const Page = () => {
});
const currentTenantInfo = ApiGetCall({
- url: "/api/ListTenants",
- queryKey: `ListTenants`,
+ url: "/api/listTenants",
+ data: { AllTenantSelector: true },
+ queryKey: "TenantSelector",
+ refetchOnMount: false,
+ refetchOnReconnect: false,
+ keepPreviousData: true,
});
const reportData =
diff --git a/src/pages/identity/administration/offboarding-wizard/index.js b/src/pages/identity/administration/offboarding-wizard/index.js
index 931cfded2eef..6bf7ec6ff0c8 100644
--- a/src/pages/identity/administration/offboarding-wizard/index.js
+++ b/src/pages/identity/administration/offboarding-wizard/index.js
@@ -5,9 +5,30 @@ import { CippTenantStep } from "../../../../components/CippWizard/CippTenantStep
import { CippWizardAutoComplete } from "../../../../components/CippWizard/CippWizardAutoComplete";
import { CippWizardOffboarding } from "../../../../components/CippWizard/CippWizardOffboarding";
import { useSettings } from "../../../../hooks/use-settings";
+import CippTablePage from "../../../../components/CippComponents/CippTablePage";
+import { PersonOff } from "@mui/icons-material";
+import { Button } from "@mui/material";
+import { useState } from "react";
+import ScheduledTaskDetails from "../../../../components/CippComponents/ScheduledTaskDetails";
+import { CippScheduledTaskActions } from "../../../../components/CippComponents/CippScheduledTaskActions";
+import { CippSchedulerDrawer } from "../../../../components/CippComponents/CippSchedulerDrawer";
const Page = () => {
+ const [wizardOpen, setWizardOpen] = useState(false);
+ const [editTaskId, setEditTaskId] = useState(null);
+ const [cloneTaskId, setCloneTaskId] = useState(null);
const initialState = useSettings();
+ const currentTenant = initialState.currentTenant;
+
+ const drawerHandlers = {
+ openEditDrawer: (row) => setEditTaskId(row.RowKey),
+ openCloneDrawer: (row) => setCloneTaskId(row.RowKey),
+ };
+
+ const actions = CippScheduledTaskActions(drawerHandlers, {
+ hideActions: ["Edit Job", "Clone Job"],
+ });
+
const steps = [
{
title: "Step 1",
@@ -61,10 +82,92 @@ const Page = () => {
},
];
+ const filterList = [
+ {
+ filterName: "Running",
+ value: [{ id: "TaskState", value: "Running" }],
+ type: "column",
+ },
+ {
+ filterName: "Planned",
+ value: [{ id: "TaskState", value: "Planned" }],
+ type: "column",
+ },
+ {
+ filterName: "Failed",
+ value: [{ id: "TaskState", value: "Failed" }],
+ type: "column",
+ },
+ {
+ filterName: "Completed",
+ value: [{ id: "TaskState", value: "Completed" }],
+ type: "column",
+ },
+ ];
+
+ const offCanvas = {
+ children: (extendedData) => (
+
+ ),
+ size: "xl",
+ actions: actions,
+ };
+
return (
<>
+ setWizardOpen(true)} startIcon={}>
+ Start Offboarding
+
+ }
+ title="User Offboarding"
+ apiUrl="/api/ListScheduledItems?Type=Invoke-CIPPOffboardingJob&"
+ queryKey={`OffboardingJobs-${currentTenant}`}
+ actions={actions}
+ simpleColumns={[
+ "Tenant",
+ "Parameters.Username",
+ "TaskState",
+ "ScheduledTime",
+ "ExecutedTime",
+ ]}
+ filters={filterList}
+ offCanvas={offCanvas}
+ />
+
+ {/* Edit Drawer */}
+ {editTaskId && (
+ setEditTaskId(null)}
+ onClose={() => setEditTaskId(null)}
+ PermissionButton={({ children }) => <>{children}>}
+ />
+ )}
+
+ {/* Clone Drawer */}
+ {cloneTaskId && (
+ setCloneTaskId(null)}
+ onClose={() => setCloneTaskId(null)}
+ PermissionButton={({ children }) => <>{children}>}
+ />
+ )}
setWizardOpen(false)}
+ dialogIcon={}
+ relatedQueryKeys={[`OffboardingJobs-${currentTenant}`]}
+ initialState={{
+ ...initialState.offboardingDefaults,
+ ...{ Scheduled: { enabled: false } },
+ }}
steps={steps}
postUrl="/api/ExecOffboardUser"
wizardTitle="User Offboarding Wizard"
diff --git a/src/pages/identity/administration/vacation-mode/index.js b/src/pages/identity/administration/vacation-mode/index.js
index 1b85952156e1..f2caa0ec2468 100644
--- a/src/pages/identity/administration/vacation-mode/index.js
+++ b/src/pages/identity/administration/vacation-mode/index.js
@@ -5,8 +5,12 @@ import { EyeIcon } from "@heroicons/react/24/outline";
import { Button } from "@mui/material";
import Link from "next/link";
import { EventAvailable } from "@mui/icons-material";
+import { useSettings } from "../../../../hooks/use-settings.js";
const Page = () => {
+ const initialState = useSettings();
+ const currentTenant = initialState.currentTenant;
+
const actions = [
{
label: "View Task Details",
@@ -76,17 +80,9 @@ const Page = () => {
}
title="Vacation Mode"
apiUrl="/api/ListScheduledItems?SearchTitle=*Vacation*"
- queryKey="VacationMode"
- tenantInTitle={false}
+ queryKey={`VacationMode-${currentTenant}`}
actions={actions}
- simpleColumns={[
- "Tenant",
- "Name",
- "Reference",
- "TaskState",
- "ScheduledTime",
- "ExecutedTime",
- ]}
+ simpleColumns={["Tenant", "Name", "Reference", "TaskState", "ScheduledTime", "ExecutedTime"]}
filters={filterList}
offCanvas={{
extendedInfoFields: [
diff --git a/src/pages/teams-share/sharepoint/index.js b/src/pages/teams-share/sharepoint/index.js
index b92b04c5c5aa..21cefc406ca4 100644
--- a/src/pages/teams-share/sharepoint/index.js
+++ b/src/pages/teams-share/sharepoint/index.js
@@ -199,11 +199,9 @@ const Page = () => {
title="Site Members"
queryKey={`site-members-${row.siteId}`}
api={{
- url: "/api/ListGraphRequest",
+ url: "/api/ListSiteMembers",
data: {
- Endpoint: `/sites/${row.siteId}/lists/User%20Information%20List/items`,
- AsApp: "true",
- expand: "fields",
+ SiteId: row.siteId,
tenantFilter: tenantFilter,
},
dataKey: "Results",
diff --git a/src/pages/tenant/administration/tenants/groups/index.js b/src/pages/tenant/administration/tenants/groups/index.js
index ca49669462af..5095155edfef 100644
--- a/src/pages/tenant/administration/tenants/groups/index.js
+++ b/src/pages/tenant/administration/tenants/groups/index.js
@@ -2,7 +2,7 @@ import { Layout as DashboardLayout } from "../../../../../layouts/index.js";
import { TabbedLayout } from "../../../../../layouts/TabbedLayout";
import { CippTablePage } from "../../../../../components/CippComponents/CippTablePage.jsx";
import tabOptions from "../tabOptions";
-import { Edit, PlayArrow, GroupAdd } from "@mui/icons-material";
+import { Edit, PlayArrow, GroupAdd, ViewList } from "@mui/icons-material";
import { TrashIcon } from "@heroicons/react/24/outline";
import { CippAddTenantGroupDrawer } from "../../../../../components/CippComponents/CippAddTenantGroupDrawer";
import { CippApiLogsDrawer } from "../../../../../components/CippComponents/CippApiLogsDrawer";
@@ -10,12 +10,16 @@ import { CippTenantGroupOffCanvas } from "../../../../../components/CippComponen
import { CippApiDialog } from "../../../../../components/CippComponents/CippApiDialog.jsx";
import { Box, Button } from "@mui/material";
import { useDialog } from "../../../../../hooks/use-dialog.js";
+import { useState } from "react"
const Page = () => {
const pageTitle = "Tenant Groups";
const createDefaultGroupsDialog = useDialog();
+ const [showUsage, setShowUsage] = useState(false);
- const simpleColumns = ["Name", "Description", "GroupType", "Members"];
+ const simpleColumns = showUsage
+ ? ["Name", "Description", "GroupType", "Members", "Usage"]
+ : ["Name", "Description", "GroupType", "Members"];
const offcanvas = {
children: (row) => {
@@ -57,12 +61,16 @@ const Page = () => {
tenantInTitle={false}
simpleColumns={simpleColumns}
apiUrl="/api/ListTenantGroups"
- queryKey="TenantGroupListPage"
+ apiData={{ includeUsage: showUsage }}
+ queryKey={showUsage ? "TenantGroupListPage-usage" : "TenantGroupListPage"}
apiDataKey="Results"
actions={actions}
cardButton={
+ setShowUsage(!showUsage)} startIcon={}>
+ {showUsage ? "Hide Usage" : "Show Usage"}
+
}>
Create Default Groups
diff --git a/src/pages/tenant/standards/templates/template.jsx b/src/pages/tenant/standards/templates/template.jsx
index a64c30f67276..19cf27c788f2 100644
--- a/src/pages/tenant/standards/templates/template.jsx
+++ b/src/pages/tenant/standards/templates/template.jsx
@@ -398,7 +398,7 @@ const Page = () => {
-
+
{/* Left Column for Accordions */}
diff --git a/src/utils/get-cipp-filter-variant.js b/src/utils/get-cipp-filter-variant.js
index ab59e94bcc32..539fbd464cfb 100644
--- a/src/utils/get-cipp-filter-variant.js
+++ b/src/utils/get-cipp-filter-variant.js
@@ -50,6 +50,12 @@ export const getCippFilterVariant = (providedColumnKeys, arg) => {
}));
}
+ // Add "No Licenses Assigned" option at beginning
+ filterSelectOptions.unshift({
+ label: "No Licenses Assigned",
+ value: "__no_license__",
+ });
+
return {
filterVariant: "multi-select",
sortingFn: "alphanumeric",
@@ -58,11 +64,28 @@ export const getCippFilterVariant = (providedColumnKeys, arg) => {
if (!filterValue || !Array.isArray(filterValue) || filterValue.length === 0) {
return true;
}
- if (!userLicenses || !Array.isArray(userLicenses) || userLicenses.length === 0) {
+
+ const hasNoLicenseFilter = filterValue.includes("__no_license__");
+ const otherFilters = filterValue.filter((v) => v !== "__no_license__");
+ const isUnlicensed = !userLicenses || !Array.isArray(userLicenses) || userLicenses.length === 0;
+
+ // If user selected "No Licenses Assigned" and this user is unlicensed → match
+ if (hasNoLicenseFilter && isUnlicensed) {
+ return true;
+ }
+
+ // If only "No Licenses Assigned" is selected and user has licenses → no match
+ if (hasNoLicenseFilter && otherFilters.length === 0 && !isUnlicensed) {
return false;
}
+
+ // Check other license filters
+ if (isUnlicensed) {
+ return false;
+ }
+
const userSkuIds = userLicenses.map((license) => license.skuId).filter(Boolean);
- return filterValue.some((selectedSkuId) => userSkuIds.includes(selectedSkuId));
+ return otherFilters.some((selectedSkuId) => userSkuIds.includes(selectedSkuId));
},
filterSelectOptions: filterSelectOptions,
};
diff --git a/src/utils/get-cipp-formatting.js b/src/utils/get-cipp-formatting.js
index 538116993b53..217b1ae2a6a1 100644
--- a/src/utils/get-cipp-formatting.js
+++ b/src/utils/get-cipp-formatting.js
@@ -430,7 +430,7 @@ export const getCippFormatting = (data, cellName, type, canReceive, flatten = tr
);
}
- if (cellName === "ClientId" || cellName === "role" || cellName === "appId") {
+ if (cellName === "ClientId" || cellName === "role" || cellName === "appId" || cellName === "SID") {
return isText ? data : ;
}
@@ -643,6 +643,23 @@ export const getCippFormatting = (data, cellName, type, canReceive, flatten = tr
);
}
+ // Handle businessPhones
+ if (cellName === "businessPhones") {
+ if (!Array.isArray(data)) {
+ data = [data];
+ }
+
+ if (data.length === 0) {
+ return isText ? (
+ "No data"
+ ) : (
+
+ );
+ }
+
+ return isText ? data.join(", ") : renderChipList(data);
+ }
+
//handle assignedUsers
if (cellName === "AssignedUsers" || cellName === "assignedUsers") {
//show the display name in text. otherwise, just return the obj.
diff --git a/yarn.lock b/yarn.lock
index 605fa06ca1d0..d9c51cd68ed2 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2255,20 +2255,20 @@
resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.11.2.tgz#00409e743ac4eea9afe5b7708594d5fcebb00212"
integrity sha512-vTtpNt7mKCiZ1pwU9hfKPhpdVO2sVzFQsxoVBGtOSHxlrRRzYr8iQ2TlwbAcRYCcEiZ9ECAM8kBzH0v2+VzfKw==
-"@tiptap/core@^3.20.0", "@tiptap/core@^3.4.1":
- version "3.20.0"
- resolved "https://registry.yarnpkg.com/@tiptap/core/-/core-3.20.0.tgz#dac72894d83829f2fbbabee2e90a748d7c1479ee"
- integrity sha512-aC9aROgia/SpJqhsXFiX9TsligL8d+oeoI8W3u00WI45s0VfsqjgeKQLDLF7Tu7hC+7F02teC84SAHuup003VQ==
+"@tiptap/core@^3.20.1", "@tiptap/core@^3.4.1":
+ version "3.20.1"
+ resolved "https://registry.yarnpkg.com/@tiptap/core/-/core-3.20.1.tgz#3e870175541144cc9ca292c804609f7c43549a8b"
+ integrity sha512-SwkPEWIfaDEZjC8SEIi4kZjqIYUbRgLUHUuQezo5GbphUNC8kM1pi3C3EtoOPtxXrEbY6e4pWEzW54Pcrd+rVA==
-"@tiptap/extension-blockquote@^3.20.0":
- version "3.20.0"
- resolved "https://registry.yarnpkg.com/@tiptap/extension-blockquote/-/extension-blockquote-3.20.0.tgz#92e4a8ed00cf4fcab056766b848fc0a551847e5b"
- integrity sha512-LQzn6aGtL4WXz2+rYshl/7/VnP2qJTpD7fWL96GXAzhqviPEY1bJES7poqJb3MU/gzl8VJUVzVzU1VoVfUKlbA==
+"@tiptap/extension-blockquote@^3.20.1":
+ version "3.20.1"
+ resolved "https://registry.yarnpkg.com/@tiptap/extension-blockquote/-/extension-blockquote-3.20.1.tgz#7991cbe4250f4389c80fc06adc499ad1e65cb7bf"
+ integrity sha512-WzNXk/63PQI2fav4Ta6P0GmYRyu8Gap1pV3VUqaVK829iJ6Zt1T21xayATHEHWMK27VT1GLPJkx9Ycr2jfDyQw==
-"@tiptap/extension-bold@^3.20.0":
- version "3.20.0"
- resolved "https://registry.yarnpkg.com/@tiptap/extension-bold/-/extension-bold-3.20.0.tgz#4298d24cb6c7759f6233eec5e24e9fd7c7efdf38"
- integrity sha512-sQklEWiyf58yDjiHtm5vmkVjfIc/cBuSusmCsQ0q9vGYnEF1iOHKhGpvnCeEXNeqF3fiJQRlquzt/6ymle3Iwg==
+"@tiptap/extension-bold@^3.20.1":
+ version "3.20.1"
+ resolved "https://registry.yarnpkg.com/@tiptap/extension-bold/-/extension-bold-3.20.1.tgz#c5dda7450cb1d575ee3f53fafd02fb0169151df6"
+ integrity sha512-fz++Qv6Rk/Hov0IYG/r7TJ1Y4zWkuGONe0UN5g0KY32NIMg3HeOHicbi4xsNWTm9uAOl3eawWDkezEMrleObMw==
"@tiptap/extension-bubble-menu@^3.13.0":
version "3.13.0"
@@ -2277,127 +2277,127 @@
dependencies:
"@floating-ui/dom" "^1.0.0"
-"@tiptap/extension-bullet-list@^3.20.0":
- version "3.20.0"
- resolved "https://registry.yarnpkg.com/@tiptap/extension-bullet-list/-/extension-bullet-list-3.20.0.tgz#57bcba5988990f39cb71c7901173da9b4979523b"
- integrity sha512-OcKMeopBbqWzhSi6o8nNz0aayogg1sfOAhto3NxJu3Ya32dwBFqmHXSYM6uW4jOphNvVPyjiq9aNRh3qTdd1dw==
+"@tiptap/extension-bullet-list@^3.20.1":
+ version "3.20.1"
+ resolved "https://registry.yarnpkg.com/@tiptap/extension-bullet-list/-/extension-bullet-list-3.20.1.tgz#7184db6e65533904d65d403690784f5fea508208"
+ integrity sha512-mbrlvOZo5OF3vLhp+3fk9KuL/6J/wsN0QxF6ZFRAHzQ9NkJdtdfARcBeBnkWXGN8inB6YxbTGY1/E4lmBkOpOw==
-"@tiptap/extension-code-block@^3.20.0":
- version "3.20.0"
- resolved "https://registry.yarnpkg.com/@tiptap/extension-code-block/-/extension-code-block-3.20.0.tgz#5c00e8ae017c32ff4dd629447635a25fcb9d0f52"
- integrity sha512-lBbmNek14aCjrHcBcq3PRqWfNLvC6bcRa2Osc6e/LtmXlcpype4f6n+Yx+WZ+f2uUh0UmDRCz7BEyUETEsDmlQ==
+"@tiptap/extension-code-block@^3.20.1":
+ version "3.20.1"
+ resolved "https://registry.yarnpkg.com/@tiptap/extension-code-block/-/extension-code-block-3.20.1.tgz#c4b69ff6eb0929700cfd70aedee0754960efb17d"
+ integrity sha512-vKejwBq+Nlj4Ybd3qOyDxIQKzYymdNH+8eXkKwGShk2nfLJIxq69DCyGvmuHgipIO1qcYPJ149UNpGN+YGcdmA==
-"@tiptap/extension-code@^3.20.0":
- version "3.20.0"
- resolved "https://registry.yarnpkg.com/@tiptap/extension-code/-/extension-code-3.20.0.tgz#6aa18bc21ed8a3a6a899653c48add1cecc64783d"
- integrity sha512-TYDWFeSQ9umiyrqsT6VecbuhL8XIHkUhO+gEk0sVvH67ZLwjFDhAIIgWIr1/dbIGPcvMZM19E7xUUhAdIaXaOQ==
+"@tiptap/extension-code@^3.20.1":
+ version "3.20.1"
+ resolved "https://registry.yarnpkg.com/@tiptap/extension-code/-/extension-code-3.20.1.tgz#611b95bb1b583fdca53b39d3abd46165c7fe3721"
+ integrity sha512-509DHINIA/Gg+eTG7TEkfsS8RUiPLH5xZNyLRT0A1oaoaJmECKfrV6aAm05IdfTyqDqz6LW5pbnX6DdUC4keug==
-"@tiptap/extension-document@^3.20.0":
- version "3.20.0"
- resolved "https://registry.yarnpkg.com/@tiptap/extension-document/-/extension-document-3.20.0.tgz#e10f92139c97354ab917f3095d4011d6703528f6"
- integrity sha512-oJfLIG3vAtZo/wg29WiBcyWt22KUgddpP8wqtCE+kY5Dw8znLR9ehNmVWlSWJA5OJUMO0ntAHx4bBT+I2MBd5w==
+"@tiptap/extension-document@^3.20.1":
+ version "3.20.1"
+ resolved "https://registry.yarnpkg.com/@tiptap/extension-document/-/extension-document-3.20.1.tgz#b8cc670096ad755f3c0daa1f5297aa7536947ff0"
+ integrity sha512-9vrqdGmRV7bQCSY3NLgu7UhIwgOCDp4sKqMNsoNRX0aZ021QQMTvBQDPkiRkCf7MNsnWrNNnr52PVnULEn3vFQ==
-"@tiptap/extension-dropcursor@^3.20.0":
- version "3.20.0"
- resolved "https://registry.yarnpkg.com/@tiptap/extension-dropcursor/-/extension-dropcursor-3.20.0.tgz#459d6c5d7e5f4dc1152246901387e000596fbb40"
- integrity sha512-d+cxplRlktVgZPwatnc34IArlppM0IFKS1J5wLk+ba1jidizsbMVh45tP/BTK2flhyfRqcNoB5R0TArhUpbkNQ==
+"@tiptap/extension-dropcursor@^3.20.1":
+ version "3.20.1"
+ resolved "https://registry.yarnpkg.com/@tiptap/extension-dropcursor/-/extension-dropcursor-3.20.1.tgz#c2e3f8536164258c7532da32bb132c082c8e625d"
+ integrity sha512-K18L9FX4znn+ViPSIbTLOGcIaXMx/gLNwAPE8wPLwswbHhQqdiY1zzdBw6drgOc1Hicvebo2dIoUlSXOZsOEcw==
"@tiptap/extension-floating-menu@^3.13.0":
version "3.13.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-floating-menu/-/extension-floating-menu-3.13.0.tgz#03d03292add49d1b380cdb1ff3890b2956d4e3f5"
integrity sha512-OsezV2cMofZM4c13gvgi93IEYBUzZgnu8BXTYZQiQYekz4bX4uulBmLa1KOA9EN71FzS+SoLkXHU0YzlbLjlxA==
-"@tiptap/extension-gapcursor@^3.20.0":
- version "3.20.0"
- resolved "https://registry.yarnpkg.com/@tiptap/extension-gapcursor/-/extension-gapcursor-3.20.0.tgz#df89dd19417c020c6e9529e2db5077d08353e4a1"
- integrity sha512-P/LasfvG9/qFq43ZAlNbAnPnXC+/RJf49buTrhtFvI9Zg0+Lbpjx1oh6oMHB19T88Y28KtrckfFZ8aTSUWDq6w==
+"@tiptap/extension-gapcursor@^3.20.1":
+ version "3.20.1"
+ resolved "https://registry.yarnpkg.com/@tiptap/extension-gapcursor/-/extension-gapcursor-3.20.1.tgz#b1ba47085c3a9deb4d6a899c85ac358b33e37772"
+ integrity sha512-kZOtttV6Ai8VUAgEng3h4WKFbtdSNJ6ps7r0cRPY+FctWhVmgNb/JJwwyC+vSilR7nRENAhrA/Cv/RxVlvLw+g==
-"@tiptap/extension-hard-break@^3.20.0":
- version "3.20.0"
- resolved "https://registry.yarnpkg.com/@tiptap/extension-hard-break/-/extension-hard-break-3.20.0.tgz#fff1553d3b41ad32d3979d618f0f894cee926f46"
- integrity sha512-rqvhMOw4f+XQmEthncbvDjgLH6fz8L9splnKZC7OeS0eX8b0qd7+xI1u5kyxF3KA2Z0BnigES++jjWuecqV6mA==
+"@tiptap/extension-hard-break@^3.20.1":
+ version "3.20.1"
+ resolved "https://registry.yarnpkg.com/@tiptap/extension-hard-break/-/extension-hard-break-3.20.1.tgz#5cb4e0892f05f4658d948831afcc3c6121147c77"
+ integrity sha512-9sKpmg/IIdlLXimYWUZ3PplIRcehv4Oc7V1miTqlnAthMzjMqigDkjjgte4JZV67RdnDJTQkRw8TklCAU28Emg==
-"@tiptap/extension-heading@^3.20.0", "@tiptap/extension-heading@^3.4.1":
- version "3.20.0"
- resolved "https://registry.yarnpkg.com/@tiptap/extension-heading/-/extension-heading-3.20.0.tgz#a16dbb625d91556399912fa110b04b2ad2dfd2e3"
- integrity sha512-JgJhurnCe3eN6a0lEsNQM/46R1bcwzwWWZEFDSb1P9dR8+t1/5v7cMZWsSInpD7R4/74iJn0+M5hcXLwCmBmYA==
+"@tiptap/extension-heading@^3.20.1", "@tiptap/extension-heading@^3.4.1":
+ version "3.20.1"
+ resolved "https://registry.yarnpkg.com/@tiptap/extension-heading/-/extension-heading-3.20.1.tgz#e626c7ad0b9906d8045372e726a9a277657f3eaa"
+ integrity sha512-unudyfQP6FxnyWinxvPqe/51DG91J6AaJm666RnAubgYMCgym+33kBftx4j4A6qf+ddWYbD00thMNKOnVLjAEQ==
-"@tiptap/extension-horizontal-rule@^3.20.0":
- version "3.20.0"
- resolved "https://registry.yarnpkg.com/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.20.0.tgz#53ffe2f9b9627f27f85b02d75878583dfb044107"
- integrity sha512-6uvcutFMv+9wPZgptDkbRDjAm3YVxlibmkhWD5GuaWwS9L/yUtobpI3GycujRSUZ8D3q6Q9J7LqpmQtQRTalWA==
+"@tiptap/extension-horizontal-rule@^3.20.1":
+ version "3.20.1"
+ resolved "https://registry.yarnpkg.com/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.20.1.tgz#a7c53eeffce44d20106e3fd84a17dd3b65f887fd"
+ integrity sha512-rjFKFXNntdl0jay8oIGFvvykHlpyQTLmrH3Ag2fj3i8yh6MVvqhtaDomYQbw5sxECd5hBkL+T4n2d2DRuVw/QQ==
"@tiptap/extension-image@^3.4.1":
version "3.13.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-image/-/extension-image-3.13.0.tgz#55edb952e86c2ebed436cd53def8b2e743d71d7e"
integrity sha512-223uzLUkIa1rkK7aQK3AcIXe6LbCtmnpVb7sY5OEp+LpSaSPyXwyrZ4A0EO1o98qXG68/0B2OqMntFtA9c5Fbw==
-"@tiptap/extension-italic@^3.20.0":
- version "3.20.0"
- resolved "https://registry.yarnpkg.com/@tiptap/extension-italic/-/extension-italic-3.20.0.tgz#176c2080a75082d296797618c2ed84e9defc7ef9"
- integrity sha512-/DhnKQF8yN8RxtuL8abZ28wd5281EaGoE2Oha35zXSOF1vNYnbyt8Ymkv/7u1BcWEWTvRPgaju0YCGXisPRLYw==
+"@tiptap/extension-italic@^3.20.1":
+ version "3.20.1"
+ resolved "https://registry.yarnpkg.com/@tiptap/extension-italic/-/extension-italic-3.20.1.tgz#9ddd60cd41c5ad4b614909ab2cab385c886c1d4b"
+ integrity sha512-ZYRX13Kt8tR8JOzSXirH3pRpi8x30o7LHxZY58uXBdUvr3tFzOkh03qbN523+diidSVeHP/aMd/+IrplHRkQug==
-"@tiptap/extension-link@^3.20.0":
- version "3.20.0"
- resolved "https://registry.yarnpkg.com/@tiptap/extension-link/-/extension-link-3.20.0.tgz#b3f2d89aabb88ed1eb66925e59fe82d1d2b866e9"
- integrity sha512-qI/5A+R0ZWBxo/8HxSn1uOyr7odr3xHBZ/gzOR1GUJaZqjlJxkWFX0RtXMbLKEGEvT25o345cF7b0wFznEh8qA==
+"@tiptap/extension-link@^3.20.1":
+ version "3.20.1"
+ resolved "https://registry.yarnpkg.com/@tiptap/extension-link/-/extension-link-3.20.1.tgz#e9cf33ae4a30b3ceb41a1aa7f923cce9fb5809ca"
+ integrity sha512-oYTTIgsQMqpkSnJAuAc+UtIKMuI4lv9e1y4LfI1iYm6NkEUHhONppU59smhxHLzb3Ww7YpDffbp5IgDTAiJztA==
dependencies:
linkifyjs "^4.3.2"
-"@tiptap/extension-list-item@^3.20.0":
- version "3.20.0"
- resolved "https://registry.yarnpkg.com/@tiptap/extension-list-item/-/extension-list-item-3.20.0.tgz#60ec36a3326d3bf28982b653b63f6cab5c7d9d9f"
- integrity sha512-qEtjaaGPuqaFB4VpLrGDoIe9RHnckxPfu6d3rc22ap6TAHCDyRv05CEyJogqccnFceG/v5WN4znUBER8RWnWHA==
+"@tiptap/extension-list-item@^3.20.1":
+ version "3.20.1"
+ resolved "https://registry.yarnpkg.com/@tiptap/extension-list-item/-/extension-list-item-3.20.1.tgz#fb48984d8fbd776dc85d39641047d3bfc103a8dd"
+ integrity sha512-tzgnyTW82lYJkUnadYbatwkI9dLz/OWRSWuFpQPRje/ItmFMWuQ9c9NDD8qLbXPdEYnvrgSAA+ipCD/1G0qA0Q==
-"@tiptap/extension-list-keymap@^3.20.0":
- version "3.20.0"
- resolved "https://registry.yarnpkg.com/@tiptap/extension-list-keymap/-/extension-list-keymap-3.20.0.tgz#3e49292b0cdf0a7b6d8f08ae5acdd7014c15cdc1"
- integrity sha512-Z4GvKy04Ms4cLFN+CY6wXswd36xYsT2p/YL0V89LYFMZTerOeTjFYlndzn6svqL8NV1PRT5Diw4WTTxJSmcJPA==
+"@tiptap/extension-list-keymap@^3.20.1":
+ version "3.20.1"
+ resolved "https://registry.yarnpkg.com/@tiptap/extension-list-keymap/-/extension-list-keymap-3.20.1.tgz#8737d2b11e222376b12ad5d6f063ff1cf45251e6"
+ integrity sha512-Dr0xsQKx0XPOgDg7xqoWwfv7FFwZ3WeF3eOjqh3rDXlNHMj1v+UW5cj1HLphrsAZHTrVTn2C+VWPJkMZrSbpvQ==
-"@tiptap/extension-list@^3.20.0":
- version "3.20.0"
- resolved "https://registry.yarnpkg.com/@tiptap/extension-list/-/extension-list-3.20.0.tgz#17d379fe34e09a9b42ab620f7ff571826485c7d5"
- integrity sha512-+V0/gsVWAv+7vcY0MAe6D52LYTIicMSHw00wz3ISZgprSb2yQhJ4+4gurOnUrQ4Du3AnRQvxPROaofwxIQ66WQ==
+"@tiptap/extension-list@^3.20.1":
+ version "3.20.1"
+ resolved "https://registry.yarnpkg.com/@tiptap/extension-list/-/extension-list-3.20.1.tgz#cfbfeb8b66139598501467d9b0ec889821a79bd9"
+ integrity sha512-euBRAn0mkV7R2VEE+AuOt3R0j9RHEMFXamPFmtvTo8IInxDClusrm6mJoDjS8gCGAXsQCRiAe1SCQBPgGbOOwg==
-"@tiptap/extension-ordered-list@^3.20.0":
- version "3.20.0"
- resolved "https://registry.yarnpkg.com/@tiptap/extension-ordered-list/-/extension-ordered-list-3.20.0.tgz#ec449716854d496ef7c1ea9243c2c467fa5d3cb1"
- integrity sha512-jVKnJvrizLk7etwBMfyoj6H2GE4M+PD4k7Bwp6Bh1ohBWtfIA1TlngdS842Mx5i1VB2e3UWIwr8ZH46gl6cwMA==
+"@tiptap/extension-ordered-list@^3.20.1":
+ version "3.20.1"
+ resolved "https://registry.yarnpkg.com/@tiptap/extension-ordered-list/-/extension-ordered-list-3.20.1.tgz#749b514ac42b1315d637d5d29c692f6e2344e428"
+ integrity sha512-Y+3Ad7OwAdagqdYwCnbqf7/to5ypD4NnUNHA0TXRCs7cAHRA8AdgPoIcGFpaaSpV86oosNU3yfeJouYeroffog==
-"@tiptap/extension-paragraph@^3.20.0":
- version "3.20.0"
- resolved "https://registry.yarnpkg.com/@tiptap/extension-paragraph/-/extension-paragraph-3.20.0.tgz#b1778746021ac38894287d62d9429ac309a632ef"
- integrity sha512-mM99zK4+RnEXIMCv6akfNATAs0Iija6FgyFA9J9NZ6N4o8y9QiNLLa6HjLpAC+W+VoCgQIekyoF/Q9ftxmAYDQ==
+"@tiptap/extension-paragraph@^3.20.1":
+ version "3.20.1"
+ resolved "https://registry.yarnpkg.com/@tiptap/extension-paragraph/-/extension-paragraph-3.20.1.tgz#0c2ee1cdf9f3bacf713ce4db0f47abcb393c29c8"
+ integrity sha512-QFrAtXNyv7JSnomMQc1nx5AnG9mMznfbYJAbdOQYVdbLtAzTfiTuNPNbQrufy5ZGtGaHxDCoaygu2QEfzaKG+Q==
-"@tiptap/extension-strike@^3.20.0":
- version "3.20.0"
- resolved "https://registry.yarnpkg.com/@tiptap/extension-strike/-/extension-strike-3.20.0.tgz#a1ec09a1a56aad98d6e7dc493d77547cad4403ee"
- integrity sha512-0vcTZRRAiDfon3VM1mHBr9EFmTkkUXMhm0Xtdtn0bGe+sIqufyi+hUYTEw93EQOD9XNsPkrud6jzQNYpX2H3AQ==
+"@tiptap/extension-strike@^3.20.1":
+ version "3.20.1"
+ resolved "https://registry.yarnpkg.com/@tiptap/extension-strike/-/extension-strike-3.20.1.tgz#b8110518b00aa780fecb70e206ea82a12f447319"
+ integrity sha512-EYgyma10lpsY+rwbVQL9u+gA7hBlKLSMFH7Zgd37FSxukOjr+HE8iKPQQ+SwbGejyDsPlLT8Z5Jnuxo5Ng90Pg==
"@tiptap/extension-table@^3.19.0":
version "3.19.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-table/-/extension-table-3.19.0.tgz#a5f9be88e319f60dc7b8df1321f95a31b20fe991"
integrity sha512-Lg8DlkkDUMYE/CcGOxoCWF98B2i7VWh+AGgqlF+XWrHjhlKHfENLRXm1a0vWuyyP3NknRYILoaaZ1s7QzmXKRA==
-"@tiptap/extension-text@^3.20.0":
- version "3.20.0"
- resolved "https://registry.yarnpkg.com/@tiptap/extension-text/-/extension-text-3.20.0.tgz#bdf0ac6e0c638c9dbb99a5a2b5a07db2b8cba1de"
- integrity sha512-tf8bE8tSaOEWabCzPm71xwiUhyMFKqY9jkP5af3Kr1/F45jzZFIQAYZooHI/+zCHRrgJ99MQHKHe1ZNvODrKHQ==
+"@tiptap/extension-text@^3.20.1":
+ version "3.20.1"
+ resolved "https://registry.yarnpkg.com/@tiptap/extension-text/-/extension-text-3.20.1.tgz#f102ee6d28961f4f62a41f589d0cebfa93483c09"
+ integrity sha512-7PlIbYW8UenV6NPOXHmv8IcmPGlGx6HFq66RmkJAOJRPXPkTLAiX0N8rQtzUJ6jDEHqoJpaHFEHJw0xzW1yF+A==
-"@tiptap/extension-underline@^3.20.0":
- version "3.20.0"
- resolved "https://registry.yarnpkg.com/@tiptap/extension-underline/-/extension-underline-3.20.0.tgz#535aebafc9e30da51df3be2c2065c49539535672"
- integrity sha512-LzNXuy2jwR/y+ymoUqC72TiGzbOCjioIjsDu0MNYpHuHqTWPK5aV9Mh0nbZcYFy/7fPlV1q0W139EbJeYBZEAQ==
+"@tiptap/extension-underline@^3.20.1":
+ version "3.20.1"
+ resolved "https://registry.yarnpkg.com/@tiptap/extension-underline/-/extension-underline-3.20.1.tgz#a07f6631a2bc5932e9872b97c22f16c6dd792611"
+ integrity sha512-fmHvDKzwCgnZUwRreq8tYkb1YyEwgzZ6QQkAQ0CsCRtvRMqzerr3Duz0Als4i8voZTuGDEL3VR6nAJbLAb/wPg==
-"@tiptap/extensions@^3.20.0":
- version "3.20.0"
- resolved "https://registry.yarnpkg.com/@tiptap/extensions/-/extensions-3.20.0.tgz#22bad09a1d861446e17e0d9732439940d80ed808"
- integrity sha512-HIsXX942w3nbxEQBlMAAR/aa6qiMBEP7CsSMxaxmTIVAmW35p6yUASw6GdV1u0o3lCZjXq2OSRMTskzIqi5uLg==
+"@tiptap/extensions@^3.20.1":
+ version "3.20.1"
+ resolved "https://registry.yarnpkg.com/@tiptap/extensions/-/extensions-3.20.1.tgz#c95ffd378c2417cf88879ac46e78d4633d6eabe0"
+ integrity sha512-JRc/v+OBH0qLTdvQ7HvHWTxGJH73QOf1MC0R8NhOX2QnAbg2mPFv1h+FjGa2gfLGuCXBdWQomjekWkUKbC4e5A==
-"@tiptap/pm@^3.20.0", "@tiptap/pm@^3.4.1":
- version "3.20.0"
- resolved "https://registry.yarnpkg.com/@tiptap/pm/-/pm-3.20.0.tgz#d9a1b92a1cb061059977952e6ec2afe8dff67857"
- integrity sha512-jn+2KnQZn+b+VXr8EFOJKsnjVNaA4diAEr6FOazupMt8W8ro1hfpYtZ25JL87Kao/WbMze55sd8M8BDXLUKu1A==
+"@tiptap/pm@^3.20.1", "@tiptap/pm@^3.4.1":
+ version "3.20.1"
+ resolved "https://registry.yarnpkg.com/@tiptap/pm/-/pm-3.20.1.tgz#06827f1497c3477147908e25eb91dfa1ce1c4988"
+ integrity sha512-6kCiGLvpES4AxcEuOhb7HR7/xIeJWMjZlb6J7e8zpiIh5BoQc7NoRdctsnmFEjZvC19bIasccshHQ7H2zchWqw==
dependencies:
prosemirror-changeset "^2.3.0"
prosemirror-collab "^1.3.1"
@@ -2430,35 +2430,35 @@
"@tiptap/extension-bubble-menu" "^3.13.0"
"@tiptap/extension-floating-menu" "^3.13.0"
-"@tiptap/starter-kit@^3.20.0":
- version "3.20.0"
- resolved "https://registry.yarnpkg.com/@tiptap/starter-kit/-/starter-kit-3.20.0.tgz#d356f15632c52f90e8eea8b5c912cab3b01b0866"
- integrity sha512-W4+1re35pDNY/7rpXVg+OKo/Fa4Gfrn08Bq3E3fzlJw6gjE3tYU8dY9x9vC2rK9pd9NOp7Af11qCFDaWpohXkw==
- dependencies:
- "@tiptap/core" "^3.20.0"
- "@tiptap/extension-blockquote" "^3.20.0"
- "@tiptap/extension-bold" "^3.20.0"
- "@tiptap/extension-bullet-list" "^3.20.0"
- "@tiptap/extension-code" "^3.20.0"
- "@tiptap/extension-code-block" "^3.20.0"
- "@tiptap/extension-document" "^3.20.0"
- "@tiptap/extension-dropcursor" "^3.20.0"
- "@tiptap/extension-gapcursor" "^3.20.0"
- "@tiptap/extension-hard-break" "^3.20.0"
- "@tiptap/extension-heading" "^3.20.0"
- "@tiptap/extension-horizontal-rule" "^3.20.0"
- "@tiptap/extension-italic" "^3.20.0"
- "@tiptap/extension-link" "^3.20.0"
- "@tiptap/extension-list" "^3.20.0"
- "@tiptap/extension-list-item" "^3.20.0"
- "@tiptap/extension-list-keymap" "^3.20.0"
- "@tiptap/extension-ordered-list" "^3.20.0"
- "@tiptap/extension-paragraph" "^3.20.0"
- "@tiptap/extension-strike" "^3.20.0"
- "@tiptap/extension-text" "^3.20.0"
- "@tiptap/extension-underline" "^3.20.0"
- "@tiptap/extensions" "^3.20.0"
- "@tiptap/pm" "^3.20.0"
+"@tiptap/starter-kit@^3.20.1":
+ version "3.20.1"
+ resolved "https://registry.yarnpkg.com/@tiptap/starter-kit/-/starter-kit-3.20.1.tgz#a97c2d1389f05be22742b29bbe3c6cac9fe2ec70"
+ integrity sha512-opqWxL/4OTEiqmVC0wsU4o3JhAf6LycJ2G/gRIZVAIFLljI9uHfpPMTFGxZ5w9IVVJaP5PJysfwW/635kKqkrw==
+ dependencies:
+ "@tiptap/core" "^3.20.1"
+ "@tiptap/extension-blockquote" "^3.20.1"
+ "@tiptap/extension-bold" "^3.20.1"
+ "@tiptap/extension-bullet-list" "^3.20.1"
+ "@tiptap/extension-code" "^3.20.1"
+ "@tiptap/extension-code-block" "^3.20.1"
+ "@tiptap/extension-document" "^3.20.1"
+ "@tiptap/extension-dropcursor" "^3.20.1"
+ "@tiptap/extension-gapcursor" "^3.20.1"
+ "@tiptap/extension-hard-break" "^3.20.1"
+ "@tiptap/extension-heading" "^3.20.1"
+ "@tiptap/extension-horizontal-rule" "^3.20.1"
+ "@tiptap/extension-italic" "^3.20.1"
+ "@tiptap/extension-link" "^3.20.1"
+ "@tiptap/extension-list" "^3.20.1"
+ "@tiptap/extension-list-item" "^3.20.1"
+ "@tiptap/extension-list-keymap" "^3.20.1"
+ "@tiptap/extension-ordered-list" "^3.20.1"
+ "@tiptap/extension-paragraph" "^3.20.1"
+ "@tiptap/extension-strike" "^3.20.1"
+ "@tiptap/extension-text" "^3.20.1"
+ "@tiptap/extension-underline" "^3.20.1"
+ "@tiptap/extensions" "^3.20.1"
+ "@tiptap/pm" "^3.20.1"
"@trysound/sax@0.2.0":
version "0.2.0"
@@ -4898,12 +4898,12 @@ hyphen@^1.6.4:
resolved "https://registry.yarnpkg.com/hyphen/-/hyphen-1.10.6.tgz#0e779d280e696102b97d7e42f5ca5de2cc97e274"
integrity sha512-fXHXcGFTXOvZTSkPJuGOQf5Lv5T/R2itiiCVPg9LxAje5D00O0pP83yJShFq5V89Ly//Gt6acj7z8pbBr34stw==
-i18next@25.8.13:
- version "25.8.13"
- resolved "https://registry.yarnpkg.com/i18next/-/i18next-25.8.13.tgz#1f9df59329f1706f02b2b58b5d1f75196ddb6e4a"
- integrity sha512-E0vzjBY1yM+nsFrtgkjLhST2NBkirkvOVoQa0MSldhsuZ3jUge7ZNpuwG0Cfc74zwo5ZwRzg3uOgT+McBn32iA==
+i18next@25.8.18:
+ version "25.8.18"
+ resolved "https://registry.yarnpkg.com/i18next/-/i18next-25.8.18.tgz#51863b65bc42e3525271f2680ebbf7d150ff53cc"
+ integrity sha512-lzY5X83BiL5AP77+9DydbrqkQHFN9hUzWGjqjLpPcp5ZOzuu1aSoKaU3xbBLSjWx9dAzW431y+d+aogxOZaKRA==
dependencies:
- "@babel/runtime" "^7.28.4"
+ "@babel/runtime" "^7.28.6"
ignore@^5.2.0:
version "5.3.2"
@@ -6898,10 +6898,10 @@ react-virtualized-auto-sizer@^1.0.26:
resolved "https://registry.yarnpkg.com/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.26.tgz#e9470ef6a778dc4f1d5fd76305fa2d8b610c357a"
integrity sha512-CblNyiNVw2o+hsa5/49NH2ogGxZ+t+3aweRvNSq7TVjDIlwk7ir4lencEg5HxHeSzwNarSkNkiu0qJSOXtxm5A==
-react-virtuoso@^4.18.1:
- version "4.18.1"
- resolved "https://registry.yarnpkg.com/react-virtuoso/-/react-virtuoso-4.18.1.tgz#3eb7078f2739a31b96c723374019e587deeb6ebc"
- integrity sha512-KF474cDwaSb9+SJ380xruBB4P+yGWcVkcu26HtMqYNMTYlYbrNy8vqMkE+GpAApPPufJqgOLMoWMFG/3pJMXUA==
+react-virtuoso@^4.18.3:
+ version "4.18.3"
+ resolved "https://registry.yarnpkg.com/react-virtuoso/-/react-virtuoso-4.18.3.tgz#12e69600c258bc6e6bd31c2516942ef08700deac"
+ integrity sha512-fLz/peHAx4Eu0DLHurFEEI7Y6n5CqEoxBh04rgJM9yMuOJah2a9zWg/MUOmZLcp7zuWYorXq5+5bf3IRgkNvWg==
react-window@^2.2.5:
version "2.2.5"
@@ -7805,10 +7805,10 @@ typescript-eslint@^8.46.0:
"@typescript-eslint/typescript-estree" "8.56.0"
"@typescript-eslint/utils" "8.56.0"
-typescript@5.9.2:
- version "5.9.2"
- resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.2.tgz#d93450cddec5154a2d5cabe3b8102b83316fb2a6"
- integrity sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==
+typescript@5.9.3:
+ version "5.9.3"
+ resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f"
+ integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==
uc.micro@^2.0.0, uc.micro@^2.1.0:
version "2.1.0"