Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
a838aac
Improve Autocomplete popover behavior for multiple mode
JohnDuprey Nov 21, 2025
c7105aa
add tooltips
JohnDuprey Nov 21, 2025
c85f3bc
push yarn lock
JohnDuprey Nov 22, 2025
8d6db15
add version selector
JohnDuprey Nov 22, 2025
788ece4
Add Directory Audits page and tab option
JohnDuprey Nov 22, 2025
5ec683d
casing
JohnDuprey Nov 22, 2025
1622ac1
Refactor GUID resolution logic in CippJSONView
JohnDuprey Nov 22, 2025
b12f546
Add manual pagination and increase page size
JohnDuprey Nov 22, 2025
1d2cfc9
Enhance central search with tab options and permissions
JohnDuprey Nov 22, 2025
3ae7e89
formating
JohnDuprey Nov 22, 2025
f85c7c5
Add diff view support to CippJSONView
JohnDuprey Nov 22, 2025
b1e68cb
Improve error handling in CippApiResults
JohnDuprey Nov 23, 2025
7d50787
Rename and enhance application template actions
JohnDuprey Nov 23, 2025
d1426a7
Update app registration actions and template UI
JohnDuprey Nov 23, 2025
5cd1482
Remove back buttons and add breadcrumb navigation
JohnDuprey Nov 24, 2025
85814bf
Update CippTransportRuleDrawer.jsx
Zacgoose Nov 24, 2025
6e7120f
Support dynamic default form values in CippApiDialog
JohnDuprey Nov 24, 2025
c1c0f97
Improve navigation path matching and menu config
JohnDuprey Nov 24, 2025
5c18ec3
Add hierarchical/history breadcrumb navigation
JohnDuprey Nov 24, 2025
8729c85
Improve breadcrumb title handling and group edit page title
JohnDuprey Nov 24, 2025
dd47ca7
Improve breadcrumb title update and settings init
JohnDuprey Nov 24, 2025
91e8b65
Fix: Add condition for copy sent items based on recipient type and re…
kris6673 Nov 24, 2025
c02c994
Improve breadcrumb navigation for tab pages
JohnDuprey Nov 24, 2025
10c9be8
Add scheduled task filter to API logs drawer
JohnDuprey Nov 24, 2025
24c46ee
Bump version to 8.7.1
JohnDuprey Nov 24, 2025
3b05feb
Merge pull request #4988 from Zacgoose/transport-rule-fix
JohnDuprey Nov 24, 2025
ff9bf81
Merge pull request #4992 from kris6673/copysent
JohnDuprey Nov 24, 2025
c3bbb62
Adjust Box padding in user pages
JohnDuprey Nov 24, 2025
fb37af3
Potential fix for code scanning alert no. 53: Incomplete string escap…
JohnDuprey Nov 24, 2025
c925e7d
Merge pull request #4994 from KelvinTegelaar/dev
JohnDuprey Nov 24, 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
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "cipp",
"version": "8.7.0",
"version": "8.7.1",
"author": "CIPP Contributors",
"homepage": "https://cipp.app/",
"bugs": {
Expand Down Expand Up @@ -89,8 +89,6 @@
"react-leaflet": "5.0.0",
"react-leaflet-markercluster": "^5.0.0-rc.0",
"react-markdown": "10.1.0",
"rehype-raw": "^7.0.0",
"remark-gfm": "^3.0.1",
"react-media-hook": "^0.5.0",
"react-papaparse": "^4.4.0",
"react-quill": "^2.0.0",
Expand All @@ -103,6 +101,8 @@
"redux-devtools-extension": "2.13.9",
"redux-persist": "^6.0.0",
"redux-thunk": "3.1.0",
"rehype-raw": "^7.0.0",
"remark-gfm": "^3.0.1",
"simplebar": "6.3.2",
"simplebar-react": "3.3.2",
"stylis-plugin-rtl": "2.1.1",
Expand All @@ -114,4 +114,4 @@
"eslint": "9.35.0",
"eslint-config-next": "15.5.2"
}
}
}
4 changes: 2 additions & 2 deletions public/version.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"version": "8.7.0"
}
"version": "8.7.1"
}
17 changes: 1 addition & 16 deletions src/components/CippCards/CippPageCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,27 +25,12 @@ const CippPageCard = (props) => {
<Box
sx={{
flexGrow: 1,
py: 4,
pb: 4,
}}
>
<Container maxWidth={cardSize}>
<Stack spacing={2}>
<Stack spacing={2}>
<div>
{!hideBackButton && (
<Button
color="inherit"
onClick={handleBackClick} // Go back to the previous page
startIcon={
<SvgIcon fontSize="small">
<ArrowLeftIcon />
</SvgIcon>
}
>
{backButtonTitle}
</Button>
)}
</div>
{hideTitleText !== true && (
<div>
<Typography variant="h4">{title}</Typography>
Expand Down
2 changes: 1 addition & 1 deletion src/components/CippComponents/CippApiDialog.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export const CippApiDialog = (props) => {
}

const formHook = useForm({
defaultValues: defaultvalues || {},
defaultValues: typeof defaultvalues === "function" ? defaultvalues(row) : defaultvalues || {},
mode: "onChange", // Enable real-time validation
});

Expand Down
9 changes: 7 additions & 2 deletions src/components/CippComponents/CippApiLogsDrawer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const CippApiLogsDrawer = ({
apiFilter = null,
tenantFilter = null,
standardFilter = null,
scheduledTaskFilter = null,
requiredPermissions = [],
PermissionButton = Button,
title = "API Logs",
Expand All @@ -28,7 +29,9 @@ export const CippApiLogsDrawer = ({
// Build the API URL with the filter
const apiUrl = `/api/ListLogs?Filter=true${apiFilter ? `&API=${apiFilter}` : ""}${
tenantFilter ? `&Tenant=${tenantFilter}` : ""
}${standardFilter ? `&StandardTemplateId=${standardFilter}` : ""}`;
}${standardFilter ? `&StandardTemplateId=${standardFilter}` : ""}${
scheduledTaskFilter ? `&ScheduledTaskId=${scheduledTaskFilter}` : ""
}`;

// Define the columns for the logs table
const simpleColumns = [
Expand Down Expand Up @@ -74,7 +77,9 @@ export const CippApiLogsDrawer = ({
url: apiUrl,
dataKey: "",
}}
queryKey={`APILogs-${apiFilter || "All"}`}
queryKey={`APILogs-${apiFilter || "All"}-${tenantFilter || "AllTenants"}-${
standardFilter || "NoStandard"
}-${scheduledTaskFilter || "NoTask"}`}
simpleColumns={simpleColumns}
exportEnabled={true}
offCanvas={{
Expand Down
46 changes: 21 additions & 25 deletions src/components/CippComponents/CippApiResults.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -158,8 +158,27 @@ export const CippApiResults = (props) => {

const allResults = useMemo(() => {
const apiResults = extractAllResults(correctResultObj);

// Also extract error results if there's an error
if (apiObject.isError && apiObject.error) {
const errorResults = extractAllResults(apiObject.error.response.data);
if (errorResults.length > 0) {
// Mark all error results with error severity and merge with success results
return [...apiResults, ...errorResults.map((r) => ({ ...r, severity: "error" }))];
}

// Fallback to getCippError if extraction didn't work
const processedError = getCippError(apiObject.error);
if (typeof processedError === "string") {
return [
...apiResults,
{ text: processedError, copyField: processedError, severity: "error" },
];
}
}

return apiResults;
}, [correctResultObj]);
}, [correctResultObj, apiObject.isError, apiObject.error]);

useEffect(() => {
setErrorVisible(!!apiObject.isError);
Expand Down Expand Up @@ -250,31 +269,8 @@ export const CippApiResults = (props) => {
</Alert>
</Collapse>
)}
{/* Error alert */}
<Collapse in={errorVisible} unmountOnExit>
{apiObject.isError && (
<Alert
sx={alertSx}
variant="filled"
severity="error"
action={
<IconButton
aria-label="close"
color="inherit"
size="small"
onClick={() => setErrorVisible(false)}
>
<Close fontSize="inherit" />
</IconButton>
}
>
{getCippError(apiObject.error)}
</Alert>
)}
</Collapse>

{/* Individual result alerts */}
{apiObject.isSuccess && !errorsOnly && hasVisibleResults && (
{hasVisibleResults && (
<>
{finalResults.map((resultObj) => (
<React.Fragment key={resultObj.id}>
Expand Down
156 changes: 91 additions & 65 deletions src/components/CippComponents/CippAutocomplete.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
createFilterOptions,
TextField,
IconButton,
Tooltip,
} from "@mui/material";
import { useEffect, useState, useMemo, useCallback, useRef } from "react";
import { useSettings } from "../../hooks/use-settings";
Expand Down Expand Up @@ -89,6 +90,7 @@ export const CippAutoComplete = (props) => {
const [offCanvasVisible, setOffCanvasVisible] = useState(false);
const [fullObject, setFullObject] = useState(null);
const [internalValue, setInternalValue] = useState(null); // Track selected value internally
const [open, setOpen] = useState(false); // Control popover open state

// Sync internalValue when external value or defaultValue prop changes (e.g., when editing a form)
useEffect(() => {
Expand Down Expand Up @@ -295,6 +297,15 @@ export const CippAutoComplete = (props) => {
<Autocomplete
ref={autocompleteRef}
key={stableKey}
open={open}
onOpen={() => setOpen(true)}
onClose={(event, reason) => {
// Keep open if Tab was used in multiple mode
if (reason === "selectOption" && multiple && event?.type === "click") {
return;
}
setOpen(false);
}}
disabled={disabled || actionGetRequest.isFetching || isFetching}
popupIcon={
actionGetRequest.isFetching || isFetching ? (
Expand Down Expand Up @@ -425,6 +436,17 @@ export const CippAutoComplete = (props) => {
event.preventDefault();
// Trigger a click on the highlighted option
highlightedOption.click();

// In multiple mode, keep the popover open and refocus
if (multiple) {
setTimeout(() => {
setOpen(true);
const input = autocompleteRef.current?.querySelector("input");
if (input) {
input.focus();
}
}, 50);
}
}
}
}}
Expand All @@ -439,79 +461,83 @@ export const CippAutoComplete = (props) => {
{...other}
/>
{api?.url && api?.showRefresh && (
<IconButton
size="small"
onClick={() => {
actionGetRequest.refetch();
}}
>
<Sync />
</IconButton>
<Tooltip title="Refresh">
<IconButton
size="small"
onClick={() => {
actionGetRequest.refetch();
}}
>
<Sync />
</IconButton>
</Tooltip>
)}
{api?.templateView && (
<IconButton
size="small"
onClick={() => {
// Use internalValue if value prop is not available
const currentValue = value || internalValue;

// Get the full object from the selected value
if (multiple) {
// For multiple selection, get all full objects
const fullObjects = currentValue
.map((v) => {
const valueToFind = v?.value || v;
const found = usedOptions.find((opt) => opt.value === valueToFind);
let rawData = found?.rawData;

// If property is specified, extract and parse JSON from that property
if (rawData && api?.templateView?.property) {
try {
const propertyValue = rawData[api.templateView.property];
if (typeof propertyValue === "string") {
rawData = JSON.parse(propertyValue);
} else {
rawData = propertyValue;
<Tooltip title={`View ${api?.templateView.title}` || "View details"}>
<IconButton
size="small"
onClick={() => {
// Use internalValue if value prop is not available
const currentValue = value || internalValue;

// Get the full object from the selected value
if (multiple) {
// For multiple selection, get all full objects
const fullObjects = currentValue
.map((v) => {
const valueToFind = v?.value || v;
const found = usedOptions.find((opt) => opt.value === valueToFind);
let rawData = found?.rawData;

// If property is specified, extract and parse JSON from that property
if (rawData && api?.templateView?.property) {
try {
const propertyValue = rawData[api.templateView.property];
if (typeof propertyValue === "string") {
rawData = JSON.parse(propertyValue);
} else {
rawData = propertyValue;
}
} catch (e) {
console.error("Failed to parse JSON from property:", e);
// Keep original rawData if parsing fails
}
} catch (e) {
console.error("Failed to parse JSON from property:", e);
// Keep original rawData if parsing fails
}
}

return rawData;
})
.filter(Boolean);
setFullObject(fullObjects);
} else {
// For single selection, get the full object
const valueToFind = currentValue?.value || currentValue;
const selectedOption = usedOptions.find((opt) => opt.value === valueToFind);
let rawData = selectedOption?.rawData || null;

// If property is specified, extract and parse JSON from that property
if (rawData && api?.templateView?.property) {
try {
const propertyValue = rawData[api.templateView.property];
if (typeof propertyValue === "string") {
rawData = JSON.parse(propertyValue);
} else {
rawData = propertyValue;
return rawData;
})
.filter(Boolean);
setFullObject(fullObjects);
} else {
// For single selection, get the full object
const valueToFind = currentValue?.value || currentValue;
const selectedOption = usedOptions.find((opt) => opt.value === valueToFind);
let rawData = selectedOption?.rawData || null;

// If property is specified, extract and parse JSON from that property
if (rawData && api?.templateView?.property) {
try {
const propertyValue = rawData[api.templateView.property];
if (typeof propertyValue === "string") {
rawData = JSON.parse(propertyValue);
} else {
rawData = propertyValue;
}
} catch (e) {
console.error("Failed to parse JSON from property:", e);
// Keep original rawData if parsing fails
}
} catch (e) {
console.error("Failed to parse JSON from property:", e);
// Keep original rawData if parsing fails
}
}

setFullObject(rawData);
}
setOffCanvasVisible(true);
}}
title={api?.templateView.title || "View details"}
>
<Visibility />
</IconButton>
setFullObject(rawData);
}
setOffCanvasVisible(true);
}}
title={api?.templateView.title || "View details"}
>
<Visibility />
</IconButton>
</Tooltip>
)}
</Stack>
)}
Expand Down
Loading