Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
69216ba
Feat: Exchange Retention Policy & Tag Management
Zacgoose Aug 9, 2025
9f3819e
Update HeaderedTabbedLayout.jsx
Zacgoose Aug 12, 2025
8130d91
corrected files layout
Zacgoose Aug 12, 2025
a914b8a
fix rendering issues with standards dialog
JohnDuprey Aug 18, 2025
e90fc8d
Add SendNotificationToUser handling in calendar permissions dialog an…
kris6673 Aug 19, 2025
52d33ed
Enhance CippAutoComplete preselection logic and enable preselected op…
kris6673 Aug 19, 2025
be575eb
Add useRef for preselection handling in CippAutoComplete
kris6673 Aug 19, 2025
36c20d3
Merge pull request #4542 from kris6673/fix-preselect-again
KelvinTegelaar Aug 19, 2025
1d7984f
Merge pull request #4541 from kris6673/issue4538
KelvinTegelaar Aug 19, 2025
dec041f
Merge pull request #4515 from Zacgoose/exchange-retention
KelvinTegelaar Aug 19, 2025
f26a0b8
allow cloning of drift standards
KelvinTegelaar Aug 20, 2025
6bf9707
Merge branch 'dev' of https://github.com/KelvinTegelaar/CIPP into dev
KelvinTegelaar Aug 20, 2025
e894150
Initial plan
Copilot Aug 20, 2025
219fa8c
Initial plan: Remove redundant 'Add Status Page' menu item
Copilot Aug 20, 2025
2155107
Remove redundant 'Add Status Page' menu item - functionality already …
Copilot Aug 20, 2025
04a5648
Fix whoops
kris6673 Aug 20, 2025
e223701
Merge pull request #4545 from kris6673/copilot/fix-4b97f601-e4f9-494f…
KelvinTegelaar Aug 20, 2025
c5eb0e1
clone to drift
KelvinTegelaar Aug 20, 2025
9d6d311
Added pointer to AlertSmtpAuthSuccess
DirkHaex Aug 20, 2025
a7cb450
Update alerts.json
DirkHaex Aug 20, 2025
08fabd7
Add functionality for user to select HaloPSA ticket outcome and add h…
jspern Aug 20, 2025
12135b7
Merge pull request #4550 from N2M-Technology/feat-halopsa-outcomes
JohnDuprey Aug 20, 2025
08b27f9
null safety for property missing from user table
JohnDuprey Aug 20, 2025
3f94728
Merge pull request #4547 from DirkHaex/patch-3
KelvinTegelaar Aug 20, 2025
7187ad0
Fix copy
KelvinTegelaar Aug 20, 2025
956786f
Update version.json
JohnDuprey Aug 20, 2025
dad942c
Merge pull request #4552 from KelvinTegelaar/dev
JohnDuprey Aug 20, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion generate-placeholders.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,6 @@ const pages = [
{ title: "Profiles", path: "/endpoint/autopilot/list-profiles" },
{ title: "Add Profile", path: "/endpoint/autopilot/add-profile" },
{ title: "Status Pages", path: "/endpoint/autopilot/list-status-pages" },
{ title: "Add Status Page", path: "/endpoint/autopilot/add-status-page" },
{ title: "Devices", path: "/endpoint/MEM/devices" },
{ title: "Configuration Policies", path: "/endpoint/MEM/list-policies" },
{ title: "Compliance Policies", path: "/endpoint/MEM/list-compliance-policies" },
Expand Down
2 changes: 1 addition & 1 deletion public/version.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"version": "8.3.1"
"version": "8.3.2"
}
8 changes: 8 additions & 0 deletions src/components/CippCards/CippExchangeInfoCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,14 @@ export const CippExchangeInfoCard = (props) => {
{getCippFormatting(exchangeData?.BlockedForSpam, "BlockedForSpam")}
</Typography>
</Grid>
<Grid size={{ xs: 12, md: 12 }}>
<Typography variant="inherit" color="text.primary" gutterBottom>
Retention Policy:
</Typography>
<Typography variant="inherit">
{getCippFormatting(exchangeData?.RetentionPolicy, "RetentionPolicy")}
</Typography>
</Grid>
</Grid>
)
}
Expand Down
464 changes: 341 additions & 123 deletions src/components/CippCards/CippStandardsDialog.jsx

Large diffs are not rendered by default.

34 changes: 26 additions & 8 deletions src/components/CippComponents/CippAutocomplete.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
TextField,
IconButton,
} from "@mui/material";
import { useEffect, useState, useMemo, useCallback } from "react";
import { useEffect, useState, useMemo, useCallback, useRef } from "react";
import { useSettings } from "../../hooks/use-settings";
import { getCippError } from "../../utils/get-cipp-error";
import { ApiGetCallWithPagination } from "../../api/ApiCall";
Expand Down Expand Up @@ -78,6 +78,7 @@ export const CippAutoComplete = (props) => {

const [usedOptions, setUsedOptions] = useState(options);
const [getRequestInfo, setGetRequestInfo] = useState({ url: "", waiting: false, queryKey: "" });
const hasPreselectedRef = useRef(false);
const filter = createFilterOptions({
stringify: (option) => JSON.stringify(option),
});
Expand Down Expand Up @@ -207,15 +208,32 @@ export const CippAutoComplete = (props) => {
return finalOptions;
}, [api, usedOptions, options, removeOptions, sortOptions]);

// Dedicated effect for handling preselected value
// Dedicated effect for handling preselected value - only runs once
useEffect(() => {
if (preselectedValue && !defaultValue && !value && memoizedOptions.length > 0) {
const preselectedOption = memoizedOptions.find((option) => option.value === preselectedValue);
if (preselectedValue && memoizedOptions.length > 0 && !hasPreselectedRef.current) {
// Check if we should skip preselection due to existing defaultValue
const hasDefaultValue =
defaultValue && (Array.isArray(defaultValue) ? defaultValue.length > 0 : true);

if (preselectedOption) {
const newValue = multiple ? [preselectedOption] : preselectedOption;
if (onChange) {
onChange(newValue, newValue?.addedFields);
if (!hasDefaultValue) {
// For multiple mode, check if value is empty array or null/undefined
// For single mode, check if value is null/undefined
const shouldPreselect = multiple
? !value || (Array.isArray(value) && value.length === 0)
: !value;

if (shouldPreselect) {
const preselectedOption = memoizedOptions.find(
(option) => option.value === preselectedValue
);

if (preselectedOption) {
const newValue = multiple ? [preselectedOption] : preselectedOption;
hasPreselectedRef.current = true; // Mark that we've preselected
if (onChange) {
onChange(newValue, newValue?.addedFields);
}
}
}
}
}
Expand Down
32 changes: 32 additions & 0 deletions src/components/CippComponents/CippCalendarPermissionsDialog.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,15 @@ const CippCalendarPermissionsDialog = ({ formHook, combinedOptions, isUserGroupL
}
}, [isEditor, formHook]);

// default SendNotificationToUser to false on mount
useEffect(() => {
formHook.setValue("SendNotificationToUser", false);
}, [formHook]);

// Only certain permission levels support sending a notification when calendar permissions are added
const notifyAllowed = ["AvailabilityOnly", "LimitedDetails", "Reviewer", "Editor"];
const isNotifyAllowed = notifyAllowed.includes(permissionLevel?.value ?? permissionLevel);

return (
<Stack spacing={3} sx={{ mt: 1 }}>
<Box>
Expand Down Expand Up @@ -80,6 +89,29 @@ const CippCalendarPermissionsDialog = ({ formHook, combinedOptions, isUserGroupL
</span>
</Tooltip>
</Box>

<Box>
<Tooltip
title={
!isNotifyAllowed
? `Send notification is only supported for: ${notifyAllowed.join(", ")}`
: ""
}
followCursor
placement="right"
>
<span>
<CippFormComponent
type="switch"
label="Send notification"
name="SendNotificationToUser"
formControl={formHook}
disabled={!isNotifyAllowed}
sx={{ ml: 1.5, mt: 0, mb: 0 }}
/>
</span>
</Tooltip>
</Box>
</Stack>
);
};
Expand Down
45 changes: 45 additions & 0 deletions src/components/CippComponents/CippExchangeActions.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,51 @@ export const CippExchangeActions = () => {
multiPost: false,
condition: (row) => row.ArchiveGuid === "00000000-0000-0000-0000-000000000000",
},
{
label: "Set Retention Policy",
type: "POST",
url: "/api/ExecSetMailboxRetentionPolicies",
icon: <MailLock />,
confirmText: "Set the specified retention policy for selected mailboxes?",
multiPost: false,
fields: [
{
type: "autoComplete",
name: "policyName",
label: "Retention Policy",
multiple: false,
creatable: false,
validators: { required: "Please select a retention policy" },
api: {
url: "/api/ExecManageRetentionPolicies",
labelField: "Name",
valueField: "Name",
queryKey: `RetentionPolicies-${tenant}`,
data: {
tenantFilter: tenant,
},
},
},
],
customDataformatter: (rows, action, formData) => {
const mailboxArray = Array.isArray(rows) ? rows : [rows];

// Extract mailbox identities - using UPN as the identifier
const mailboxes = mailboxArray.map(mailbox => mailbox.UPN);

// Handle autocomplete selection - could be string or object
const policyName = typeof formData.policyName === 'object'
? formData.policyName.value
: formData.policyName;

return {
PolicyName: policyName,
Mailboxes: mailboxes,
tenantFilter: tenant
};
},
color: "primary",
},
{
label: "Enable Auto-Expanding Archive",
type: "POST",
Expand Down
1 change: 1 addition & 0 deletions src/components/CippComponents/CippPolicyDeployDrawer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ export const CippPolicyDeployDrawer = ({
required={true}
disableClearable={false}
allTenants={true}
preselectedEnabled={true}
type="multiple"
/>
<CippFormComponent
Expand Down
22 changes: 12 additions & 10 deletions src/components/CippComponents/CippUserActions.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -435,7 +435,7 @@ export const CippUserActions = () => {
},
confirmText: "Are you sure you want to clear the Immutable ID for [userPrincipalName]?",
multiPost: false,
condition: (row) => !row.onPremisesSyncEnabled && row?.onPremisesImmutableId && canWriteUser,
condition: (row) => !row?.onPremisesSyncEnabled && row?.onPremisesImmutableId && canWriteUser,
},
{
label: "Revoke all user sessions",
Expand Down Expand Up @@ -465,17 +465,19 @@ export const CippUserActions = () => {
customFunction: (users, action, formData) => {
// Handle both single user and multiple users
const userData = Array.isArray(users) ? users : [users];

// Store users in session storage to avoid URL length limits
sessionStorage.setItem('patchWizardUsers', JSON.stringify(userData));
sessionStorage.setItem("patchWizardUsers", JSON.stringify(userData));

// Use Next.js router for internal navigation
import('next/router').then(({ default: router }) => {
router.push('/identity/administration/users/patch-wizard');
}).catch(() => {
// Fallback to window.location if router is not available
window.location.href = '/identity/administration/users/patch-wizard';
});
import("next/router")
.then(({ default: router }) => {
router.push("/identity/administration/users/patch-wizard");
})
.catch(() => {
// Fallback to window.location if router is not available
window.location.href = "/identity/administration/users/patch-wizard";
});
},
condition: () => canWriteUser,
},
Expand Down
106 changes: 60 additions & 46 deletions src/components/CippTable/CippDataTable.js
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,20 @@ export const CippDataTable = (props) => {
};

const table = useMaterialReactTable({
muiTableBodyCellProps: {
onCopy: (e) => {
const sel = window.getSelection()?.toString() ?? "";
if (sel) {
e.preventDefault();
e.stopPropagation();
e.nativeEvent?.stopImmediatePropagation?.();
e.clipboardData.setData("text/plain", sel);
if (navigator.clipboard?.writeText) {
navigator.clipboard.writeText(sel).catch(() => {});
}
}
},
},
mrtTheme: (theme) => ({
baseBackgroundColor: theme.palette.background.paper,
}),
Expand All @@ -215,66 +229,66 @@ export const CippDataTable = (props) => {
muiTableHeadCellProps: {
sx: {
// Target the filter row cells
'& .MuiTableCell-root': {
padding: '8px 16px',
"& .MuiTableCell-root": {
padding: "8px 16px",
},
// Target the Autocomplete component in filter cells
'& .MuiAutocomplete-root': {
width: '100%',
"& .MuiAutocomplete-root": {
width: "100%",
},
// Force the tags container to be single line with ellipsis
'& .MuiAutocomplete-root .MuiInputBase-root': {
height: '40px !important',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
display: 'flex',
flexWrap: 'nowrap',
"& .MuiAutocomplete-root .MuiInputBase-root": {
height: "40px !important",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
display: "flex",
flexWrap: "nowrap",
},
// Target the tags container specifically
'& .MuiAutocomplete-root .MuiInputBase-root .MuiInputBase-input': {
height: '24px',
minHeight: '24px',
maxHeight: '24px',
"& .MuiAutocomplete-root .MuiInputBase-root .MuiInputBase-input": {
height: "24px",
minHeight: "24px",
maxHeight: "24px",
},
// Target regular input fields (not in Autocomplete)
'& .MuiInputBase-root': {
height: '40px !important',
"& .MuiInputBase-root": {
height: "40px !important",
},
// Ensure all input fields have consistent styling
'& .MuiInputBase-input': {
height: '24px',
minHeight: '24px',
maxHeight: '24px',
"& .MuiInputBase-input": {
height: "24px",
minHeight: "24px",
maxHeight: "24px",
},
// Target the specific chip class mentioned
'& .MuiChip-label.MuiChip-labelMedium': {
maxWidth: '80px',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
padding: '0 4px',
"& .MuiChip-label.MuiChip-labelMedium": {
maxWidth: "80px",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
padding: "0 4px",
},
// Make chips smaller overall and add title attribute for tooltip
'& .MuiChip-root': {
height: '24px',
maxHeight: '24px',
"& .MuiChip-root": {
height: "24px",
maxHeight: "24px",
// This adds a tooltip effect using the browser's native tooltip
'&::before': {
content: 'attr(data-label)',
display: 'none',
"&::before": {
content: "attr(data-label)",
display: "none",
},
'&:hover::before': {
display: 'block',
position: 'absolute',
top: '-25px',
left: '0',
backgroundColor: 'rgba(0, 0, 0, 0.8)',
color: 'white',
padding: '4px 8px',
borderRadius: '4px',
fontSize: '12px',
whiteSpace: 'nowrap',
"&:hover::before": {
display: "block",
position: "absolute",
top: "-25px",
left: "0",
backgroundColor: "rgba(0, 0, 0, 0.8)",
color: "white",
padding: "4px 8px",
borderRadius: "4px",
fontSize: "12px",
whiteSpace: "nowrap",
zIndex: 9999,
},
},
Expand Down Expand Up @@ -570,7 +584,7 @@ export const CippDataTable = (props) => {
</Scrollbar>
) : (
// Render the table inside a Card
(<Card style={{ width: "100%" }} {...props.cardProps}>
<Card style={{ width: "100%" }} {...props.cardProps}>
{cardButton || !hideTitle ? (
<>
<CardHeader action={cardButton} title={hideTitle ? "" : title} />
Expand Down Expand Up @@ -602,7 +616,7 @@ export const CippDataTable = (props) => {
)}
</Scrollbar>
</CardContent>
</Card>)
</Card>
)}
<CippOffCanvas
isFetching={getRequestData.isFetching}
Expand Down
Loading