diff --git a/.gitignore b/.gitignore
index 5ee28a7a617b..97735ad0415c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -31,3 +31,4 @@ app.log
# AI rules
.*/rules
+AGENTS.md
diff --git a/package.json b/package.json
index c408d29c4b78..1444ad6102ca 100644
--- a/package.json
+++ b/package.json
@@ -89,6 +89,8 @@
"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",
@@ -112,4 +114,4 @@
"eslint": "9.35.0",
"eslint-config-next": "15.5.2"
}
-}
\ No newline at end of file
+}
diff --git a/public/version.json b/public/version.json
index d0e25ec56792..c9486452da33 100644
--- a/public/version.json
+++ b/public/version.json
@@ -1,3 +1,3 @@
{
- "version": "8.5.2"
-}
\ No newline at end of file
+ "version": "8.6.0"
+}
diff --git a/src/api/ApiCall.jsx b/src/api/ApiCall.jsx
index c069208c2dc4..67f602114c3c 100644
--- a/src/api/ApiCall.jsx
+++ b/src/api/ApiCall.jsx
@@ -21,6 +21,8 @@ export function ApiGetCall(props) {
refetchOnReconnect = true,
keepPreviousData = false,
refetchInterval = false,
+ responseType = "json",
+ convertToDataUrl = false,
} = props;
const queryClient = useQueryClient();
const dispatch = useDispatch();
@@ -107,9 +109,22 @@ export function ApiGetCall(props) {
headers: {
"Content-Type": "application/json",
},
+ responseType: responseType,
});
+
+ let responseData = response.data;
+
+ // Convert blob to data URL if requested
+ if (convertToDataUrl && responseType === "blob" && response.data) {
+ responseData = await new Promise((resolve) => {
+ const reader = new FileReader();
+ reader.onloadend = () => resolve(reader.result);
+ reader.readAsDataURL(response.data);
+ });
+ }
+
if (onResult) {
- onResult(response.data); // Emit each result as it arrives
+ onResult(responseData); // Emit each result as it arrives
}
if (relatedQueryKeys) {
const clearKeys = Array.isArray(relatedQueryKeys) ? relatedQueryKeys : [relatedQueryKeys];
@@ -137,7 +152,7 @@ export function ApiGetCall(props) {
});
}, 1000);
}
- return response.data;
+ return responseData;
}
},
staleTime: staleTime,
diff --git a/src/components/CippComponents/CippAddEditTenantGroups.jsx b/src/components/CippComponents/CippAddEditTenantGroups.jsx
index 35696ef1fe52..9ba637d003c8 100644
--- a/src/components/CippComponents/CippAddEditTenantGroups.jsx
+++ b/src/components/CippComponents/CippAddEditTenantGroups.jsx
@@ -1,53 +1,96 @@
import CippFormComponent from "/src/components/CippComponents/CippFormComponent";
-import { Stack, Typography } from "@mui/material";
-import CippFormSection from "/src/components/CippFormPages/CippFormSection";
+import { Typography } from "@mui/material";
+import { Grid } from "@mui/system";
import { CippFormTenantSelector } from "./CippFormTenantSelector";
+import { CippFormCondition } from "./CippFormCondition";
+import CippTenantGroupRuleBuilder from "./CippTenantGroupRuleBuilder";
-const CippAddEditTenantGroups = ({ formControl, initialValues, title, backButtonTitle }) => {
+const CippAddEditTenantGroups = ({ formControl, initialValues, title, backButtonTitle, hideSubmitButton = false }) => {
return (
- {
- return {
- ...values,
- Action: "AddEdit",
- };
- }}
- initialValues={initialValues}
- >
+ <>
Properties
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+ {/* Group Type Selection */}
+
+
+
+
+ {/* Static Group Members - Show only when Static is selected */}
+
+
+
+
+
+
+ {/* Dynamic Group Rules - Show only when Dynamic is selected */}
+
+
+
+
+
+
+ >
);
};
diff --git a/src/components/CippComponents/CippAddTenantAllowBlockListDrawer.jsx b/src/components/CippComponents/CippAddTenantAllowBlockListDrawer.jsx
new file mode 100644
index 000000000000..23fbe252ae55
--- /dev/null
+++ b/src/components/CippComponents/CippAddTenantAllowBlockListDrawer.jsx
@@ -0,0 +1,378 @@
+import { useEffect, useState } from "react";
+import { Button, Divider } from "@mui/material";
+import { Grid } from "@mui/system";
+import { useForm, useFormState, useWatch } from "react-hook-form";
+import { PlaylistAdd } from "@mui/icons-material";
+import { CippOffCanvas } from "./CippOffCanvas";
+import CippFormComponent from "./CippFormComponent";
+import { CippFormTenantSelector } from "./CippFormTenantSelector";
+import { CippApiResults } from "./CippApiResults";
+import { ApiPostCall } from "../../api/ApiCall";
+import { getCippValidator } from "../../utils/get-cipp-validator";
+
+const defaultValues = {
+ tenantID: [],
+ entries: "",
+ notes: "",
+ listType: null,
+ listMethod: null,
+ NoExpiration: false,
+ RemoveAfter: false,
+};
+
+export const CippAddTenantAllowBlockListDrawer = ({
+ buttonText = "Add Entry",
+ requiredPermissions = [],
+ PermissionButton = Button,
+}) => {
+ const [drawerVisible, setDrawerVisible] = useState(false);
+ const formControl = useForm({
+ mode: "onChange",
+ defaultValues,
+ });
+
+ const { isValid } = useFormState({ control: formControl.control });
+
+ const noExpiration = useWatch({ control: formControl.control, name: "NoExpiration" });
+ const removeAfter = useWatch({ control: formControl.control, name: "RemoveAfter" });
+ const listMethod = useWatch({ control: formControl.control, name: "listMethod" });
+ const listType = useWatch({ control: formControl.control, name: "listType" });
+
+ const isListMethodBlock = listMethod?.value === "Block";
+ const isListTypeFileHash = listType?.value === "FileHash";
+ const isListTypeSenderUrlOrFileHash = ["Sender", "Url", "FileHash"].includes(listType?.value);
+ const isNoExpirationCompatible =
+ isListMethodBlock || (listMethod?.value === "Allow" && ["Url", "IP"].includes(listType?.value));
+
+ const addEntry = ApiPostCall({});
+
+ useEffect(() => {
+ if (noExpiration && formControl.getValues("RemoveAfter")) {
+ formControl.setValue("RemoveAfter", false, { shouldValidate: true });
+ }
+
+ if (removeAfter && formControl.getValues("NoExpiration")) {
+ formControl.setValue("NoExpiration", false, { shouldValidate: true });
+ }
+
+ if (isListMethodBlock && formControl.getValues("RemoveAfter")) {
+ formControl.setValue("RemoveAfter", false, { shouldValidate: true });
+ }
+
+ if (listType && !isListTypeSenderUrlOrFileHash && formControl.getValues("RemoveAfter")) {
+ formControl.setValue("RemoveAfter", false, { shouldValidate: true });
+ }
+
+ if (isListTypeFileHash && listMethod?.value !== "Block") {
+ formControl.setValue(
+ "listMethod",
+ { label: "Block", value: "Block" },
+ { shouldValidate: true }
+ );
+ }
+
+ if ((listMethod || listType) && noExpiration && !isNoExpirationCompatible) {
+ formControl.setValue("NoExpiration", false, { shouldValidate: true });
+ }
+ }, [
+ noExpiration,
+ removeAfter,
+ isListMethodBlock,
+ listType,
+ isListTypeSenderUrlOrFileHash,
+ isListTypeFileHash,
+ isNoExpirationCompatible,
+ listMethod,
+ formControl,
+ ]);
+
+ useEffect(() => {
+ if (addEntry.isSuccess) {
+ const currentTenants = formControl.getValues("tenantID");
+ formControl.reset({
+ ...defaultValues,
+ tenantID: currentTenants,
+ });
+ }
+ }, [addEntry.isSuccess, formControl]);
+
+ const validateEntries = (value) => {
+ if (!value) return true;
+
+ const entries = value
+ .split(/[,;]/)
+ .map((entry) => entry.trim())
+ .filter(Boolean);
+ const currentListType = listType?.value;
+
+ if (currentListType === "FileHash") {
+ for (const entry of entries) {
+ if (entry.length !== 64) return "File hash entries must be exactly 64 characters";
+
+ const hashResult = getCippValidator(entry, "sha256");
+ if (hashResult !== true) return hashResult;
+ }
+ return true;
+ }
+
+ if (currentListType === "IP") {
+ for (const entry of entries) {
+ const ipv6Result = getCippValidator(entry, "ipv6");
+ const ipv6CidrResult = getCippValidator(entry, "ipv6cidr");
+
+ if (ipv6Result !== true && ipv6CidrResult !== true) {
+ return "Invalid IPv6 address format. Use colon-hexadecimal or CIDR notation";
+ }
+ }
+ return true;
+ }
+
+ if (currentListType === "Url") {
+ for (const entry of entries) {
+ if (entry.length > 250) {
+ return "URL entries must be 250 characters or less";
+ }
+
+ if (entry.includes("*") || entry.includes("~")) {
+ const wildcardUrlResult = getCippValidator(entry, "wildcardUrl");
+ const wildcardDomainResult = getCippValidator(entry, "wildcardDomain");
+
+ if (wildcardUrlResult === true || wildcardDomainResult === true) {
+ continue;
+ }
+
+ if (!/^[a-zA-Z0-9.\-*~\/]+$/.test(entry)) {
+ return "Invalid wildcard pattern. Use only letters, numbers, dots, hyphens, slashes, and wildcards (* or ~)";
+ }
+
+ return "Invalid wildcard format. Common formats are *.domain.com or domain.*";
+ }
+
+ const ipv4Result = getCippValidator(entry, "ip");
+ const ipv4CidrResult = getCippValidator(entry, "ipv4cidr");
+ const ipv6Result = getCippValidator(entry, "ipv6");
+ const ipv6CidrResult = getCippValidator(entry, "ipv6cidr");
+ const hostnameResult = getCippValidator(entry, "hostname");
+ const urlResult = getCippValidator(entry, "url");
+
+ if (
+ ipv4Result !== true &&
+ ipv4CidrResult !== true &&
+ ipv6Result !== true &&
+ ipv6CidrResult !== true &&
+ hostnameResult !== true &&
+ urlResult !== true
+ ) {
+ return "Invalid URL format. Enter hostnames, IPv4, or IPv6 addresses";
+ }
+ }
+ return true;
+ }
+
+ if (currentListType === "Sender") {
+ for (const entry of entries) {
+ if (entry.includes("*") || entry.includes("~")) {
+ const wildcardDomainResult = getCippValidator(entry, "wildcardDomain");
+
+ if (wildcardDomainResult !== true) {
+ return "Invalid sender wildcard pattern. Common format is *.domain.com";
+ }
+ continue;
+ }
+
+ const senderResult = getCippValidator(entry, "senderEntry");
+ if (senderResult !== true) {
+ return senderResult;
+ }
+ }
+ return true;
+ }
+
+ return true;
+ };
+
+ const handleSubmit = formControl.handleSubmit((values) => {
+ const payload = {
+ tenantID: values.tenantID,
+ entries: values.entries,
+ listType: values.listType?.value,
+ notes: values.notes,
+ listMethod: values.listMethod?.value,
+ NoExpiration: values.NoExpiration,
+ RemoveAfter: values.RemoveAfter,
+ };
+
+ addEntry.mutate({
+ url: "/api/AddTenantAllowBlockList",
+ data: payload,
+ });
+ });
+
+ const handleCloseDrawer = () => {
+ setDrawerVisible(false);
+ formControl.reset(defaultValues);
+ };
+
+ return (
+ <>
+ setDrawerVisible(true)}
+ startIcon={}
+ >
+ {buttonText}
+
+
+
+
+
+
+ }
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
+
+export default CippAddTenantAllowBlockListDrawer;
diff --git a/src/components/CippComponents/CippAddTenantGroupDrawer.jsx b/src/components/CippComponents/CippAddTenantGroupDrawer.jsx
new file mode 100644
index 000000000000..75804c15f89c
--- /dev/null
+++ b/src/components/CippComponents/CippAddTenantGroupDrawer.jsx
@@ -0,0 +1,121 @@
+import React, { useState, useEffect } from "react";
+import { Button, Box } from "@mui/material";
+import { useForm, useFormState } from "react-hook-form";
+import { GroupAdd } from "@mui/icons-material";
+import { CippOffCanvas } from "./CippOffCanvas";
+import { CippApiResults } from "./CippApiResults";
+import { ApiPostCall } from "../../api/ApiCall";
+import CippAddEditTenantGroups from "./CippAddEditTenantGroups";
+import { getCippValidator } from "../../utils/get-cipp-validator";
+
+export const CippAddTenantGroupDrawer = ({
+ buttonText = "Add Tenant Group",
+ requiredPermissions = [],
+ PermissionButton = Button,
+}) => {
+ const [drawerVisible, setDrawerVisible] = useState(false);
+
+ const formControl = useForm({
+ mode: "onChange",
+ defaultValues: {
+ groupType: "static",
+ ruleLogic: "and",
+ dynamicRules: [{}]
+ },
+ });
+
+ const createTenantGroup = ApiPostCall({
+ urlFromData: true,
+ relatedQueryKeys: ["TenantGroupListPage"],
+ });
+
+ const { isValid, isDirty } = useFormState({ control: formControl.control });
+
+ useEffect(() => {
+ if (createTenantGroup.isSuccess) {
+ formControl.reset({
+ groupType: "static",
+ ruleLogic: "and",
+ dynamicRules: [{}]
+ });
+ }
+ }, [createTenantGroup.isSuccess]);
+
+ const handleSubmit = (data) => {
+ const formattedData = {
+ ...data,
+ Action: "AddEdit",
+ };
+
+ // If it's a dynamic group, format the rules for the backend
+ if (data.groupType === "dynamic" && data.dynamicRules) {
+ formattedData.dynamicRules = data.dynamicRules.map(rule => ({
+ property: rule.property?.value || rule.property,
+ operator: rule.operator?.value || rule.operator,
+ value: rule.value,
+ }));
+ formattedData.ruleLogic = data.ruleLogic || "and";
+ }
+
+ createTenantGroup.mutate({
+ url: "/api/ExecTenantGroup",
+ data: formattedData,
+ });
+ };
+
+ const handleCloseDrawer = () => {
+ setDrawerVisible(false);
+ formControl.reset({
+ groupType: "static",
+ ruleLogic: "and",
+ dynamicRules: [{}]
+ });
+ };
+
+ return (
+ <>
+ setDrawerVisible(true)}
+ startIcon={}
+ >
+ {buttonText}
+
+
+
+
+
+ }
+ >
+
+
+
+
+
+ >
+ );
+};
\ No newline at end of file
diff --git a/src/components/CippComponents/CippAddUserDrawer.jsx b/src/components/CippComponents/CippAddUserDrawer.jsx
index a0802bde5f5c..f6a650ce1b11 100644
--- a/src/components/CippComponents/CippAddUserDrawer.jsx
+++ b/src/components/CippComponents/CippAddUserDrawer.jsx
@@ -8,6 +8,7 @@ import { CippApiResults } from "./CippApiResults";
import { useSettings } from "../../hooks/use-settings";
import { ApiPostCall } from "../../api/ApiCall";
import CippAddEditUser from "../CippFormPages/CippAddEditUser";
+import { Stack } from "@mui/system";
export const CippAddUserDrawer = ({
buttonText = "Add User",
@@ -105,23 +106,26 @@ export const CippAddUserDrawer = ({
onClose={handleCloseDrawer}
size="xl"
footer={
-
-
-
-
+
+
+
+
+
+
+
}
>
@@ -162,7 +166,6 @@ export const CippAddUserDrawer = ({
formType="add"
/>
-
>
);
diff --git a/src/components/CippComponents/CippApiLogsDrawer.jsx b/src/components/CippComponents/CippApiLogsDrawer.jsx
new file mode 100644
index 000000000000..3349e891a9f5
--- /dev/null
+++ b/src/components/CippComponents/CippApiLogsDrawer.jsx
@@ -0,0 +1,95 @@
+import { useState } from "react";
+import { Button, Box } from "@mui/material";
+import { History } from "@mui/icons-material";
+import { EyeIcon } from "@heroicons/react/24/outline";
+import { CippOffCanvas } from "./CippOffCanvas";
+import { CippDataTable } from "../CippTable/CippDataTable";
+
+export const CippApiLogsDrawer = ({
+ buttonText = "View API Logs",
+ apiFilter = null,
+ tenantFilter = null,
+ requiredPermissions = [],
+ PermissionButton = Button,
+ title = "API Logs",
+ ...props
+}) => {
+ const [drawerVisible, setDrawerVisible] = useState(false);
+
+ const handleCloseDrawer = () => {
+ setDrawerVisible(false);
+ };
+
+ const handleOpenDrawer = () => {
+ setDrawerVisible(true);
+ };
+
+ // Build the API URL with the filter
+ const apiUrl = `/api/ListLogs?Filter=true${apiFilter ? `&API=${apiFilter}` : ""}${
+ tenantFilter ? `&Tenant=${tenantFilter}` : ""
+ }`;
+
+ // Define the columns for the logs table
+ const simpleColumns = ["DateTime", "Severity", "Message", "User", "Tenant", "API"];
+
+ const actions = [
+ {
+ label: "View Log Entry",
+ link: "/cipp/logs/logentry?logentry=[RowKey]",
+ icon: ,
+ color: "primary",
+ },
+ ];
+
+ return (
+ <>
+ }
+ {...props}
+ >
+ {buttonText}
+
+
+
+
+
+
+ >
+ );
+};
diff --git a/src/components/CippComponents/CippGraphAttributeSelector.jsx b/src/components/CippComponents/CippGraphAttributeSelector.jsx
new file mode 100644
index 000000000000..3c3d9231342f
--- /dev/null
+++ b/src/components/CippComponents/CippGraphAttributeSelector.jsx
@@ -0,0 +1,77 @@
+import { useWatch } from "react-hook-form";
+import CippFormComponent from "./CippFormComponent";
+
+/**
+ * A form component for selecting attributes from a Graph API endpoint
+ * @param {Object} props - Component props
+ * @param {Object} props.formControl - React Hook Form control object
+ * @param {string} props.name - Field name for the form
+ * @param {string} props.resourceFieldName - Name of the field that contains the selected resource type
+ * @param {string} props.label - Label for the field
+ * @param {string} props.helperText - Helper text for the field
+ * @param {boolean} props.multiple - Whether to allow multiple selections
+ * @param {boolean} props.required - Whether the field is required
+ * @param {Object} props.gridProps - Grid props to pass to the wrapper
+ */
+const CippGraphAttributeSelector = ({
+ formControl,
+ name,
+ resourceFieldName = "DeltaResource",
+ label = "Attributes to Monitor",
+ helperText,
+ multiple = true,
+ required = false,
+ ...otherProps
+}) => {
+ // Watch for changes in the resource type field
+ const selectedResource = useWatch({
+ control: formControl.control,
+ name: resourceFieldName,
+ });
+
+ // Extract the value whether selectedResource is an object or string
+ const resourceValue = selectedResource?.value || selectedResource;
+
+ const getHelperText = () => {
+ if (helperText) return helperText;
+
+ if (!resourceValue) {
+ return "Select a resource type above to view available attributes";
+ }
+
+ return "Select which attributes to monitor for changes";
+ };
+
+ const api = resourceValue
+ ? {
+ url: "/api/ListGraphRequest",
+ queryKey: `graph-properties-${resourceValue}`,
+ data: {
+ Endpoint: resourceValue,
+ ListProperties: true,
+ IgnoreErrors: true,
+ },
+ labelField: (item) => item,
+ valueField: (item) => item,
+ dataKey: "Results",
+ }
+ : null;
+
+ return (
+
+ );
+};
+
+export default CippGraphAttributeSelector;
diff --git a/src/components/CippComponents/CippGraphResourceSelector.jsx b/src/components/CippComponents/CippGraphResourceSelector.jsx
new file mode 100644
index 000000000000..f2c2015c61f8
--- /dev/null
+++ b/src/components/CippComponents/CippGraphResourceSelector.jsx
@@ -0,0 +1,132 @@
+import { useWatch } from "react-hook-form";
+import CippFormComponent from "./CippFormComponent";
+
+/**
+ * A form component for selecting specific resources from a Graph API endpoint
+ * @param {Object} props - Component props
+ * @param {Object} props.formControl - React Hook Form control object
+ * @param {string} props.name - Field name for the form
+ * @param {string} props.resourceFieldName - Name of the field that contains the selected resource type
+ * @param {string} props.label - Label for the field
+ * @param {string} props.helperText - Helper text for the field
+ * @param {boolean} props.multiple - Whether to allow multiple selections
+ * @param {boolean} props.required - Whether the field is required
+ * @param {Object} props.gridProps - Grid props to pass to the wrapper
+ */
+const CippGraphResourceSelector = ({
+ formControl,
+ name,
+ resourceFieldName = "DeltaResource",
+ tenantFilterFieldName = "tenantFilter",
+ label = "Filter Specific Resources (Optional)",
+ helperText,
+ multiple = true,
+ required = false,
+ ...otherProps
+}) => {
+ // Watch for changes in the resource type field
+ const selectedResource = useWatch({
+ control: formControl.control,
+ name: resourceFieldName,
+ });
+
+ // Watch for changes in the tenant filter field
+ const tenantFilter = useWatch({
+ control: formControl.control,
+ name: tenantFilterFieldName,
+ });
+
+ // Extract the value whether selectedResource is an object or string
+ const resourceValue = selectedResource?.value || selectedResource;
+
+ // Extract the tenant filter value - handle both object and string formats
+ const tenantFilterValue = tenantFilter?.value || tenantFilter;
+
+ const getHelperText = () => {
+ if (helperText) return helperText;
+
+ if (!resourceValue) {
+ return "Select a resource type above to filter specific resources";
+ }
+
+ if (
+ !tenantFilterValue ||
+ tenantFilterValue === "AllTenants" ||
+ (tenantFilter && typeof tenantFilter === "object" && tenantFilter.type === "Group")
+ ) {
+ return "Resource filtering is not available for All Tenants or tenant groups";
+ }
+
+ if (multiple) {
+ return "Optionally select specific resources to monitor (will create filter with OR statements: id eq 'id1' or id eq 'id2')";
+ }
+
+ return "Optionally select a specific resource to monitor";
+ };
+
+ // Check if we should make the API call
+ const shouldFetchResources = () => {
+ // Must have a resource type selected
+ if (!resourceValue) return false;
+
+ // Must have a tenant filter
+ if (!tenantFilterValue) return false;
+
+ // Cannot be null or undefined
+ if (tenantFilterValue === null || tenantFilterValue === undefined) return false;
+
+ // Cannot be AllTenants
+ if (tenantFilterValue === "AllTenants") return false;
+
+ // Cannot be a tenant group (check if tenantFilter object has type: "Group")
+ if (tenantFilter && typeof tenantFilter === "object" && tenantFilter.type === "Group")
+ return false;
+
+ return true;
+ };
+
+ const isDisabled = !resourceValue || !shouldFetchResources();
+
+ const api = shouldFetchResources()
+ ? {
+ url: "/api/ListGraphRequest",
+ queryKey: `graph-resources-${resourceValue}-${tenantFilterValue}`,
+ data: {
+ Endpoint: resourceValue,
+ IgnoreErrors: true,
+ $select: "id,displayName",
+ $top: 100,
+ tenantFilter: tenantFilterValue,
+ },
+ labelField: (item) => item.displayName || item.id,
+ valueField: "id",
+ dataKey: "Results",
+ waiting: true,
+ }
+ : null;
+
+ return (
+
+ );
+};
+
+export default CippGraphResourceSelector;
diff --git a/src/components/CippComponents/CippPolicyDeployDrawer.jsx b/src/components/CippComponents/CippPolicyDeployDrawer.jsx
index 3702111ed47f..619f516496cd 100644
--- a/src/components/CippComponents/CippPolicyDeployDrawer.jsx
+++ b/src/components/CippComponents/CippPolicyDeployDrawer.jsx
@@ -30,7 +30,7 @@ export const CippPolicyDeployDrawer = ({
const jsonWatch = useWatch({ control: formControl.control, name: "RAWJson" });
useEffect(() => {
if (CATemplates.isSuccess && watcher?.value) {
- const template = CATemplates.data.find((template) => template.GUID === watcher.value);
+ const template = CATemplates.data?.find((template) => template.GUID === watcher.value);
if (template) {
const jsonTemplate = template.RAWJson ? JSON.parse(template.RAWJson) : null;
setJSONData(jsonTemplate);
@@ -129,7 +129,7 @@ export const CippPolicyDeployDrawer = ({
multiple={false}
formControl={formControl}
options={
- CATemplates.isSuccess
+ CATemplates.isSuccess && Array.isArray(CATemplates.data)
? CATemplates.data.map((template) => ({
label: template.Displayname,
value: template.GUID,
diff --git a/src/components/CippComponents/CippScheduledTaskActions.jsx b/src/components/CippComponents/CippScheduledTaskActions.jsx
index 2a1ec00236ff..2df628a81cdb 100644
--- a/src/components/CippComponents/CippScheduledTaskActions.jsx
+++ b/src/components/CippComponents/CippScheduledTaskActions.jsx
@@ -2,7 +2,7 @@ import { EyeIcon, TrashIcon } from "@heroicons/react/24/outline";
import { CopyAll, Edit, PlayArrow } from "@mui/icons-material";
import { usePermissions } from "../../hooks/use-permissions";
-export const CippScheduledTaskActions = () => {
+export const CippScheduledTaskActions = (drawerHandlers = {}) => {
const { checkPermissions } = usePermissions();
const canWriteScheduler = checkPermissions(["CIPP.Scheduler.ReadWrite"]);
const canReadScheduler = checkPermissions(["CIPP.Scheduler.Read", "CIPP.Scheduler.ReadWrite"]);
@@ -26,20 +26,32 @@ export const CippScheduledTaskActions = () => {
},
{
label: "Edit Job",
- link: "/cipp/scheduler/job?id=[RowKey]",
+ customFunction:
+ drawerHandlers.openEditDrawer ||
+ ((row) => {
+ // Fallback to page navigation if no drawer handler provided
+ window.location.href = `/cipp/scheduler/job?id=${row.RowKey}`;
+ }),
multiPost: false,
icon: ,
color: "success",
showInActionsMenu: true,
+ noConfirm: true,
condition: () => canWriteScheduler,
},
{
- label: "Clone and Edit Job",
- link: "/cipp/scheduler/job?id=[RowKey]&Clone=True",
+ label: "Clone Job",
+ customFunction:
+ drawerHandlers.openCloneDrawer ||
+ ((row) => {
+ // Fallback to page navigation if no drawer handler provided
+ window.location.href = `/cipp/scheduler/job?id=${row.RowKey}&Clone=True`;
+ }),
multiPost: false,
icon: ,
color: "success",
showInActionsMenu: true,
+ noConfirm: true,
condition: () => canWriteScheduler,
},
{
diff --git a/src/components/CippComponents/CippSchedulerDrawer.jsx b/src/components/CippComponents/CippSchedulerDrawer.jsx
new file mode 100644
index 000000000000..98510e77ce53
--- /dev/null
+++ b/src/components/CippComponents/CippSchedulerDrawer.jsx
@@ -0,0 +1,97 @@
+import { useState, useEffect } from "react";
+import { Button, Box, Typography, Alert, AlertTitle } from "@mui/material";
+import { useForm, useFormState } from "react-hook-form";
+import { CalendarDaysIcon } from "@heroicons/react/24/outline";
+import { CippOffCanvas } from "./CippOffCanvas";
+import CippSchedulerForm from "../CippFormPages/CippSchedulerForm";
+import { useSettings } from "../../hooks/use-settings";
+
+export const CippSchedulerDrawer = ({
+ buttonText = "Add Task",
+ requiredPermissions = [],
+ PermissionButton = Button,
+ onSuccess,
+ onClose,
+ taskId = null,
+ cloneMode = false,
+}) => {
+ const [drawerVisible, setDrawerVisible] = useState(false);
+ const [formKey, setFormKey] = useState(0);
+ const userSettingsDefaults = useSettings();
+
+ const formControl = useForm({
+ mode: "onBlur",
+ defaultValues: {
+ tenantFilter: userSettingsDefaults.currentTenant,
+ Recurrence: { value: "0", label: "Once" },
+ taskType: { value: "scheduled", label: "Scheduled Task" },
+ },
+ });
+
+ const handleCloseDrawer = () => {
+ setDrawerVisible(false);
+ // Increment form key to force complete remount when reopening
+ setFormKey((prev) => prev + 1);
+ // Call onClose callback if provided (to clear parent state)
+ if (onClose) {
+ onClose();
+ }
+ // Add a small delay before resetting to ensure drawer is closed
+ setTimeout(() => {
+ // Reset form to default values
+ formControl.reset({
+ tenantFilter: userSettingsDefaults.currentTenant,
+ Recurrence: { value: "0", label: "Once" },
+ taskType: { value: "scheduled", label: "Scheduled Task" },
+ });
+ }, 100);
+ };
+
+ const handleOpenDrawer = () => {
+ setDrawerVisible(true);
+ };
+
+ // Auto-open drawer if taskId is provided (for edit mode)
+ useEffect(() => {
+ if (taskId) {
+ setDrawerVisible(true);
+ }
+ }, [taskId]);
+
+ return (
+ <>
+ }
+ >
+ {buttonText}
+
+
+
+
+ Task Configuration
+ {taskId && cloneMode
+ ? "Clone this task with the same configuration. Modify the settings as needed and save to create a new task."
+ : taskId
+ ? "Edit the task configuration. Changes will be applied when you save."
+ : "Create a scheduled task or event-triggered task. Scheduled tasks run PowerShell commands at specified times, while triggered tasks respond to events like Azure AD changes."}
+
+
+
+
+
+ >
+ );
+};
diff --git a/src/components/CippComponents/CippTenantGroupOffCanvas.jsx b/src/components/CippComponents/CippTenantGroupOffCanvas.jsx
new file mode 100644
index 000000000000..05ed8e18f836
--- /dev/null
+++ b/src/components/CippComponents/CippTenantGroupOffCanvas.jsx
@@ -0,0 +1,275 @@
+import React from "react";
+import {
+ Box,
+ Typography,
+ Card,
+ CardContent,
+ Chip,
+ Alert,
+ AlertTitle,
+ useTheme,
+ Stack,
+} from "@mui/material";
+import { Groups, Business, Rule, Info } from "@mui/icons-material";
+import { CippDataTable } from "../CippTable/CippDataTable";
+
+export const CippTenantGroupOffCanvas = ({ data }) => {
+ const theme = useTheme();
+
+ if (!data) {
+ return (
+
+ No group data available
+
+ );
+ }
+
+ const isDynamic = data.GroupType === "dynamic";
+ const hasMembers = data.Members && data.Members.length > 0;
+ const hasDynamicRules =
+ data.DynamicRules &&
+ ((Array.isArray(data.DynamicRules) && data.DynamicRules.length > 0) ||
+ (!Array.isArray(data.DynamicRules) && Object.keys(data.DynamicRules).length > 0));
+
+ const renderDynamicRules = () => {
+ if (!hasDynamicRules) {
+ return (
+
+ No Dynamic Rules
+ This dynamic group has no rules configured.
+
+ );
+ }
+
+ const operatorDisplay = {
+ eq: "equals",
+ ne: "not equals",
+ in: "in",
+ notIn: "not in",
+ contains: "contains",
+ startsWith: "starts with",
+ endsWith: "ends with",
+ };
+
+ // Handle both single rule object and array of rules
+ const rules = Array.isArray(data.DynamicRules) ? data.DynamicRules : [data.DynamicRules];
+
+ const renderRule = (rule, index) => (
+
+
+ Rule {rules.length > 1 ? `${index + 1}:` : "Configuration:"}
+
+
+ Property: {rule.property}
+
+
+ Operator: {operatorDisplay[rule.operator] || rule.operator}
+
+
+ Value(s):
+
+ {Array.isArray(rule.value) ? (
+
+ {rule.value.map((item, valueIndex) => (
+
+ ))}
+
+ ) : (
+
+ )}
+
+ );
+
+ const renderRulesWithLogic = () => {
+ if (rules.length === 1) {
+ return renderRule(rules[0], 0);
+ }
+
+ return rules.map((rule, index) => (
+
+ {renderRule(rule, index)}
+ {index < rules.length - 1 && (
+
+
+
+ )}
+
+ ));
+ };
+
+ return (
+
+
+
+
+ Dynamic Rules
+ {rules.length > 1 && (
+
+ )}
+
+ {renderRulesWithLogic()}
+
+
+ );
+ };
+
+ const renderMembers = () => {
+ if (!hasMembers) {
+ return (
+
+ No Members
+ {isDynamic
+ ? "This dynamic group has no members that match the current rules."
+ : "This static group has no members assigned."}
+
+ );
+ }
+
+ const memberColumns = ["displayName", "defaultDomainName", "customerId"];
+
+ return (
+
+ ,
+ }}
+ />
+
+ );
+ };
+
+ return (
+
+ {/* Header Section */}
+
+
+
+
+
+ {data.Name}
+
+
+
+
+ ID: {data.Id}
+
+
+
+
+
+ {data.Description && (
+
+ Description
+ {data.Description}
+
+ )}
+
+
+ {/* Content Sections */}
+
+ {/* Dynamic Rules Section (only for dynamic groups) */}
+ {isDynamic && {renderDynamicRules()}}
+
+ {/* Members Section */}
+ {renderMembers()}
+
+ {/* Additional Info */}
+
+
+
+
+ Additional Information
+
+
+
+
+ Group Type
+
+ {isDynamic ? "Dynamic" : "Static"}
+
+
+
+ Member Count
+
+
+ {data.Members?.length || 0} tenant{(data.Members?.length || 0) !== 1 ? "s" : ""}
+
+
+ {isDynamic && (
+ <>
+
+
+ Rule Logic
+
+
+ {data.RuleLogic?.toUpperCase() || "AND"}
+
+
+
+
+ Has Rules
+
+ {hasDynamicRules ? "Yes" : "No"}
+
+ >
+ )}
+
+
+
+
+
+ );
+};
diff --git a/src/components/CippComponents/CippTenantGroupRuleBuilder.jsx b/src/components/CippComponents/CippTenantGroupRuleBuilder.jsx
new file mode 100644
index 000000000000..2964b81a0d83
--- /dev/null
+++ b/src/components/CippComponents/CippTenantGroupRuleBuilder.jsx
@@ -0,0 +1,186 @@
+ import React, { useState } from "react";
+import { Box, Button, IconButton, Typography, Alert, Paper } from "@mui/material";
+import { Grid } from "@mui/system";
+import { Add as AddIcon, Delete as DeleteIcon } from "@mui/icons-material";
+import CippFormComponent from "./CippFormComponent";
+import { CippFormCondition } from "./CippFormCondition";
+import { useWatch } from "react-hook-form";
+import {
+ getTenantGroupPropertyOptions,
+ getTenantGroupOperatorOptions,
+ getTenantGroupValueOptions,
+} from "../../utils/get-cipp-tenant-group-options";
+
+const CippTenantGroupRuleBuilder = ({ formControl, name = "dynamicRules" }) => {
+ const [ruleCount, setRuleCount] = useState(1);
+
+ // Watch the rules array to get current values
+ const watchedRules = useWatch({
+ control: formControl.control,
+ name: name,
+ defaultValue: [{}],
+ });
+
+ // Watch the logic operator
+ const ruleLogic = useWatch({
+ control: formControl.control,
+ name: "ruleLogic",
+ defaultValue: "and"
+ });
+
+ const propertyOptions = getTenantGroupPropertyOptions();
+
+ const addRule = () => {
+ const currentRules = formControl.getValues(name) || [];
+ const newRules = [...currentRules, {}];
+ formControl.setValue(name, newRules);
+ setRuleCount(ruleCount + 1);
+ };
+
+ const removeRule = (index) => {
+ const currentRules = formControl.getValues(name) || [];
+ const newRules = currentRules.filter((_, i) => i !== index);
+ formControl.setValue(name, newRules);
+ setRuleCount(Math.max(1, ruleCount - 1));
+ };
+
+ const getValueOptions = (ruleIndex) => {
+ const rules = watchedRules || [];
+ const rule = rules[ruleIndex];
+ const propertyType = rule?.property?.type;
+ return getTenantGroupValueOptions(propertyType);
+ };
+
+ const getOperatorOptions = (ruleIndex) => {
+ const rules = watchedRules || [];
+ const rule = rules[ruleIndex];
+ const propertyType = rule?.property?.type;
+ return getTenantGroupOperatorOptions(propertyType);
+ };
+
+ const renderRule = (ruleIndex) => {
+ const isFirstRule = ruleIndex === 0;
+ const canRemove = (watchedRules?.length || 0) > 1;
+
+ return (
+
+ {!isFirstRule && (
+
+ {(ruleLogic || 'and').toUpperCase()}
+
+ )}
+
+
+ {/* Property Selection */}
+
+
+
+
+ {/* Operator Selection */}
+
+
+
+
+
+
+ {/* Value Selection - Conditional based on property type */}
+
+
+
+
+
+
+ {/* Remove Rule Button */}
+
+ {canRemove && (
+ removeRule(ruleIndex)} size="small">
+
+
+ )}
+
+
+
+ );
+ };
+
+ return (
+
+
+ Dynamic Rules
+
+
+
+ Define rules to automatically include tenants in this group. Rules are combined with the selected logic operator.
+ Example: "Available License equals Microsoft 365 E3" {(ruleLogic || 'and').toUpperCase()} "Delegated Access Status equals Direct Tenant"
+
+
+ {/* Logic Operator Selection */}
+
+
+
+
+ {/* Render existing rules */}
+ {(watchedRules || [{}]).map((_, index) => renderRule(index))}
+
+ {/* Add Rule Button */}
+
+ } onClick={addRule}>
+ Add Rule
+
+
+
+ );
+};
+
+export default CippTenantGroupRuleBuilder;
diff --git a/src/components/CippComponents/ScheduledTaskDetails.jsx b/src/components/CippComponents/ScheduledTaskDetails.jsx
index 8e3f50161694..554c72261c4b 100644
--- a/src/components/CippComponents/ScheduledTaskDetails.jsx
+++ b/src/components/CippComponents/ScheduledTaskDetails.jsx
@@ -118,6 +118,31 @@ const ScheduledTaskDetails = ({ data, showActions = true }) => {
isFetching={taskDetailResults.isFetching}
/>
+ {taskDetails?.Task?.Trigger && (
+
+ }>
+ Trigger Configuration
+
+
+ {
+ return {
+ label: key,
+ value: getCippFormatting(value, key),
+ };
+ })}
+ isFetching={taskDetailResults.isFetching}
+ />
+
+
+ )}
+
{taskDetailResults.isFetching ? (
) : (
diff --git a/src/components/CippFormPages/CippExchangeSettingsForm.jsx b/src/components/CippFormPages/CippExchangeSettingsForm.jsx
index b7c1cdc983a2..db7c15da5af8 100644
--- a/src/components/CippFormPages/CippExchangeSettingsForm.jsx
+++ b/src/components/CippFormPages/CippExchangeSettingsForm.jsx
@@ -10,8 +10,9 @@ import {
Typography,
Tooltip,
CircularProgress,
+ IconButton,
} from "@mui/material";
-import { Check, Error } from "@mui/icons-material";
+import { Check, Error, Sync } from "@mui/icons-material";
import CippFormComponent from "/src/components/CippComponents/CippFormComponent";
import { ApiGetCall, ApiPostCall } from "../../api/ApiCall";
import { useSettings } from "../../hooks/use-settings";
@@ -74,13 +75,19 @@ const CippExchangeSettingsForm = (props) => {
// If this was an OOO submission, preserve the submitted values
if (relatedQueryKeys.includes(`ooo-${userId}`)) {
const submittedValues = formControl.getValues();
- const oooFields = ['AutoReplyState', 'InternalMessage', 'ExternalMessage', 'StartTime', 'EndTime'];
-
+ const oooFields = [
+ "AutoReplyState",
+ "InternalMessage",
+ "ExternalMessage",
+ "StartTime",
+ "EndTime",
+ ];
+
// Reset the form
formControl.reset();
-
+
// Restore the submitted OOO values
- oooFields.forEach(field => {
+ oooFields.forEach((field) => {
const value = submittedValues.ooo?.[field];
if (value !== undefined) {
formControl.setValue(`ooo.${field}`, value);
@@ -144,14 +151,14 @@ const CippExchangeSettingsForm = (props) => {
cardLabelBox: {
cardLabelBoxHeader: isFetching ? (
- ) : (currentSettings?.ForwardingAddress) ? (
-
+ ) : currentSettings?.ForwardingAddress ? (
+
) : (
-
+
),
},
text: "Mailbox Forwarding",
- subtext: (currentSettings?.ForwardingAddress)
+ subtext: currentSettings?.ForwardingAddress
? "Email forwarding is configured for this mailbox"
: "No email forwarding configured for this mailbox",
formContent: (
@@ -171,6 +178,16 @@ const CippExchangeSettingsForm = (props) => {
},
text: "Out Of Office",
subtext: "Set automatic replies for when you are away",
+ action: oooRequest
+ ? {
+ tooltip: oooRequest.isFetching
+ ? "Refreshing Out Of Office data"
+ : "Refresh Out Of Office data",
+ onClick: () => oooRequest.refetch(),
+ disabled: oooRequest.isFetching,
+ isLoading: oooRequest.isFetching,
+ }
+ : null,
formContent: (
@@ -190,8 +207,12 @@ const CippExchangeSettingsForm = (props) => {
/>
-
@@ -206,8 +227,12 @@ const CippExchangeSettingsForm = (props) => {
-
@@ -277,7 +302,7 @@ const CippExchangeSettingsForm = (props) => {
validators={{
required: "Please enter a number",
min: { value: 1, message: "The minimum is 1" },
- max: { value: 1000, message: "The maximum is 1000" },
+ max: { value: 1000, message: "The maximum is 1000" },
}}
/>
@@ -348,15 +373,49 @@ const CippExchangeSettingsForm = (props) => {
-
-
-
+
+ {section.action && (
+
+
+ {
+ event.stopPropagation();
+ section.action.onClick?.();
+ }}
+ disabled={section.action.disabled}
+ sx={{
+ color: "text.secondary",
+ }}
+ >
+
+
+
+
+
+
+ )}
+
+
+
+
diff --git a/src/components/CippFormPages/CippSchedulerForm.jsx b/src/components/CippFormPages/CippSchedulerForm.jsx
index 049a554b8626..50be6839a2e9 100644
--- a/src/components/CippFormPages/CippSchedulerForm.jsx
+++ b/src/components/CippFormPages/CippSchedulerForm.jsx
@@ -1,20 +1,39 @@
-import { Box, Button, Divider, Skeleton, SvgIcon, Typography } from "@mui/material";
+import {
+ Box,
+ Button,
+ Divider,
+ Skeleton,
+ SvgIcon,
+ Typography,
+ ButtonGroup,
+ Accordion,
+ AccordionSummary,
+ AccordionDetails,
+ IconButton,
+ Alert,
+} from "@mui/material";
import { Grid } from "@mui/system";
import { useWatch } from "react-hook-form";
import CippFormComponent from "/src/components/CippComponents/CippFormComponent";
import { CippFormTenantSelector } from "/src/components/CippComponents/CippFormTenantSelector";
import { CippFormCondition } from "/src/components/CippComponents/CippFormCondition";
+import CippGraphResourceSelector from "/src/components/CippComponents/CippGraphResourceSelector";
+import CippGraphAttributeSelector from "/src/components/CippComponents/CippGraphAttributeSelector";
import { getCippValidator } from "/src/utils/get-cipp-validator";
import { useRouter } from "next/router";
+import Link from "next/link";
import { ApiGetCall, ApiPostCall } from "/src/api/ApiCall";
-import { useEffect } from "react";
+import { useEffect, useState } from "react";
import CippFormInputArray from "../CippComponents/CippFormInputArray";
import { CippApiResults } from "../CippComponents/CippApiResults";
import { CalendarDaysIcon } from "@heroicons/react/24/outline";
+import { ExpandMoreOutlined, Delete, Add } from "@mui/icons-material";
const CippSchedulerForm = (props) => {
- const { formControl, fullWidth = false } = props; // Added fullWidth prop
+ const { formControl, fullWidth = false, taskId = null, cloneMode = false } = props;
const selectedCommand = useWatch({ control: formControl.control, name: "command" });
+ const [addedConditions, setAddedConditions] = useState([{ id: 0 }]);
+ const [isResourcePickerDisabled, setIsResourcePickerDisabled] = useState(false);
const fieldRequired = (field) => {
if (field?.Required) {
@@ -26,6 +45,17 @@ const CippSchedulerForm = (props) => {
}
};
+ const handleAddCondition = () => {
+ setAddedConditions([...addedConditions, { id: addedConditions.length }]);
+ };
+
+ const handleRemoveCondition = (id) => {
+ const currentConditions = formControl.getValues("Trigger.DeltaConditions") || [];
+ const updatedConditions = currentConditions.filter((_, index) => index !== id);
+ formControl.setValue("Trigger.DeltaConditions", updatedConditions);
+ setAddedConditions(addedConditions.filter((condition, index) => index !== id));
+ };
+
const postCall = ApiPostCall({
datafromUrl: true,
relatedQueryKeys: [
@@ -57,18 +87,97 @@ const CippSchedulerForm = (props) => {
{ value: "30d", label: "Every 30 days" },
{ value: "365d", label: "Every 365 days" },
];
+
+ const triggerRecurrenceOptions = [
+ { value: "15m", label: "Every 15 minutes" },
+ { value: "30m", label: "Every 30 minutes" },
+ { value: "1h", label: "Every 1 hour" },
+ { value: "4h", label: "Every 4 hours" },
+ { value: "12h", label: "Every 12 hours" },
+ { value: "1d", label: "Every 1 day" },
+ ];
+
+ const taskTypeOptions = [
+ { value: "scheduled", label: "Scheduled Task" },
+ { value: "triggered", label: "Triggered Task" },
+ ];
+
+ const triggerTypeOptions = [{ value: "DeltaQuery", label: "Delta Query" }];
+
+ const deltaResourceOptions = [
+ { value: "users", label: "Users" },
+ { value: "groups", label: "Groups" },
+ { value: "contacts", label: "Contacts" },
+ { value: "orgContact", label: "Organizational Contacts" },
+ { value: "devices", label: "Devices" },
+ { value: "applications", label: "Applications" },
+ { value: "servicePrincipals", label: "Service Principals" },
+ { value: "directoryObjects", label: "Directory Objects" },
+ { value: "directoryRole", label: "Directory Roles" },
+ { value: "administrativeUnits", label: "Administrative Units" },
+ { value: "oAuth2PermissionGrant", label: "OAuth2 Permission Grants" },
+ ];
+
+ const simpleEventOptions = [
+ { value: "created", label: "Resource Created" },
+ { value: "updated", label: "Resource Updated" },
+ { value: "deleted", label: "Resource Deleted" },
+ ];
+
+ const operatorOptions = [
+ { value: "eq", label: "Equals to" },
+ { value: "ne", label: "Not Equals to" },
+ { value: "like", label: "Like" },
+ { value: "notlike", label: "Not like" },
+ { value: "notmatch", label: "Does not match" },
+ { value: "gt", label: "Greater than" },
+ { value: "lt", label: "Less than" },
+ { value: "in", label: "In" },
+ { value: "notIn", label: "Not In" },
+ ];
+
+ // Watch for trigger-related fields
+ const selectedTaskType = useWatch({ control: formControl.control, name: "taskType" });
+ const selectedTriggerType = useWatch({ control: formControl.control, name: "Trigger.Type" });
+ const selectedDeltaResource = useWatch({
+ control: formControl.control,
+ name: "Trigger.DeltaResource",
+ });
+ const selectedTenant = useWatch({ control: formControl.control, name: "tenantFilter" });
+
+ // Watch for summary display
+ const selectedSimpleEvent = useWatch({ control: formControl.control, name: "Trigger.EventType" });
+ const selectedRecurrence = useWatch({ control: formControl.control, name: "Recurrence" });
+ const selectedScheduledTime = useWatch({ control: formControl.control, name: "ScheduledTime" });
+ const selectedExecutePerResource = useWatch({
+ control: formControl.control,
+ name: "Trigger.ExecutePerResource",
+ });
+ const selectedDeltaExecutionMode = useWatch({
+ control: formControl.control,
+ name: "Trigger.ExecutionMode",
+ });
+ const selectedUseConditions = useWatch({
+ control: formControl.control,
+ name: "Trigger.UseConditions",
+ });
+ const selectedDeltaConditions = useWatch({
+ control: formControl.control,
+ name: "Trigger.DeltaConditions",
+ });
const commands = ApiGetCall({
url: "/api/ListFunctionParameters?Module=CIPPCore",
queryKey: "ListCommands",
});
const router = useRouter();
+
const scheduledTaskList = ApiGetCall({
url: "/api/ListScheduledItems",
- queryKey: "ListScheduledItems-Edit-" + router.query.id,
- waiting: !!router.query.id,
+ queryKey: "ListScheduledItems-Edit-" + (taskId || router.query.id),
+ waiting: !!(taskId || router.query.id),
data: {
- Id: router.query.id,
+ Id: taskId || router.query.id,
},
});
@@ -76,19 +185,113 @@ const CippSchedulerForm = (props) => {
url: "/api/ListTenants?AllTenantSelector=true",
queryKey: "ListTenants-AllTenants",
});
+
+ // Check if resource picker should be disabled
useEffect(() => {
- if (scheduledTaskList.isSuccess && router.query.id) {
- const task = scheduledTaskList.data.find((task) => task.RowKey === router.query.id);
+ console.log(selectedTenant);
+ if (!selectedTenant) {
+ setIsResourcePickerDisabled(false);
+ return;
+ }
+
+ // Disable if AllTenants is selected
+ if (selectedTenant.value === "AllTenants") {
+ setIsResourcePickerDisabled(true);
+ return;
+ }
+
+ // Disable if a tenant group is selected (groups have type: "Group")
+ if (selectedTenant.type === "Group") {
+ setIsResourcePickerDisabled(true);
+ return;
+ }
+
+ setIsResourcePickerDisabled(false);
+ }, [selectedTenant]);
+
+ // Helper functions for accordion summaries
+ const getTriggerSummary = () => {
+ if (!selectedTriggerType || selectedTaskType?.value !== "triggered") return "";
+
+ let summary = selectedTriggerType.label;
+
+ if (selectedTriggerType.value === "DeltaQuery") {
+ if (selectedDeltaResource?.label) {
+ summary += ` - ${selectedDeltaResource.label}`;
+ }
+ if (selectedSimpleEvent?.label) {
+ summary += ` (${selectedSimpleEvent.label})`;
+ }
+ if (selectedUseConditions && selectedDeltaConditions?.length > 0) {
+ summary += ` with ${selectedDeltaConditions.length} condition${
+ selectedDeltaConditions.length > 1 ? "s" : ""
+ }`;
+ }
+ }
+
+ return summary;
+ };
+
+ const getScheduleSummary = () => {
+ if (selectedTaskType?.value !== "scheduled") return "";
+
+ let summary = "";
+ if (selectedScheduledTime) {
+ // Handle both Unix timestamp and regular date formats
+ let date;
+ if (
+ typeof selectedScheduledTime === "number" ||
+ (typeof selectedScheduledTime === "string" && /^\d+$/.test(selectedScheduledTime))
+ ) {
+ // Unix timestamp (seconds or milliseconds)
+ const timestamp = parseInt(selectedScheduledTime);
+ date = new Date(timestamp > 1000000000000 ? timestamp : timestamp * 1000);
+ } else {
+ date = new Date(selectedScheduledTime);
+ }
+ // Include both date and time
+ summary += `${date.toLocaleDateString()} ${date.toLocaleTimeString()}`;
+ }
+ if (selectedRecurrence) {
+ summary += summary ? ` - ${selectedRecurrence.label}` : selectedRecurrence.label;
+ }
+
+ return summary;
+ };
+
+ const getCommandSummary = () => {
+ if (!selectedCommand) return "";
+
+ let summary = selectedCommand.label;
+
+ if (selectedTaskType?.value === "triggered" && selectedTriggerType?.value === "DeltaQuery") {
+ if (selectedExecutePerResource) {
+ summary += " (per resource)";
+ }
+ if (selectedDeltaExecutionMode) {
+ summary += ` - ${selectedDeltaExecutionMode.label}`;
+ }
+ }
+
+ return summary;
+ };
+ useEffect(() => {
+ if (scheduledTaskList.isSuccess && (taskId || router.query.id)) {
+ const task = scheduledTaskList.data.find(
+ (task) => task.RowKey === (taskId || router.query.id)
+ );
// Early return if task is not found
if (!task) {
- console.warn(`Task with RowKey ${router.query.id} not found`);
+ console.warn(`Task with RowKey ${taskId || router.query.id} not found`);
return;
}
- const postExecution = task?.postExecution?.split(",").map((item) => {
- return { label: item, value: item };
- });
+ const postExecution = task?.PostExecution
+ ? task.PostExecution.split(",").map((item) => {
+ return { label: item.trim(), value: item.trim() };
+ })
+ : [];
// Find tenantFilter in tenantList, and create a label/value pair for the autocomplete
if (tenantList.isSuccess) {
@@ -180,13 +383,29 @@ const CippSchedulerForm = (props) => {
const ResetParams = {
tenantFilter: tenantFilterForForm,
- RowKey: router.query.Clone ? null : task.RowKey,
- Name: router.query.Clone ? `${task.Name} (Clone)` : task?.Name,
+ RowKey: router.query.Clone || cloneMode ? null : task.RowKey,
+ Name: router.query.Clone || cloneMode ? `${task.Name} (Clone)` : task?.Name,
command: { label: task.Command, value: task.Command, addedFields: commandForForm },
ScheduledTime: task.ScheduledTime,
Recurrence: recurrence,
parameters: task.Parameters,
postExecution: postExecution,
+ // Set task type based on whether trigger exists
+ taskType: task.Trigger
+ ? { value: "triggered", label: "Triggered Task" }
+ : { value: "scheduled", label: "Scheduled Task" },
+ // Trigger configuration - use the trigger data directly since it's already in the correct format
+ ...(task.Trigger && {
+ "Trigger.Type": task.Trigger.Type,
+ "Trigger.DeltaResource": task.Trigger.DeltaResource,
+ "Trigger.EventType": task.Trigger.EventType,
+ "Trigger.ResourceFilter": task.Trigger.ResourceFilter || [],
+ "Trigger.WatchedAttributes": task.Trigger.WatchedAttributes || [],
+ "Trigger.UseConditions": task.Trigger.UseConditions || false,
+ "Trigger.DeltaConditions": task.Trigger.DeltaConditions || [],
+ "Trigger.ExecutePerResource": task.Trigger.ExecutePerResource || false,
+ "Trigger.ExecutionMode": task.Trigger.ExecutionMode,
+ }),
// Show advanced parameters if:
// 1. RawJsonParameters exist
// 2. It's a system command with no defined parameters
@@ -200,14 +419,32 @@ const CippSchedulerForm = (props) => {
RawJsonParameters: task.RawJsonParameters || "",
};
formControl.reset(ResetParams);
+
+ // Set up condition builder if task has delta conditions
+ if (
+ task.Trigger?.DeltaConditions &&
+ Array.isArray(task.Trigger.DeltaConditions) &&
+ task.Trigger.DeltaConditions.length > 0
+ ) {
+ const conditionsWithIds = task.Trigger.DeltaConditions.map((condition, index) => ({
+ id: index,
+ ...condition,
+ }));
+ setAddedConditions(conditionsWithIds);
+ } else {
+ // Reset to default single condition if no conditions exist
+ setAddedConditions([{ id: 0 }]);
+ }
}
}
}
}, [
+ taskId,
router.query.id,
scheduledTaskList.isSuccess,
tenantList.isSuccess,
router.query.Clone,
+ cloneMode,
commands.isSuccess,
]);
@@ -230,8 +467,10 @@ const CippSchedulerForm = (props) => {
// Get the original task parameters if we're editing (to preserve complex objects)
let parametersToUse = null;
- if (router.query.id && scheduledTaskList.isSuccess) {
- const task = scheduledTaskList.data.find((task) => task.RowKey === router.query.id);
+ if ((taskId || router.query.id) && scheduledTaskList.isSuccess) {
+ const task = scheduledTaskList.data.find(
+ (task) => task.RowKey === (taskId || router.query.id)
+ );
if (task?.Parameters) {
parametersToUse = task.Parameters;
}
@@ -258,7 +497,7 @@ const CippSchedulerForm = (props) => {
formControl.setValue("RawJsonParameters", "{}");
}
}
- }, [advancedParameters, router.query.id, scheduledTaskList.isSuccess]);
+ }, [advancedParameters, taskId, router.query.id, scheduledTaskList.isSuccess]);
const gridSize = fullWidth ? 12 : 4; // Adjust size based on fullWidth prop
@@ -268,6 +507,7 @@ const CippSchedulerForm = (props) => {
{(scheduledTaskList.isFetching || tenantList.isLoading || commands.isLoading) && (
)}
+ {/* Top section: Tenant and Task Name */}
{
name="Name"
label="Task Name"
formControl={formControl}
- />
-
-
-
- {
- const baseOptions =
- commands.data?.map((command) => {
- return {
- label: command.Function,
- value: command.Function,
- addedFields: command,
- };
- }) || [];
-
- // If we're editing a task and the command isn't in the base options, add it
- if (router.query.id && scheduledTaskList.isSuccess) {
- const task = scheduledTaskList.data.find((task) => task.RowKey === router.query.id);
- if (task?.Command && !baseOptions.find((opt) => opt.value === task.Command)) {
- baseOptions.unshift({
- label: task.Command,
- value: task.Command,
- addedFields: {
- Function: task.Command,
- Parameters: [],
- },
- });
- }
- }
-
- return baseOptions;
- })()}
- validators={{
- validate: (value) => {
- if (!value) {
- return "Please select a Command";
- }
- return true;
- },
- }}
- />
-
-
-
-
+
+
{
- let options = [...recurrenceOptions];
-
- // If we're editing a task and the recurrence isn't in the base options, add it
- if (router.query.id && scheduledTaskList.isSuccess) {
- const task = scheduledTaskList.data.find((task) => task.RowKey === router.query.id);
- if (task?.Recurrence && !options.find((opt) => opt.value === task.Recurrence)) {
- options.push({
- value: task.Recurrence,
- label: `Custom: ${task.Recurrence}`,
- });
- }
- }
-
- return options;
- })()}
- multiple={false}
- disableClearable={true}
- creatable={true}
+ multiple
+ creatable={false}
+ options={[
+ { label: "Webhook", value: "Webhook" },
+ { label: "Email", value: "Email" },
+ { label: "PSA", value: "PSA" },
+ ]}
/>
- {selectedCommand?.addedFields?.Synopsis && (
-
-
- PowerShell Command:
-
- {selectedCommand.addedFields.Synopsis}
-
-
-
- )}
- {selectedCommand?.addedFields?.Parameters?.map((param, idx) => (
-
-
- {param.Type === "System.Boolean" ||
- param.Type === "System.Management.Automation.SwitchParameter" ? (
-
- ) : param.Type === "System.Collections.Hashtable" ? (
-
- ) : param.Type?.startsWith("System.String") ? (
-
- ) : (
-
- )}
-
-
- ))}
+ {/* Divider */}
-
+
+
+ {/* Task Type Selection */}
-
+
+ {taskTypeOptions.map((option) => (
+
+ ))}
+
+
+ {/* Trigger Configuration Accordion */}
- getCippValidator(value, "json"),
- }}
- formControl={formControl}
- multiline
- rows={6}
- maxRows={30}
- sx={{
- "& .MuiInputBase-root": {
- overflow: "auto",
- minHeight: "200px",
- },
- }}
- placeholder={`Enter a JSON object`}
- />
+
+ }>
+
+ Trigger Configuration
+ {getTriggerSummary() && (
+
+ - {getTriggerSummary()}
+
+ )}
+
+
+
+
+
+
+
+
+ {/* Delta Query Configuration */}
+
+
+
+
+ Delta queries track changes to Microsoft Graph resources. Learn more about{" "}
+
+ delta query concepts and usage
+ {" "}
+ in the Microsoft documentation.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Condition Builder for all event types */}
+
+
+
+
+
+
+
+
+ Delta Query Conditions
+ }
+ onClick={handleAddCondition}
+ variant="outlined"
+ size="small"
+ >
+ Add Condition
+
+
+
+ Create PowerShell-style Where-Object conditions to filter delta query
+ results. Each condition compares a resource property against a specific
+ value. Multiple conditions work as AND logic - all must be true to trigger
+ the task.
+
+
+
+ {addedConditions.map((condition, index) => (
+
+
+
+
+
+
+
+
+
+
+
+ handleRemoveCondition(index)} color="error">
+
+
+
+
+ ))}
+
+
+ {/* Delta Query Execution Options */}
+
+
+
+
+
+
+
+
+ {/* Trigger Recurrence */}
+
+
+
+
+
+
+
+
+
+
+ {/* Schedule Configuration - Only for scheduled tasks */}
+
+
+
+ }>
+
+ Schedule Configuration
+ {getScheduleSummary() && (
+
+ - {getScheduleSummary()}
+
+ )}
+
+
+
+
+
+
+
+
+ {
+ let options = [...recurrenceOptions];
+
+ // If we're editing a task and the recurrence isn't in the base options, add it
+ if ((taskId || router.query.id) && scheduledTaskList.isSuccess) {
+ const task = scheduledTaskList.data.find(
+ (task) => task.RowKey === (taskId || router.query.id)
+ );
+ if (
+ task?.Recurrence &&
+ !options.find((opt) => opt.value === task.Recurrence)
+ ) {
+ options.push({
+ value: task.Recurrence,
+ label: `Custom: ${task.Recurrence}`,
+ });
+ }
+ }
+
+ return options;
+ })()}
+ multiple={false}
+ disableClearable={true}
+ creatable={true}
+ />
+
+
+
+
+
+ {/* Command & Parameters - For both scheduled and triggered tasks */}
-
+
+ }>
+
+ Command & Parameters
+ {getCommandSummary() && (
+
+ - {getCommandSummary()}
+
+ )}
+
+
+
+
+ {/* Command selection for both scheduled and triggered tasks */}
+
+ {
+ const baseOptions =
+ commands.data?.map((command) => {
+ return {
+ label: command.Function,
+ value: command.Function,
+ addedFields: command,
+ };
+ }) || [];
+
+ // If we're editing a task and the command isn't in the base options, add it
+ if ((taskId || router.query.id) && scheduledTaskList.isSuccess) {
+ const task = scheduledTaskList.data.find(
+ (task) => task.RowKey === (taskId || router.query.id)
+ );
+ if (
+ task?.Command &&
+ !baseOptions.find((opt) => opt.value === task.Command)
+ ) {
+ baseOptions.unshift({
+ label: task.Command,
+ value: task.Command,
+ addedFields: {
+ Function: task.Command,
+ Parameters: [],
+ },
+ });
+ }
+ }
+
+ return baseOptions;
+ })()}
+ validators={{
+ validate: (value) => {
+ if (!value) {
+ return "Please select a Command";
+ }
+ return true;
+ },
+ }}
+ />
+
+
+ {selectedCommand?.addedFields?.Synopsis && (
+
+
+ PowerShell Command:
+
+ {selectedCommand.addedFields.Synopsis}
+
+
+
+ )}
+
+ {selectedCommand?.addedFields?.Parameters?.map((param, idx) => (
+
+
+ {param.Type === "System.Boolean" ||
+ param.Type === "System.Management.Automation.SwitchParameter" ? (
+
+ ) : param.Type === "System.Collections.Hashtable" ? (
+
+ ) : param.Type?.startsWith("System.String") ? (
+
+ ) : (
+
+ )}
+
+
+ ))}
+
+
+
+
+
+
+
+ getCippValidator(value, "json"),
+ }}
+ formControl={formControl}
+ multiline
+ rows={6}
+ maxRows={30}
+ sx={{
+ "& .MuiInputBase-root": {
+ overflow: "auto",
+ minHeight: "200px",
+ },
+ }}
+ placeholder={`Enter a JSON object`}
+ />
+
+
+
+
+
+
diff --git a/src/components/CippStandards/CippStandardsSideBar.jsx b/src/components/CippStandards/CippStandardsSideBar.jsx
index 7c4590fc8cee..09cc642f61bd 100644
--- a/src/components/CippStandards/CippStandardsSideBar.jsx
+++ b/src/components/CippStandards/CippStandardsSideBar.jsx
@@ -418,9 +418,9 @@ const CippStandardsSideBar = ({
{/* Show drift error */}
{isDriftMode && driftError && {driftError}}
- {watchForm.tenantFilter?.some(
+ {(watchForm.tenantFilter?.some(
(tenant) => tenant.value === "AllTenants" || tenant.type === "Group"
- ) && (
+ ) || (watchForm.excludedTenants && watchForm.excludedTenants.length > 0)) && (
<>
{
{cardButton || !hideTitle ? (
<>
-
+
>
) : null}
diff --git a/src/components/PrivateRoute.js b/src/components/PrivateRoute.js
index 34db29fd7a1c..5b067cf4e7c3 100644
--- a/src/components/PrivateRoute.js
+++ b/src/components/PrivateRoute.js
@@ -69,10 +69,14 @@ export const PrivateRoute = ({ children, routeType }) => {
const userRoles = roles?.filter((role) => !blockedRoles.includes(role)) ?? [];
const isAuthenticated = userRoles.length > 0 && !apiRoles?.error;
const isAdmin = roles?.includes("admin") || roles?.includes("superadmin");
- if (routeType === "admin") {
- return !isAdmin ? : children;
- } else {
- return !isAuthenticated ? : children;
+ if (routeType === "admin" && !isAdmin) {
+ return ;
}
+
+ if (!isAuthenticated) {
+ return ;
+ }
+
+ return children;
}
};
diff --git a/src/components/ReleaseNotesDialog.js b/src/components/ReleaseNotesDialog.js
new file mode 100644
index 000000000000..6fc9274dbca3
--- /dev/null
+++ b/src/components/ReleaseNotesDialog.js
@@ -0,0 +1,474 @@
+import {
+ Component,
+ forwardRef,
+ useCallback,
+ useEffect,
+ useImperativeHandle,
+ useMemo,
+ useRef,
+ useState,
+} from "react";
+import {
+ Box,
+ Button,
+ CircularProgress,
+ Dialog,
+ DialogActions,
+ DialogContent,
+ DialogTitle,
+ Link,
+ Stack,
+ Typography,
+} from "@mui/material";
+import ReactMarkdown from "react-markdown";
+import remarkGfm from "remark-gfm";
+import remarkParse from "remark-parse";
+import rehypeRaw from "rehype-raw";
+import { unified } from "unified";
+import packageInfo from "../../public/version.json";
+import { ApiGetCall } from "../api/ApiCall";
+import { GitHub } from "@mui/icons-material";
+import { CippAutoComplete } from "./CippComponents/CippAutocomplete";
+
+const RELEASE_COOKIE_KEY = "cipp_release_notice";
+const RELEASE_OWNER = "KelvinTegelaar";
+const RELEASE_REPO = "CIPP";
+
+const secureFlag = () => {
+ if (typeof window === "undefined") {
+ return "";
+ }
+
+ return window.location.protocol === "https:" ? " Secure" : "";
+};
+
+const getCookie = (name) => {
+ if (typeof document === "undefined") {
+ return null;
+ }
+
+ const cookiePrefix = `${name}=`;
+ const cookies = document.cookie.split("; ");
+
+ for (const cookie of cookies) {
+ if (cookie.startsWith(cookiePrefix)) {
+ return decodeURIComponent(cookie.slice(cookiePrefix.length));
+ }
+ }
+
+ return null;
+};
+
+const setCookie = (name, value, days = 365) => {
+ if (typeof document === "undefined") {
+ return;
+ }
+
+ const expires = new Date(Date.now() + days * 24 * 60 * 60 * 1000).toUTCString();
+ document.cookie = `${name}=${encodeURIComponent(
+ value
+ )}; expires=${expires}; path=/; SameSite=Lax;${secureFlag()}`;
+};
+
+const buildReleaseMetadata = (version) => {
+ const [major = "0", minor = "0", patch = "0"] = String(version).split(".");
+ const currentTag = `v${major}.${minor}.${patch}`;
+ const baseTag = `v${major}.${minor}.0`;
+ const tagToUse = patch === "0" ? currentTag : baseTag;
+
+ return {
+ currentTag,
+ releaseTag: tagToUse,
+ releaseUrl: `https://github.com/${RELEASE_OWNER}/${RELEASE_REPO}/releases/tag/${tagToUse}`,
+ };
+};
+
+const formatReleaseBody = (body) => {
+ if (!body) {
+ return "";
+ }
+
+ return body.replace(/(^|[^\w/])@([a-zA-Z0-9-]+)/g, (match, prefix, username) => {
+ return `${prefix}[@${username}](https://github.com/${username})`;
+ });
+};
+
+class MarkdownErrorBoundary extends Component {
+ constructor(props) {
+ super(props);
+ this.state = { hasError: false, error: null };
+ }
+
+ static getDerivedStateFromError(error) {
+ return { hasError: true, error };
+ }
+
+ componentDidCatch(error) {
+ if (process.env.NODE_ENV !== "production") {
+ // eslint-disable-next-line no-console
+ console.error("Failed to render release notes", error);
+ }
+ }
+
+ render() {
+ if (this.state.hasError) {
+ return this.props.fallback(this.state.error);
+ }
+
+ return this.props.children;
+ }
+}
+
+export const ReleaseNotesDialog = forwardRef((_props, ref) => {
+ const releaseMeta = useMemo(() => buildReleaseMetadata(packageInfo.version), []);
+ const [isEligible, setIsEligible] = useState(false);
+ const [open, setOpen] = useState(false);
+ const [isExpanded, setIsExpanded] = useState(false);
+ const [manualOpenRequested, setManualOpenRequested] = useState(false);
+ const [selectedReleaseTag, setSelectedReleaseTag] = useState(releaseMeta.releaseTag);
+ const hasOpenedRef = useRef(false);
+
+ useEffect(() => {
+ hasOpenedRef.current = false;
+ }, [releaseMeta.releaseTag]);
+
+ useEffect(() => {
+ setSelectedReleaseTag(releaseMeta.releaseTag);
+ }, [releaseMeta.releaseTag]);
+
+ useEffect(() => {
+ if (typeof window === "undefined") {
+ return;
+ }
+
+ const storedValue = getCookie(RELEASE_COOKIE_KEY);
+
+ if (storedValue !== releaseMeta.releaseTag) {
+ setIsEligible(true);
+ }
+ }, [releaseMeta.releaseTag]);
+
+ const shouldFetchReleaseList = isEligible || manualOpenRequested || open;
+
+ const releaseListQuery = ApiGetCall({
+ url: "/api/ListGitHubReleaseNotes",
+ queryKey: "list-github-release-options",
+ data: {
+ Owner: RELEASE_OWNER,
+ Repository: RELEASE_REPO,
+ },
+ waiting: shouldFetchReleaseList,
+ staleTime: 300000,
+ });
+
+ const isReleaseListLoading = releaseListQuery.isLoading || releaseListQuery.isFetching;
+
+ const releaseCatalog = useMemo(() => {
+ return Array.isArray(releaseListQuery.data) ? releaseListQuery.data : [];
+ }, [releaseListQuery.data]);
+
+ useEffect(() => {
+ if (!releaseCatalog.length) {
+ return;
+ }
+
+ if (!selectedReleaseTag) {
+ setSelectedReleaseTag(releaseCatalog[0].releaseTag);
+ return;
+ }
+
+ const hasSelected = releaseCatalog.some((release) => release.releaseTag === selectedReleaseTag);
+
+ if (!hasSelected) {
+ const fallbackRelease =
+ releaseCatalog.find((release) => release.releaseTag === releaseMeta.releaseTag) ||
+ releaseCatalog[0];
+ if (fallbackRelease) {
+ setSelectedReleaseTag(fallbackRelease.releaseTag);
+ }
+ }
+ }, [releaseCatalog, selectedReleaseTag, releaseMeta.releaseTag]);
+
+ const releaseOptions = useMemo(() => {
+ const mapped = releaseCatalog.map((release) => {
+ const tag = release.releaseTag ?? release.tagName;
+ const label = release.name ? `${release.name} (${tag})` : tag;
+ return {
+ label,
+ value: tag,
+ addedFields: {
+ htmlUrl: release.htmlUrl,
+ publishedAt: release.publishedAt,
+ },
+ };
+ });
+
+ if (selectedReleaseTag && !mapped.some((option) => option.value === selectedReleaseTag)) {
+ mapped.push({
+ label: selectedReleaseTag,
+ value: selectedReleaseTag,
+ addedFields: {
+ htmlUrl: releaseMeta.releaseUrl,
+ publishedAt: null,
+ },
+ });
+ }
+
+ return mapped;
+ }, [releaseCatalog, selectedReleaseTag, releaseMeta.releaseUrl]);
+
+ const selectedReleaseValue = useMemo(() => {
+ if (!selectedReleaseTag) {
+ return null;
+ }
+
+ return (
+ releaseOptions.find((option) => option.value === selectedReleaseTag) || {
+ label: selectedReleaseTag,
+ value: selectedReleaseTag,
+ }
+ );
+ }, [releaseOptions, selectedReleaseTag]);
+
+ const handleReleaseChange = useCallback(
+ (newValue) => {
+ const nextValue = Array.isArray(newValue) ? newValue[0] : newValue;
+ if (nextValue?.value && nextValue.value !== selectedReleaseTag) {
+ setSelectedReleaseTag(nextValue.value);
+ }
+ },
+ [selectedReleaseTag]
+ );
+
+ useImperativeHandle(ref, () => ({
+ open: () => {
+ setManualOpenRequested(true);
+ setOpen(true);
+ },
+ }));
+
+ const selectedReleaseData = useMemo(() => {
+ if (!selectedReleaseTag) {
+ return null;
+ }
+
+ return (
+ releaseCatalog.find((release) => release.releaseTag === selectedReleaseTag) ||
+ releaseCatalog.find((release) => release.releaseTag === releaseMeta.releaseTag) ||
+ null
+ );
+ }, [releaseCatalog, selectedReleaseTag, releaseMeta.releaseTag]);
+
+ const handleDismissUntilNextRelease = () => {
+ const newestRelease = releaseCatalog[0];
+ const tagToStore =
+ newestRelease?.releaseTag ?? newestRelease?.tagName ?? releaseMeta.releaseTag;
+ setCookie(RELEASE_COOKIE_KEY, tagToStore);
+ setOpen(false);
+ setIsExpanded(false);
+ setManualOpenRequested(false);
+ setIsEligible(false);
+ };
+
+ const handleRemindLater = () => {
+ setOpen(false);
+ setIsExpanded(false);
+ setManualOpenRequested(false);
+ };
+
+ const toggleExpanded = () => {
+ setIsExpanded((prev) => !prev);
+ };
+
+ const requestedVersionLabel =
+ selectedReleaseData?.releaseTag ?? selectedReleaseTag ?? releaseMeta.currentTag;
+ const releaseName =
+ selectedReleaseData?.name || selectedReleaseValue?.label || `CIPP ${releaseMeta.currentTag}`;
+ const releaseHeading = releaseName || requestedVersionLabel;
+ const releaseBody = typeof selectedReleaseData?.body === "string" ? selectedReleaseData.body : "";
+ const releaseUrl =
+ selectedReleaseData?.htmlUrl ??
+ selectedReleaseValue?.addedFields?.htmlUrl ??
+ releaseMeta.releaseUrl;
+ const formattedReleaseBody = useMemo(() => formatReleaseBody(releaseBody), [releaseBody]);
+ const gfmSupport = useMemo(() => {
+ if (!formattedReleaseBody) {
+ return { plugins: [remarkGfm], error: null };
+ }
+
+ try {
+ unified().use(remarkParse).use(remarkGfm).parse(formattedReleaseBody);
+ return { plugins: [remarkGfm], error: null };
+ } catch (err) {
+ return { plugins: [], error: err };
+ }
+ }, [formattedReleaseBody]);
+
+ useEffect(() => {
+ if (!isEligible || hasOpenedRef.current) {
+ return;
+ }
+
+ if (releaseCatalog.length || releaseListQuery.error) {
+ setOpen(true);
+ hasOpenedRef.current = true;
+ }
+ }, [isEligible, releaseCatalog.length, releaseListQuery.error]);
+
+ return (
+
+ );
+});
+
+ReleaseNotesDialog.displayName = "ReleaseNotesDialog";
diff --git a/src/contexts/release-notes-context.js b/src/contexts/release-notes-context.js
new file mode 100644
index 000000000000..54f29623522e
--- /dev/null
+++ b/src/contexts/release-notes-context.js
@@ -0,0 +1,30 @@
+import { createContext, useCallback, useContext, useMemo, useRef } from "react";
+import PropTypes from "prop-types";
+import { ReleaseNotesDialog } from "../components/ReleaseNotesDialog";
+
+const ReleaseNotesContext = createContext({
+ openReleaseNotes: () => {},
+});
+
+export const ReleaseNotesProvider = ({ children }) => {
+ const dialogRef = useRef(null);
+
+ const openReleaseNotes = useCallback(() => {
+ dialogRef.current?.open();
+ }, []);
+
+ const value = useMemo(() => ({ openReleaseNotes }), [openReleaseNotes]);
+
+ return (
+
+ {children}
+
+
+ );
+};
+
+ReleaseNotesProvider.propTypes = {
+ children: PropTypes.node.isRequired,
+};
+
+export const useReleaseNotes = () => useContext(ReleaseNotesContext);
diff --git a/src/data/alerts.json b/src/data/alerts.json
index 7ca2114492e7..082fe856f297 100644
--- a/src/data/alerts.json
+++ b/src/data/alerts.json
@@ -213,6 +213,12 @@
"recommendedRunInterval": "7d",
"description": "Monitors domain security scores from the DomainAnalyser and alerts when scores fall below the specified threshold."
},
+ {
+ "name": "MXRecordChanged",
+ "label": "Alert on MX record changes",
+ "recommendedRunInterval": "1d",
+ "description": "Monitors MX records for all domains and alerts when changes are detected. This helps identify potential mail routing changes that could indicate security issues or unauthorized modifications."
+ },
{
"name": "GlobalAdminNoAltEmail",
"label": "Alert on Global Admin accounts without alternate email address",
diff --git a/src/data/standards.json b/src/data/standards.json
index d146bf5b9a7a..c29a094ed366 100644
--- a/src/data/standards.json
+++ b/src/data/standards.json
@@ -912,6 +912,39 @@
"powershellEquivalent": "Update-MgPolicyAuthorizationPolicy",
"recommendedBy": ["CIS", "CIPP"]
},
+ {
+ "name": "standards.BitLockerKeysForOwnedDevice",
+ "cat": "Entra (AAD) Standards",
+ "tag": [],
+ "helpText": "Controls whether standard users can recover BitLocker keys for devices they own.",
+ "docsDescription": "Updates the Microsoft Entra authorization policy that controls whether standard users can read BitLocker recovery keys for devices they own. Choose to restrict access for tighter security or allow self-service recovery when operational needs require it.",
+ "executiveText": "Gives administrators centralized control over BitLocker recovery secrets—restrict access to ensure IT-assisted recovery flows, or allow self-service when rapid device unlocks are a priority.",
+ "addedComponent": [
+ {
+ "type": "autoComplete",
+ "multiple": false,
+ "creatable": false,
+ "label": "Select state",
+ "name": "standards.BitLockerKeysForOwnedDevice.state",
+ "options": [
+ {
+ "label": "Restrict",
+ "value": "restrict"
+ },
+ {
+ "label": "Allow",
+ "value": "allow"
+ }
+ ]
+ }
+ ],
+ "label": "Control BitLocker key recovery for owned devices",
+ "impact": "Low Impact",
+ "impactColour": "info",
+ "addedDate": "2025-10-12",
+ "powershellEquivalent": "Update-MgBetaPolicyAuthorizationPolicy",
+ "recommendedBy": []
+ },
{
"name": "standards.DisableSecurityGroupUsers",
"cat": "Entra (AAD) Standards",
diff --git a/src/layouts/account-popover.js b/src/layouts/account-popover.js
index cc0da2049060..579780b6adc8 100644
--- a/src/layouts/account-popover.js
+++ b/src/layouts/account-popover.js
@@ -24,7 +24,8 @@ import {
import { usePopover } from "../hooks/use-popover";
import { paths } from "../paths";
import { ApiGetCall } from "../api/ApiCall";
-import { CogIcon } from "@heroicons/react/24/outline";
+import { CogIcon, DocumentTextIcon } from "@heroicons/react/24/outline";
+import { useReleaseNotes } from "../contexts/release-notes-context";
import { useQueryClient } from "@tanstack/react-query";
export const AccountPopover = (props) => {
@@ -39,11 +40,25 @@ export const AccountPopover = (props) => {
const mdDown = useMediaQuery((theme) => theme.breakpoints.down("md"));
const popover = usePopover();
const queryClient = useQueryClient();
+ const { openReleaseNotes } = useReleaseNotes();
const orgData = ApiGetCall({
url: "/api/me",
queryKey: "authmecipp",
});
+ const userDetails = orgData.data?.clientPrincipal?.userDetails;
+
+ // Cache user photo with user-specific key
+ const userPhoto = ApiGetCall({
+ url: "/api/ListUserPhoto",
+ data: { UserID: userDetails },
+ queryKey: `userPhoto-${userDetails}`,
+ waiting: !!userDetails,
+ staleTime: Infinity,
+ responseType: "blob",
+ convertToDataUrl: true,
+ });
+
const handleLogout = useCallback(async () => {
try {
popover.handleClose();
@@ -63,15 +78,12 @@ export const AccountPopover = (props) => {
sx={{
height: 40,
width: 40,
+ fontSize: 20,
}}
variant="rounded"
- src={
- orgData.data?.clientPrincipal?.userDetails
- ? `/api/ListUserPhoto?UserID=${orgData.data?.clientPrincipal?.userDetails}`
- : ""
- }
+ src={userPhoto.data && !userPhoto.isError ? userPhoto.data : undefined}
>
- {orgData.data?.userDetails?.[0] || ""}
+ {userDetails?.[0]?.toUpperCase() || ""}
);
@@ -152,6 +164,19 @@ export const AccountPopover = (props) => {
+ {
+ popover.handleClose();
+ openReleaseNotes();
+ }}
+ >
+
+
+
+
+
+
+
diff --git a/src/layouts/side-nav.js b/src/layouts/side-nav.js
index 47c601fa3027..772671f5e3db 100644
--- a/src/layouts/side-nav.js
+++ b/src/layouts/side-nav.js
@@ -135,6 +135,11 @@ export const SideNav = (props) => {
imagesrc: "/sponsors/huntress_teal.png",
priority: 1,
},
+ {
+ link: "https://rightofboom.com/rob-2026-overview/rob-2026-registration/?utm_source=CIPP&utm_medium=referral&utm_campaign=CIPPM365&utm_content=cta_button",
+ imagesrc: theme === "light" ? "/sponsors/RoB-light.png" : "/sponsors/RoB.png",
+ priority: 1,
+ },
];
const randomSponsorImage = () => {
@@ -237,7 +242,7 @@ export const SideNav = (props) => {
cursor: "pointer",
maxHeight: "50px", // Limit the height of the image
width: "auto",
- maxWidth: "100px" // Maintain aspect ratio with max width
+ maxWidth: "100px", // Maintain aspect ratio with max width
}}
onClick={() => window.open(randomimg.link)}
/>
diff --git a/src/pages/_app.js b/src/pages/_app.js
index 3623d863d18a..b7af5f4a3bcc 100644
--- a/src/pages/_app.js
+++ b/src/pages/_app.js
@@ -4,6 +4,7 @@ import { Provider as ReduxProvider } from "react-redux";
import { CacheProvider } from "@emotion/react";
import { ThemeProvider } from "@mui/material/styles";
import CssBaseline from "@mui/material/CssBaseline";
+import { ReleaseNotesProvider } from "../contexts/release-notes-context";
import { SettingsConsumer, SettingsProvider } from "../contexts/settings-context";
import { RTL } from "../components/rtl";
import { store } from "../store";
@@ -298,7 +299,9 @@ const App = (props) => {
- {getLayout()}
+
+ {getLayout()}
+
{
- const actions = CippScheduledTaskActions();
+ const [editTaskId, setEditTaskId] = useState(null);
+ const [cloneTaskId, setCloneTaskId] = useState(null);
+
+ const drawerHandlers = {
+ openEditDrawer: (row) => {
+ setEditTaskId(row.RowKey);
+ },
+ openCloneDrawer: (row) => {
+ setCloneTaskId(row.RowKey);
+ },
+ };
+
+ const actions = CippScheduledTaskActions(drawerHandlers);
const filterList = [
{
@@ -40,39 +52,62 @@ const Page = () => {
};
const [showHiddenJobs, setShowHiddenJobs] = useState(false);
return (
-
-
- } component={Link} href="/cipp/scheduler/job">
- Add Job
-
- >
- }
- tenantInTitle={false}
- title="Scheduled Tasks"
- apiUrl={
- showHiddenJobs ? "/api/ListScheduledItems?ShowHidden=true" : "/api/ListScheduledItems"
- }
- queryKey={showHiddenJobs ? `ListScheduledItems-hidden` : `ListScheduledItems`}
- simpleColumns={[
- "ExecutedTime",
- "TaskState",
- "Tenant",
- "Name",
- "ScheduledTime",
- "Command",
- "Parameters",
- "PostExecution",
- "Recurrence",
- "Results",
- ]}
- actions={actions}
- offCanvas={offCanvas}
- filters={filterList}
- />
+ <>
+
+
+
+ >
+ }
+ tenantInTitle={false}
+ title="Scheduled Tasks"
+ apiUrl={
+ showHiddenJobs ? "/api/ListScheduledItems?ShowHidden=true" : "/api/ListScheduledItems"
+ }
+ queryKey={showHiddenJobs ? `ListScheduledItems-hidden` : `ListScheduledItems`}
+ simpleColumns={[
+ "ExecutedTime",
+ "TaskState",
+ "Tenant",
+ "Name",
+ "ScheduledTime",
+ "Command",
+ "Parameters",
+ "PostExecution",
+ "Recurrence",
+ "Results",
+ ]}
+ actions={actions}
+ offCanvas={offCanvas}
+ filters={filterList}
+ />
+
+ {/* Edit Drawer */}
+ {editTaskId && (
+ setEditTaskId(null)}
+ onClose={() => setEditTaskId(null)}
+ PermissionButton={({ children }) => <>{children}>}
+ />
+ )}
+
+ {/* Clone Drawer */}
+ {cloneTaskId && (
+ setCloneTaskId(null)}
+ onClose={() => setCloneTaskId(null)}
+ PermissionButton={({ children }) => <>{children}>}
+ />
+ )}
+ >
);
};
diff --git a/src/pages/cipp/settings/partner-webhooks.js b/src/pages/cipp/settings/partner-webhooks.js
index 67d218fb22c6..f5ac4dd87889 100644
--- a/src/pages/cipp/settings/partner-webhooks.js
+++ b/src/pages/cipp/settings/partner-webhooks.js
@@ -27,7 +27,7 @@ import { useState } from "react";
import { Close } from "@mui/icons-material";
const Page = () => {
- const pageTitle = "Partner Webhooks";
+ const pageTitle = "Automated onboarding";
const [testRunning, setTestRunning] = useState(false);
const [correlationId, setCorrelationId] = useState(null);
const [validateRunning, setValidateRunning] = useState(false);
diff --git a/src/pages/email/administration/tenant-allow-block-lists/add.jsx b/src/pages/email/administration/tenant-allow-block-lists/add.jsx
deleted file mode 100644
index ad8fda8cd4f6..000000000000
--- a/src/pages/email/administration/tenant-allow-block-lists/add.jsx
+++ /dev/null
@@ -1,300 +0,0 @@
-import { useEffect } from "react";
-import "@mui/material";
-import { Grid } from "@mui/system";
-import { useForm, useWatch } from "react-hook-form";
-import { Layout as DashboardLayout } from "/src/layouts/index.js";
-import CippFormPage from "/src/components/CippFormPages/CippFormPage";
-import CippFormComponent from "/src/components/CippComponents/CippFormComponent";
-import { useSettings } from "../../../../hooks/use-settings";
-import { getCippValidator } from "/src/utils/get-cipp-validator";
-
-const AddTenantAllowBlockList = () => {
- const tenantDomain = useSettings().currentTenant;
-
- const formControl = useForm({
- mode: "onChange",
- defaultValues: {
- entries: "",
- notes: "",
- listType: null,
- listMethod: null,
- NoExpiration: false,
- RemoveAfter: false,
- },
- });
-
- const noExpiration = useWatch({ control: formControl.control, name: "NoExpiration" });
- const removeAfter = useWatch({ control: formControl.control, name: "RemoveAfter" });
- const listMethod = useWatch({ control: formControl.control, name: "listMethod" });
- const listType = useWatch({ control: formControl.control, name: "listType" });
-
- const isListMethodBlock = listMethod?.value === "Block";
- const isListTypeFileHash = listType?.value === "FileHash";
- const isListTypeSenderUrlOrFileHash = ["Sender", "Url", "FileHash"].includes(listType?.value);
- const isNoExpirationCompatible = isListMethodBlock ||
- (listMethod?.value === "Allow" && (listType?.value === "Url" || listType?.value === "IP"));
-
- useEffect(() => {
- if (noExpiration) {
- formControl.setValue("RemoveAfter", false);
- }
-
- if (removeAfter) {
- formControl.setValue("NoExpiration", false);
- }
-
- if (isListMethodBlock) {
- formControl.setValue("RemoveAfter", false);
- }
-
- if (listType && !isListTypeSenderUrlOrFileHash) {
- formControl.setValue("RemoveAfter", false);
- }
-
- if (isListTypeFileHash) {
- formControl.setValue("listMethod", { label: "Block", value: "Block" });
- }
-
- if (listMethod || listType) {
- if (!isNoExpirationCompatible && noExpiration) {
- formControl.setValue("NoExpiration", false);
- }
- }
- }, [
- noExpiration,
- removeAfter,
- isListMethodBlock,
- listType,
- isListTypeSenderUrlOrFileHash,
- isListTypeFileHash,
- isNoExpirationCompatible,
- formControl
- ]);
-
- const validateEntries = (value) => {
- if (!value) return true;
-
- const entries = value.split(/[,;]/).map(e => e.trim());
- const currentListType = listType?.value;
-
- if (currentListType === "FileHash") {
- for (const entry of entries) {
- if (entry.length !== 64)
- return "File hash entries must be exactly 64 characters";
-
- const hashResult = getCippValidator(entry, "sha256");
- if (hashResult !== true)
- return hashResult;
- }
- } else if (currentListType === "IP") {
- for (const entry of entries) {
- const ipv6Result = getCippValidator(entry, "ipv6");
- const ipv6CidrResult = getCippValidator(entry, "ipv6cidr");
-
- if (ipv6Result !== true && ipv6CidrResult !== true)
- return "Invalid IPv6 address format. Use colon-hexadecimal or CIDR notation";
- }
- } else if (currentListType === "Url") {
- for (const entry of entries) {
- if (entry.length > 250)
- return "URL entries must be 250 characters or less";
-
- // For entries with wildcards, use the improved wildcard validators
- if (entry.includes('*') || entry.includes('~')) {
- // Try both wildcard validators
- const wildcardUrlResult = getCippValidator(entry, "wildcardUrl");
- const wildcardDomainResult = getCippValidator(entry, "wildcardDomain");
-
- if (wildcardUrlResult !== true && wildcardDomainResult !== true) {
- // If basic pattern check fails too, give a more specific message
- if (!/^[a-zA-Z0-9\.\-\*\~\/]+$/.test(entry)) {
- return "Invalid wildcard pattern. Use only letters, numbers, dots, hyphens, slashes, and wildcards (* or ~)";
- }
-
- // If it has basic valid characters but doesn't match our patterns
- return "Invalid wildcard format. Common formats are *.domain.com or domain.*";
- }
- continue;
- }
-
- // For non-wildcard entries, use standard validators
- const ipv4Result = getCippValidator(entry, "ip");
- const ipv4CidrResult = getCippValidator(entry, "ipv4cidr");
- const ipv6Result = getCippValidator(entry, "ipv6");
- const ipv6CidrResult = getCippValidator(entry, "ipv6cidr");
- const hostnameResult = getCippValidator(entry, "hostname");
- const urlResult = getCippValidator(entry, "url");
-
- // If none of the validators pass
- if (ipv4Result !== true &&
- ipv4CidrResult !== true &&
- ipv6Result !== true &&
- ipv6CidrResult !== true &&
- hostnameResult !== true &&
- urlResult !== true) {
- return "Invalid URL format. Enter hostnames, IPv4, or IPv6 addresses";
- }
- }
- } else if (currentListType === "Sender") {
- for (const entry of entries) {
- // Check for wildcards first
- if (entry.includes('*') || entry.includes('~')) {
- const wildcardDomainResult = getCippValidator(entry, "wildcardDomain");
-
- if (wildcardDomainResult !== true) {
- return "Invalid sender wildcard pattern. Common format is *.domain.com";
- }
- continue;
- }
-
- // For non-wildcard entries, use senderEntry validator
- const senderResult = getCippValidator(entry, "senderEntry");
-
- if (senderResult !== true) {
- return senderResult;
- }
- }
- }
-
- return true;
- };
-
- return (
- {
- return {
- tenantID: tenantDomain,
- entries: values.entries,
- listType: values.listType?.value,
- notes: values.notes,
- listMethod: values.listMethod?.value,
- NoExpiration: values.NoExpiration,
- RemoveAfter: values.RemoveAfter
- };
- }}
- >
-
- {/* Entries */}
-
-
-
- {/* Notes & List Type */}
-
-
-
-
-
-
-
- {/* List Method */}
-
-
-
-
- {/* No Expiration */}
-
-
-
-
- {/* Remove After */}
-
-
-
-
-
- );
-};
-
-AddTenantAllowBlockList.getLayout = (page) => {page};
-
-export default AddTenantAllowBlockList;
diff --git a/src/pages/email/administration/tenant-allow-block-lists/index.js b/src/pages/email/administration/tenant-allow-block-lists/index.js
index 4770163895fa..628eacd1be83 100644
--- a/src/pages/email/administration/tenant-allow-block-lists/index.js
+++ b/src/pages/email/administration/tenant-allow-block-lists/index.js
@@ -1,12 +1,11 @@
import { Layout as DashboardLayout } from "/src/layouts/index.js";
import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx";
-import { Button } from "@mui/material";
-import Link from "next/link";
-import TrashIcon from "@heroicons/react/24/outline/TrashIcon";
-import { PlaylistAdd } from "@mui/icons-material";
+import { Delete } from "@mui/icons-material";
+import { CippAddTenantAllowBlockListDrawer } from "/src/components/CippComponents/CippAddTenantAllowBlockListDrawer.jsx";
const Page = () => {
const pageTitle = "Tenant Allow/Block Lists";
+ const cardButtonPermissions = ["Exchange.SpamFilter.ReadWrite"];
const actions = [
{
@@ -19,7 +18,7 @@ const Page = () => {
},
confirmText: "Are you sure you want to delete this entry?",
color: "danger",
- icon: ,
+ icon: ,
},
];
@@ -39,21 +38,8 @@ const Page = () => {
apiUrl="/api/ListTenantAllowBlockList"
actions={actions}
simpleColumns={simpleColumns}
- titleButton={{
- label: "Add",
- href: "/email/administration/tenant-allow-block-list/add",
- }}
- cardButton={
- <>
- }
- >
- Add Entry
-
- >
- }
+ apiDataKey="Results"
+ cardButton={}
/>
);
};
diff --git a/src/pages/email/transport/new-rules/add.jsx b/src/pages/email/transport/new-rules/add.jsx
index 76083d0ca3d7..4e6029efc601 100644
--- a/src/pages/email/transport/new-rules/add.jsx
+++ b/src/pages/email/transport/new-rules/add.jsx
@@ -31,6 +31,107 @@ const AddTransportRule = () => {
const exceptionTypeWatch = useWatch({ control: formControl.control, name: "exceptionType" });
const applyToAllMessagesWatch = useWatch({ control: formControl.control, name: "applyToAllMessages" });
+ // Helper function to get field names for a condition
+ const getConditionFieldNames = (conditionValue) => {
+ const fields = [conditionValue];
+ // Add related fields for special cases
+ if (conditionValue === "HeaderContainsWords") {
+ fields.push("HeaderContainsWordsMessageHeader");
+ } else if (conditionValue === "HeaderMatchesPatterns") {
+ fields.push("HeaderMatchesPatternsMessageHeader");
+ }
+ return fields;
+ };
+
+ // Helper function to get field names for an action
+ const getActionFieldNames = (actionValue) => {
+ const fields = [];
+ switch (actionValue) {
+ case "RejectMessage":
+ fields.push("RejectMessageReasonText", "RejectMessageEnhancedStatusCode");
+ break;
+ case "SetHeader":
+ fields.push("SetHeaderName", "SetHeaderValue");
+ break;
+ case "ApplyHtmlDisclaimer":
+ fields.push("ApplyHtmlDisclaimerText", "ApplyHtmlDisclaimerLocation", "ApplyHtmlDisclaimerFallbackAction");
+ break;
+ default:
+ fields.push(actionValue);
+ }
+ return fields;
+ };
+
+ // Update selected conditions and clean up removed ones
+ useEffect(() => {
+ const newConditions = conditionTypeWatch || [];
+ const newConditionValues = newConditions.map(c => c.value || c);
+ const oldConditionValues = selectedConditions.map(c => c.value || c);
+
+ // Find removed conditions
+ const removedConditions = oldConditionValues.filter(
+ oldVal => !newConditionValues.includes(oldVal)
+ );
+
+ // Clear form values for removed conditions
+ removedConditions.forEach(conditionValue => {
+ const fieldNames = getConditionFieldNames(conditionValue);
+ fieldNames.forEach(fieldName => {
+ formControl.setValue(fieldName, undefined);
+ });
+ });
+
+ setSelectedConditions(newConditions);
+ }, [conditionTypeWatch]);
+
+ // Update selected actions and clean up removed ones
+ useEffect(() => {
+ const newActions = actionTypeWatch || [];
+ const newActionValues = newActions.map(a => a.value || a);
+ const oldActionValues = selectedActions.map(a => a.value || a);
+
+ // Find removed actions
+ const removedActions = oldActionValues.filter(
+ oldVal => !newActionValues.includes(oldVal)
+ );
+
+ // Clear form values for removed actions
+ removedActions.forEach(actionValue => {
+ const fieldNames = getActionFieldNames(actionValue);
+ fieldNames.forEach(fieldName => {
+ formControl.setValue(fieldName, undefined);
+ });
+ });
+
+ setSelectedActions(newActions);
+ }, [actionTypeWatch]);
+
+ // Update selected exceptions and clean up removed ones
+ useEffect(() => {
+ const newExceptions = exceptionTypeWatch || [];
+ const newExceptionValues = newExceptions.map(e => e.value || e);
+ const oldExceptionValues = selectedExceptions.map(e => e.value || e);
+
+ // Find removed exceptions
+ const removedExceptions = oldExceptionValues.filter(
+ oldVal => !newExceptionValues.includes(oldVal)
+ );
+
+ // Clear form values for removed exceptions
+ removedExceptions.forEach(exceptionValue => {
+ // Get base condition name (remove ExceptIf prefix)
+ const baseCondition = exceptionValue.replace("ExceptIf", "");
+ const fieldNames = getConditionFieldNames(baseCondition).map(
+ field => field.includes("MessageHeader") ? `ExceptIf${field}` : exceptionValue
+ );
+ fieldNames.forEach(fieldName => {
+ formControl.setValue(fieldName, undefined);
+ });
+ });
+
+ setSelectedExceptions(newExceptions);
+ }, [exceptionTypeWatch]);
+
// Update selected conditions when conditionType changes
useEffect(() => {
setSelectedConditions(conditionTypeWatch || []);
diff --git a/src/pages/endpoint/MEM/devices/index.js b/src/pages/endpoint/MEM/devices/index.js
index 8c2cf07cbcfa..2afc598b490a 100644
--- a/src/pages/endpoint/MEM/devices/index.js
+++ b/src/pages/endpoint/MEM/devices/index.js
@@ -123,7 +123,7 @@ const Page = () => {
confirmText: "Are you sure you want to locate [deviceName]?",
},
{
- label: "Retrieve LAPs password",
+ label: "Retrieve LAPS password",
type: "POST",
icon: ,
url: "/api/ExecGetLocalAdminPassword",
@@ -131,7 +131,7 @@ const Page = () => {
GUID: "azureADDeviceId",
},
condition: (row) => row.operatingSystem === "Windows",
- confirmText: "Are you sure you want to retrieve the local admin password?",
+ confirmText: "Are you sure you want to retrieve the local admin password for [deviceName]?",
},
{
label: "Rotate Local Admin Password",
@@ -154,7 +154,19 @@ const Page = () => {
GUID: "azureADDeviceId",
},
condition: (row) => row.operatingSystem === "Windows",
- confirmText: "Are you sure you want to retrieve the BitLocker keys?",
+ confirmText: "Are you sure you want to retrieve the BitLocker keys for [deviceName]?",
+ },
+ {
+ label: "Retrieve File Vault Key",
+ type: "POST",
+ icon: ,
+ url: "/api/ExecDeviceAction",
+ data: {
+ GUID: "id",
+ Action: "getFileVaultKey",
+ },
+ condition: (row) => row.operatingSystem === "macOS",
+ confirmText: "Are you sure you want to retrieve the file vault key for [deviceName]?",
},
{
label: "Windows Defender Full Scan",
diff --git a/src/pages/endpoint/MEM/list-templates/edit.jsx b/src/pages/endpoint/MEM/list-templates/edit.jsx
index 71eb1bc6c00c..c58cbe7e0920 100644
--- a/src/pages/endpoint/MEM/list-templates/edit.jsx
+++ b/src/pages/endpoint/MEM/list-templates/edit.jsx
@@ -19,7 +19,7 @@ const EditIntuneTemplate = () => {
});
const templateData = Array.isArray(templateQuery.data)
- ? templateQuery.data.find((t) => t.id === id)
+ ? templateQuery.data.find((t) => t.id === id || t.GUID === id)
: templateQuery.data;
// Custom data formatter to convert autoComplete objects to values
diff --git a/src/pages/endpoint/MEM/list-templates/index.js b/src/pages/endpoint/MEM/list-templates/index.js
index 2c06faf5f4da..d35cf8dc097e 100644
--- a/src/pages/endpoint/MEM/list-templates/index.js
+++ b/src/pages/endpoint/MEM/list-templates/index.js
@@ -139,13 +139,14 @@ const Page = () => {
size: "lg",
};
- const simpleColumns = ["displayName", "package", "description", "Type"];
+ const simpleColumns = ["displayName", "isSynced", "package", "description", "Type"];
return (
<>
{
RemoveOwner: [],
AddContact: [],
RemoveContact: [],
+ visibility: "Public",
},
});
@@ -74,6 +75,7 @@ const EditGroup = () => {
allowExternal: groupInfo?.data?.allowExternal,
sendCopies: groupInfo?.data?.sendCopies,
hideFromOutlookClients: groupInfo?.data?.hideFromOutlookClients,
+ visibility: group?.visibility ?? "Public",
displayName: group.displayName,
description: group.description || "",
membershipRules: group.membershipRule || "",
@@ -114,6 +116,7 @@ const EditGroup = () => {
sendCopies: groupInfo?.data?.sendCopies,
hideFromOutlookClients: groupInfo?.data?.hideFromOutlookClients,
securityEnabled: group.securityEnabled,
+ visibility: group.visibility ?? "Public",
});
// Reset the form with all values
@@ -132,6 +135,7 @@ const EditGroup = () => {
"sendCopies",
"hideFromOutlookClients",
"securityEnabled",
+ "visibility",
];
changeDetectionProperties.forEach((property) => {
@@ -377,6 +381,24 @@ const EditGroup = () => {
Group Settings
+
+ {groupType === "Microsoft 365" && (
+
+
+
+ )}
+
{(groupType === "Microsoft 365" || groupType === "Distribution List") && (
{
>
}
title="JIT Admin Table"
- apiUrl="/api/ExecJITAdmin?Action=List"
+ apiUrl="/api/ListJITAdmin"
apiDataKey="Results"
simpleColumns={[]}
/>
diff --git a/src/pages/identity/administration/users/index.js b/src/pages/identity/administration/users/index.js
index 8baf9eaaf76b..27148701979b 100644
--- a/src/pages/identity/administration/users/index.js
+++ b/src/pages/identity/administration/users/index.js
@@ -6,6 +6,8 @@ import { useCippUserActions } from "/src/components/CippComponents/CippUserActio
import { CippInviteGuestDrawer } from "/src/components/CippComponents/CippInviteGuestDrawer.jsx";
import { CippBulkUserDrawer } from "/src/components/CippComponents/CippBulkUserDrawer.jsx";
import { CippAddUserDrawer } from "/src/components/CippComponents/CippAddUserDrawer.jsx";
+import { CippApiLogsDrawer } from "/src/components/CippComponents/CippApiLogsDrawer.jsx";
+import { Box } from "@mui/material";
const Page = () => {
const userActions = useCippUserActions();
@@ -57,7 +59,7 @@ const Page = () => {
title={pageTitle}
apiUrl="/api/ListGraphRequest"
cardButton={
- <>
+
{
requiredPermissions={cardButtonPermissions}
PermissionButton={PermissionButton}
/>
- >
+
+
}
apiData={{
Endpoint: "users",
diff --git a/src/pages/identity/administration/users/user/exchange.jsx b/src/pages/identity/administration/users/user/exchange.jsx
index b81765ed651d..eb51eeaff7f1 100644
--- a/src/pages/identity/administration/users/user/exchange.jsx
+++ b/src/pages/identity/administration/users/user/exchange.jsx
@@ -128,6 +128,16 @@ const Page = () => {
};
}
+ // Handle arrays by joining them
+ if (Array.isArray(userIdentifier)) {
+ userIdentifier = userIdentifier.join(", ");
+ }
+
+ // Ensure userIdentifier is a string
+ if (typeof userIdentifier !== "string") {
+ userIdentifier = String(userIdentifier);
+ }
+
// Handle special built-in cases
if (userIdentifier === "Default" || userIdentifier === "Anonymous") {
return {
@@ -147,7 +157,7 @@ const Page = () => {
// Exact match on display name
(group.displayName && group.displayName === userIdentifier) ||
// Partial match - permission identifier starts with group display name (handles timestamps)
- (group.displayName && userIdentifier?.startsWith(group.displayName))
+ (group.displayName && userIdentifier.startsWith(group.displayName))
);
});
@@ -316,10 +326,15 @@ const Page = () => {
useEffect(() => {
if (userRequest.isSuccess && userRequest.data?.[0]) {
const currentSettings = userRequest.data[0];
- const forwardingAddress = currentSettings.ForwardingAddress;
+ let forwardingAddress = currentSettings.ForwardingAddress;
const forwardingSmtpAddress = currentSettings.MailboxActionsData?.ForwardingSmtpAddress;
const forwardAndDeliver = currentSettings.ForwardAndDeliver;
+ // Handle ForwardingAddress being an array or string
+ if (Array.isArray(forwardingAddress)) {
+ forwardingAddress = forwardingAddress.join(", ");
+ }
+
let forwardingType = "disabled";
let cleanAddress = "";
@@ -1245,7 +1260,7 @@ const Page = () => {
isCollapsible={true}
/>
diff --git a/src/pages/tenant/administration/alert-configuration/alert.jsx b/src/pages/tenant/administration/alert-configuration/alert.jsx
index cb51e6235fbc..b5a1b86032dd 100644
--- a/src/pages/tenant/administration/alert-configuration/alert.jsx
+++ b/src/pages/tenant/administration/alert-configuration/alert.jsx
@@ -148,6 +148,7 @@ const AlertWizard = () => {
recurrence: recurrenceOption,
postExecution: postExecutionValue,
startDateTime: startDateTimeForForm,
+ AlertComment: alert.RawAlert.AlertComment || "",
};
// Parse Parameters field if it exists and is a string
@@ -211,6 +212,7 @@ const AlertWizard = () => {
Actions: alert.RawAlert.Actions,
conditions: formattedConditions,
logbook: foundLogbook,
+ AlertComment: alert.RawAlert.AlertComment || "",
};
formControl.reset(resetData);
@@ -344,6 +346,7 @@ const AlertWizard = () => {
DesiredStartTime: values.startDateTime ? values.startDateTime.toString() : null,
Recurrence: values.recurrence,
PostExecution: values.postExecution,
+ AlertComment: values.AlertComment,
};
apiRequest.mutate({ url: "/api/AddScheduledItem?hidden=true", data: postObject });
};
@@ -623,6 +626,17 @@ const AlertWizard = () => {
options={actionsToTake}
/>
+
+
+
@@ -757,6 +771,17 @@ const AlertWizard = () => {
options={postExecutionOptions}
/>
+
+
+
diff --git a/src/pages/tenant/administration/alert-configuration/index.js b/src/pages/tenant/administration/alert-configuration/index.js
index 8040f018d37f..2b2864d14272 100644
--- a/src/pages/tenant/administration/alert-configuration/index.js
+++ b/src/pages/tenant/administration/alert-configuration/index.js
@@ -1,76 +1,77 @@
-import { Button } from "@mui/material";
-import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx";
-import { Layout as DashboardLayout } from "/src/layouts/index.js"; // had to add an extra path here because I added an extra folder structure. We should switch to absolute pathing so we dont have to deal with relative.
-import Link from "next/link";
-import { EyeIcon } from "@heroicons/react/24/outline";
-import { CopyAll, Delete, NotificationAdd } from "@mui/icons-material";
-
-const Page = () => {
- const pageTitle = "Alerts";
- const actions = [
- {
- label: "View Task Details",
- link: "/cipp/scheduler/task?id=[RowKey]",
- icon: ,
- condition: (row) => row?.EventType === "Scheduled Task",
- },
- {
- label: "Edit Alert",
- link: "/tenant/administration/alert-configuration/alert?id=[RowKey]",
- icon: ,
- color: "success",
- target: "_self",
- },
- {
- label: "Clone & Edit Alert",
- link: "/tenant/administration/alert-configuration/alert?id=[RowKey]&clone=true",
- icon: ,
- color: "success",
- target: "_self",
- },
- {
- label: "Delete Alert",
- type: "POST",
- url: "/api/RemoveQueuedAlert",
- data: {
- ID: "RowKey",
- EventType: "EventType",
- },
- icon: ,
- relatedQueryKeys: "ListAlertsQueue",
- confirmText: "Are you sure you want to delete this Alert?",
- multiPost: false,
- },
- ];
-
- return (
- }
- >
- Add Alert
-
- }
- actions={actions}
- simpleColumns={[
- "Tenants",
- "EventType",
- "Conditions",
- "RepeatsEvery",
- "Actions",
- "excludedTenants",
- ]}
- queryKey="ListAlertsQueue"
- />
- );
-};
-
-Page.getLayout = (page) => {page};
-
-export default Page;
+import { Button } from "@mui/material";
+import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx";
+import { Layout as DashboardLayout } from "/src/layouts/index.js"; // had to add an extra path here because I added an extra folder structure. We should switch to absolute pathing so we dont have to deal with relative.
+import Link from "next/link";
+import { EyeIcon } from "@heroicons/react/24/outline";
+import { CopyAll, Delete, NotificationAdd } from "@mui/icons-material";
+
+const Page = () => {
+ const pageTitle = "Alerts";
+ const actions = [
+ {
+ label: "View Task Details",
+ link: "/cipp/scheduler/task?id=[RowKey]",
+ icon: ,
+ condition: (row) => row?.EventType === "Scheduled Task",
+ },
+ {
+ label: "Edit Alert",
+ link: "/tenant/administration/alert-configuration/alert?id=[RowKey]",
+ icon: ,
+ color: "success",
+ target: "_self",
+ },
+ {
+ label: "Clone & Edit Alert",
+ link: "/tenant/administration/alert-configuration/alert?id=[RowKey]&clone=true",
+ icon: ,
+ color: "success",
+ target: "_self",
+ },
+ {
+ label: "Delete Alert",
+ type: "POST",
+ url: "/api/RemoveQueuedAlert",
+ data: {
+ ID: "RowKey",
+ EventType: "EventType",
+ },
+ icon: ,
+ relatedQueryKeys: "ListAlertsQueue",
+ confirmText: "Are you sure you want to delete this Alert?",
+ multiPost: false,
+ },
+ ];
+
+ return (
+ }
+ >
+ Add Alert
+
+ }
+ actions={actions}
+ simpleColumns={[
+ "Tenants",
+ "EventType",
+ "Conditions",
+ "RepeatsEvery",
+ "Actions",
+ "AlertComment",
+ "excludedTenants",
+ ]}
+ queryKey="ListAlertsQueue"
+ />
+ );
+};
+
+Page.getLayout = (page) => {page};
+
+export default Page;
diff --git a/src/pages/tenant/administration/tenants/groups/edit.js b/src/pages/tenant/administration/tenants/groups/edit.js
index 40326136d39e..fa52b69d553b 100644
--- a/src/pages/tenant/administration/tenants/groups/edit.js
+++ b/src/pages/tenant/administration/tenants/groups/edit.js
@@ -1,12 +1,10 @@
import { Layout as DashboardLayout } from "/src/layouts/index.js";
import { useForm } from "react-hook-form";
-import { ApiGetCall, ApiPostCall } from "/src/api/ApiCall";
+import { ApiGetCall } from "/src/api/ApiCall";
import { useEffect } from "react";
import { useRouter } from "next/router";
import { Box } from "@mui/material";
-import { Grid } from "@mui/system";
-import CippPageCard from "/src/components/CippCards/CippPageCard";
-import { CippApiResults } from "/src/components/CippComponents/CippApiResults";
+import CippFormPage from "/src/components/CippFormPages/CippFormPage";
import CippAddEditTenantGroups from "/src/components/CippComponents/CippAddEditTenantGroups";
const Page = () => {
@@ -21,53 +19,156 @@ const Page = () => {
queryKey: id ? `TenantGroupProperties_${id}` : null,
});
- const updateGroupApi = ApiPostCall({
- urlFromData: true,
- relatedQueryKeys: [
- `TenantGroupProperties_${id}`,
- "TenantGroupListPage",
- ],
- });
-
useEffect(() => {
if (groupDetails.isSuccess && groupDetails.data) {
+ const groupData = groupDetails?.data?.Results?.[0];
+
+ // Determine if this is a dynamic or static group
+ const isDynamic = groupData?.GroupType === "dynamic" && groupData?.DynamicRules;
+
+ // Format dynamic rules if they exist
+ let formattedDynamicRules = [{}];
+ if (isDynamic && groupData.DynamicRules) {
+ try {
+ let rules;
+ if (Array.isArray(groupData.DynamicRules)) {
+ rules = groupData.DynamicRules;
+ } else if (typeof groupData.DynamicRules === "string") {
+ rules = JSON.parse(groupData.DynamicRules);
+ } else if (typeof groupData.DynamicRules === "object") {
+ rules = [groupData.DynamicRules];
+ } else {
+ rules = [];
+ }
+
+ formattedDynamicRules = rules.map((rule) => {
+ // Handle value - it's always an array of objects from the backend
+ let valueForForm;
+ if (Array.isArray(rule.value)) {
+ // If it's an array of objects, extract all values
+ valueForForm = rule.value.map((item) => ({
+ label: item.label || item.value || item,
+ value: item.value || item,
+ }));
+ // For single selection operators, take just the first item
+ if (rule.operator === "eq" || rule.operator === "ne") {
+ valueForForm = valueForForm[0];
+ }
+ } else if (typeof rule.value === "object" && rule.value?.value) {
+ // If it's a single object with a value property
+ valueForForm = {
+ label: rule.value.label || rule.value.value,
+ value: rule.value.value,
+ };
+ } else {
+ // Simple value
+ valueForForm = {
+ label: rule.value,
+ value: rule.value,
+ };
+ }
+
+ return {
+ property: {
+ label:
+ rule.property === "availableLicense"
+ ? "Available License"
+ : rule.property === "availableServicePlan"
+ ? "Available Service Plan"
+ : rule.property === "delegatedAccessStatus"
+ ? "Delegated Access Status"
+ : rule.property,
+ value: rule.property,
+ type:
+ rule.property === "availableLicense"
+ ? "license"
+ : rule.property === "availableServicePlan"
+ ? "servicePlan"
+ : rule.property === "delegatedAccessStatus"
+ ? "delegatedAccess"
+ : "unknown",
+ },
+ operator: {
+ label:
+ rule.operator === "eq"
+ ? "Equals"
+ : rule.operator === "ne"
+ ? "Not Equals"
+ : rule.operator === "in"
+ ? "In"
+ : rule.operator === "notIn"
+ ? "Not In"
+ : rule.operator,
+ value: rule.operator,
+ },
+ value: valueForForm,
+ };
+ });
+ } catch (e) {
+ console.error("Error parsing dynamic rules:", e, groupData.DynamicRules);
+ formattedDynamicRules = [{}];
+ }
+ }
+
formControl.reset({
groupId: id,
- groupName: groupDetails?.data?.Results?.[0]?.Name ?? "",
- groupDescription: groupDetails?.data?.Results?.[0]?.Description ?? "",
- members:
- groupDetails?.data?.Results?.[0]?.Members?.map((member) => ({
- label: member.displayName,
- value: member.customerId,
- })) || [],
+ groupName: groupData?.Name ?? "",
+ groupDescription: groupData?.Description ?? "",
+ groupType: isDynamic ? "dynamic" : "static",
+ ruleLogic: groupData?.RuleLogic || "and",
+ members: !isDynamic
+ ? groupData?.Members?.map((member) => ({
+ label: member.displayName,
+ value: member.customerId,
+ })) || []
+ : [],
+ dynamicRules: formattedDynamicRules,
});
}
}, [groupDetails.isSuccess, groupDetails.data]);
+ const customDataFormatter = (values) => {
+ const formattedData = {
+ ...values,
+ Action: "AddEdit",
+ };
+
+ // If it's a dynamic group, format the rules for the backend
+ if (values.groupType === "dynamic" && values.dynamicRules) {
+ formattedData.dynamicRules = values.dynamicRules.map((rule) => ({
+ property: rule.property?.value || rule.property,
+ operator: rule.operator?.value || rule.operator,
+ value: rule.value,
+ }));
+ formattedData.ruleLogic = values.ruleLogic || "and";
+ }
+
+ return formattedData;
+ };
+
return (
-
-
-
-
-
-
+
-
-
-
+
);
};
diff --git a/src/pages/tenant/administration/tenants/groups/index.js b/src/pages/tenant/administration/tenants/groups/index.js
index 8d3e4c328ceb..ea0416d936bf 100644
--- a/src/pages/tenant/administration/tenants/groups/index.js
+++ b/src/pages/tenant/administration/tenants/groups/index.js
@@ -2,22 +2,43 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js";
import { TabbedLayout } from "/src/layouts/TabbedLayout";
import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx";
import tabOptions from "../tabOptions";
-import { Edit } from "@mui/icons-material";
-import { Button, SvgIcon } from "@mui/material";
-import { PlusIcon, TrashIcon } from "@heroicons/react/24/outline";
-import NextLink from "next/link";
+import { Edit, PlayArrow, GroupAdd } from "@mui/icons-material";
+import { TrashIcon } from "@heroicons/react/24/outline";
+import { CippAddTenantGroupDrawer } from "/src/components/CippComponents/CippAddTenantGroupDrawer";
+import { CippApiLogsDrawer } from "/src/components/CippComponents/CippApiLogsDrawer";
+import { CippTenantGroupOffCanvas } from "/src/components/CippComponents/CippTenantGroupOffCanvas";
+import { CippApiDialog } from "/src/components/CippComponents/CippApiDialog.jsx";
+import { Box, Button } from "@mui/material";
+import { useDialog } from "/src/hooks/use-dialog.js";
const Page = () => {
const pageTitle = "Tenant Groups";
+ const createDefaultGroupsDialog = useDialog();
- const simpleColumns = ["Name", "Description", "Members"];
+ const simpleColumns = ["Name", "Description", "GroupType", "Members"];
+ const offcanvas = {
+ children: (row) => {
+ return ;
+ },
+ size: "xl",
+ };
const actions = [
{
label: "Edit Group",
link: "/tenant/administration/tenants/groups/edit?id=[Id]",
icon: ,
},
+ {
+ label: "Run Dynamic Rules",
+ icon: ,
+ url: "/api/ExecRunTenantGroupRule",
+ type: "POST",
+ data: { groupId: "Id" },
+ queryKey: "TenantGroupListPage",
+ confirmText: "Are you sure you want to run dynamic rules for [Name]?",
+ condition: (row) => row.GroupType === "dynamic",
+ },
{
label: "Delete Group",
icon: ,
@@ -26,35 +47,47 @@ const Page = () => {
data: { action: "Delete", groupId: "Id" },
queryKey: "TenantGroupListPage",
confirmText: "Are you sure you want to delete [Name]?",
- }
+ },
];
return (
-
-
-
- }
- >
- Add Tenant Group
-
- }
- />
+ <>
+
+
+ }>
+ Create Default Groups
+
+
+
+ }
+ offCanvas={offcanvas}
+ />
+
+ >
);
};
diff --git a/src/pages/tenant/conditional/list-named-locations/index.js b/src/pages/tenant/conditional/list-named-locations/index.js
index 90bb4e3d43a2..8b19e50281cc 100644
--- a/src/pages/tenant/conditional/list-named-locations/index.js
+++ b/src/pages/tenant/conditional/list-named-locations/index.js
@@ -11,6 +11,7 @@ import {
TrashIcon,
} from "@heroicons/react/24/outline";
import { LocationOn } from "@mui/icons-material";
+import countryList from "/src/data/countryList.json";
const Page = () => {
const pageTitle = "Named Locations";
@@ -62,8 +63,18 @@ const Page = () => {
namedLocationId: "id",
change: "!addLocation",
},
- fields: [{ type: "textField", name: "input", label: "Country Code" }],
- confirmText: "Enter a two-letter country code, e.g., US.",
+ fields: [
+ {
+ type: "autoComplete",
+ name: "input",
+ label: "Country",
+ options: countryList.map(({ Code, Name }) => ({
+ value: Code,
+ label: `${Name} (${Code})`,
+ })),
+ },
+ ],
+ confirmText: "Select a country to add to this named location.",
condition: (row) => row["@odata.type"] == "#microsoft.graph.countryNamedLocation",
},
{
@@ -75,8 +86,18 @@ const Page = () => {
namedLocationId: "id",
change: "!removeLocation",
},
- fields: [{ type: "textField", name: "input", label: "Country Code" }],
- confirmText: "Enter a two-letter country code, e.g., US.",
+ fields: [
+ {
+ type: "autoComplete",
+ name: "input",
+ label: "Country",
+ options: countryList.map(({ Code, Name }) => ({
+ value: Code,
+ label: `${Name} (${Code})`,
+ })),
+ },
+ ],
+ confirmText: "Select a country to remove from this named location.",
condition: (row) => row["@odata.type"] == "#microsoft.graph.countryNamedLocation",
},
{
diff --git a/src/pages/tenant/gdap-management/invites/add.js b/src/pages/tenant/gdap-management/invites/add.js
index 22a45e334d1d..ce6fe95f1226 100644
--- a/src/pages/tenant/gdap-management/invites/add.js
+++ b/src/pages/tenant/gdap-management/invites/add.js
@@ -118,7 +118,7 @@ const Page = () => {
The onboarding process will also run on a nightly schedule. For automated
onboardings, please check out{" "}
- Partner Webhooks
+ Automated Onboarding
{" "}
in Application Settings.
diff --git a/src/pages/tenant/manage/edit.js b/src/pages/tenant/manage/edit.js
index 8ab89052ca89..d5ddc69085c2 100644
--- a/src/pages/tenant/manage/edit.js
+++ b/src/pages/tenant/manage/edit.js
@@ -174,7 +174,6 @@ const Page = () => {
{
+ const country = countryList.find((c) => c.Code === countryCode);
+ return country ? country.Name : countryCode;
+};
export const getCippFormatting = (data, cellName, type, canReceive, flatten = true) => {
const isText = type === "text";
@@ -415,6 +423,19 @@ export const getCippFormatting = (data, cellName, type, canReceive, flatten = tr
return isText ? data : ;
}
+ if (cellName === "countriesAndRegions") {
+ if (Array.isArray(data)) {
+ const countryNames = data
+ .filter((item) => item !== null && item !== undefined)
+ .map((countryCode) => getCountryNameFromCode(countryCode));
+
+ return isText ? countryNames.join(", ") : renderChipList(countryNames);
+ } else {
+ const countryName = getCountryNameFromCode(data);
+ return isText ? countryName : ;
+ }
+ }
+
if (cellName === "excludedTenants") {
// Handle null or undefined data
if (data === null || data === undefined) {
@@ -467,13 +488,56 @@ export const getCippFormatting = (data, cellName, type, canReceive, flatten = tr
}
if (cellName === "state") {
- data =
- data === "enabled"
+ if (typeof data !== "string") {
+ return isText ? data : ;
+ }
+
+ const normalized = data.trim().toLowerCase();
+ const label =
+ normalized === "enabled"
? "Enabled"
- : data === "enabledForReportingButNotEnforced"
+ : normalized === "disabled"
+ ? "Disabled"
+ : normalized === "enabledforreportingbutnotenforced" ||
+ normalized === "report-only" ||
+ normalized === "reportonly"
? "Report Only"
- : data;
- return isText ? data : ;
+ : data.charAt(0).toUpperCase() + data.slice(1);
+
+ if (isText) {
+ return label;
+ }
+
+ const chipProps = {
+ size: "small",
+ label,
+ variant: "filled",
+ color: "info",
+ };
+
+ if (normalized === "enabled") {
+ chipProps.color = "info";
+ } else if (normalized === "disabled") {
+ chipProps.color = "default";
+ chipProps.sx = (theme) => ({
+ bgcolor:
+ theme.palette.mode === "dark"
+ ? alpha(theme.palette.common.white, 0.12)
+ : alpha(theme.palette.text.primary, 0.08),
+ color: theme.palette.text.primary,
+ borderColor: "transparent",
+ });
+ } else if (
+ normalized === "enabledforreportingbutnotenforced" ||
+ normalized === "report-only" ||
+ normalized === "reportonly"
+ ) {
+ chipProps.color = "warning";
+ } else {
+ chipProps.variant = "outlined";
+ }
+
+ return ;
}
if (cellName === "Parameters.ScheduledBackupValues") {
diff --git a/src/utils/get-cipp-tenant-group-options.js b/src/utils/get-cipp-tenant-group-options.js
new file mode 100644
index 000000000000..7a27e526c16c
--- /dev/null
+++ b/src/utils/get-cipp-tenant-group-options.js
@@ -0,0 +1,183 @@
+import M365LicensesDefault from "../data/M365Licenses.json";
+import M365LicensesAdditional from "../data/M365Licenses-additional.json";
+
+/**
+ * Get all available licenses for tenant group dynamic rules
+ * @returns {Array} Array of license options with label and value (SKU)
+ */
+export const getTenantGroupLicenseOptions = () => {
+ // Combine both license files
+ const allLicenses = [...M365LicensesDefault, ...M365LicensesAdditional];
+
+ // Create unique licenses map using String_Id as key for better deduplication
+ const uniqueLicensesMap = new Map();
+
+ allLicenses.forEach((license) => {
+ if (license.String_Id && license.Product_Display_Name && license.GUID) {
+ // Use String_Id as the unique key since that's what we send to backend
+ const key = license.String_Id;
+ if (!uniqueLicensesMap.has(key)) {
+ uniqueLicensesMap.set(key, {
+ label: license.Product_Display_Name,
+ value: license.String_Id,
+ guid: license.GUID,
+ });
+ }
+ }
+ });
+
+ // Convert to array and filter out incomplete entries
+ const licenseOptions = Array.from(uniqueLicensesMap.values()).filter(
+ (license) => license.label && license.value
+ );
+
+ // Additional deduplication by label to handle cases where different String_Ids have same display name
+ const uniqueByLabelMap = new Map();
+ licenseOptions.forEach((license) => {
+ if (!uniqueByLabelMap.has(license.label)) {
+ uniqueByLabelMap.set(license.label, license);
+ }
+ });
+
+ return Array.from(uniqueByLabelMap.values()).sort((a, b) => a.label.localeCompare(b.label));
+};
+
+/**
+ * Get all available service plans for tenant group dynamic rules
+ * @returns {Array} Array of unique service plan options with label and value
+ */
+export const getTenantGroupServicePlanOptions = () => {
+ // Combine both license files
+ const allLicenses = [...M365LicensesDefault, ...M365LicensesAdditional];
+
+ // Create unique service plans map using Service_Plan_Name as key for better deduplication
+ const uniqueServicePlansMap = new Map();
+
+ allLicenses.forEach((license) => {
+ if (
+ license.Service_Plan_Name &&
+ license.Service_Plans_Included_Friendly_Names &&
+ license.Service_Plan_Id
+ ) {
+ // Use Service_Plan_Name as the unique key since that's what we send to backend
+ const key = license.Service_Plan_Name;
+ if (!uniqueServicePlansMap.has(key)) {
+ uniqueServicePlansMap.set(key, {
+ label: license.Service_Plans_Included_Friendly_Names,
+ value: license.Service_Plan_Name,
+ id: license.Service_Plan_Id,
+ });
+ }
+ }
+ });
+
+ // Convert to array and sort by display name, then deduplicate by label as well
+ const serviceOptions = Array.from(uniqueServicePlansMap.values()).filter(
+ (plan) => plan.label && plan.value
+ ); // Filter out any incomplete entries
+
+ // Additional deduplication by label to handle cases where different service plan names have same friendly name
+ const uniqueByLabelMap = new Map();
+ serviceOptions.forEach((plan) => {
+ if (!uniqueByLabelMap.has(plan.label)) {
+ uniqueByLabelMap.set(plan.label, plan);
+ }
+ });
+
+ return Array.from(uniqueByLabelMap.values()).sort((a, b) => a.label.localeCompare(b.label));
+};
+
+/**
+ * Get delegated access status options for tenant group dynamic rules
+ * @returns {Array} Array of delegated access status options
+ */
+export const getTenantGroupDelegatedAccessOptions = () => {
+ return [
+ {
+ label: "Granular Delegated Admin Privileges",
+ value: "granularDelegatedAdminPrivileges",
+ },
+ {
+ label: "Direct Tenant",
+ value: "directTenant",
+ },
+ ];
+};
+
+/**
+ * Get all property options for dynamic tenant group rules
+ * @returns {Array} Array of property options for the rule builder
+ */
+export const getTenantGroupPropertyOptions = () => {
+ return [
+ {
+ label: "Available License",
+ value: "availableLicense",
+ type: "license",
+ },
+ {
+ label: "Available Service Plan",
+ value: "availableServicePlan",
+ type: "servicePlan",
+ },
+ {
+ label: "Delegated Access Status",
+ value: "delegatedAccessStatus",
+ type: "delegatedAccess",
+ },
+ ];
+};
+
+/**
+ * Get operator options for dynamic tenant group rules
+ * @returns {Array} Array of operator options
+ */
+export const getTenantGroupOperatorOptions = (propertyType) => {
+ const baseOperators = [
+ {
+ label: "Equals",
+ value: "eq",
+ },
+ {
+ label: "Not Equals",
+ value: "ne",
+ }
+ ];
+
+ const arrayOperators = [
+ {
+ label: "In",
+ value: "in",
+ },
+ {
+ label: "Not In",
+ value: "notIn",
+ }
+ ];
+
+ // Delegated Access Status only supports equals/not equals
+ if (propertyType === "delegatedAccess") {
+ return baseOperators;
+ }
+
+ // License and Service Plan support all operators
+ return [...baseOperators, ...arrayOperators];
+};
+
+/**
+ * Get value options based on the selected property type
+ * @param {string} propertyType - The type of property (license, servicePlan, delegatedAccess)
+ * @returns {Array} Array of value options for the selected property type
+ */
+export const getTenantGroupValueOptions = (propertyType) => {
+ switch (propertyType) {
+ case "license":
+ return getTenantGroupLicenseOptions();
+ case "servicePlan":
+ return getTenantGroupServicePlanOptions();
+ case "delegatedAccess":
+ return getTenantGroupDelegatedAccessOptions();
+ default:
+ return [];
+ }
+};
diff --git a/yarn.lock b/yarn.lock
index 860b5de8865c..bdbd2027fafb 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2225,6 +2225,13 @@
"@types/linkify-it" "^5"
"@types/mdurl" "^2"
+"@types/mdast@^3.0.0":
+ version "3.0.15"
+ resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.15.tgz#49c524a263f30ffa28b71ae282f813ed000ab9f5"
+ integrity sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==
+ dependencies:
+ "@types/unist" "^2"
+
"@types/mdast@^4.0.0":
version "4.0.4"
resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-4.0.4.tgz#7ccf72edd2f1aa7dd3437e180c64373585804dd6"
@@ -3299,6 +3306,11 @@ dfa@^1.2.0:
resolved "https://registry.yarnpkg.com/dfa/-/dfa-1.2.0.tgz#96ac3204e2d29c49ea5b57af8d92c2ae12790657"
integrity sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==
+diff@^5.0.0:
+ version "5.2.0"
+ resolved "https://registry.yarnpkg.com/diff/-/diff-5.2.0.tgz#26ded047cd1179b78b9537d5ef725503ce1ae531"
+ integrity sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==
+
doctrine@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d"
@@ -3446,6 +3458,11 @@ entities@^4.2.0, entities@^4.4.0:
resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48"
integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==
+entities@^6.0.0:
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/entities/-/entities-6.0.1.tgz#c28c34a43379ca7f61d074130b2f5f7020a30694"
+ integrity sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==
+
error-ex@^1.3.1:
version "1.3.2"
resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
@@ -3588,6 +3605,11 @@ escape-string-regexp@^4.0.0:
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==
+escape-string-regexp@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz#4683126b500b61762f2dbebace1806e8be31b1c8"
+ integrity sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==
+
eslint-config-next@15.5.2:
version "15.5.2"
resolved "https://registry.yarnpkg.com/eslint-config-next/-/eslint-config-next-15.5.2.tgz#9629ed1deaa131e8e80cbae20acf631c8595ca3e"
@@ -4183,11 +4205,51 @@ hasown@^2.0.2:
dependencies:
function-bind "^1.1.2"
+hast-util-from-parse5@^8.0.0:
+ version "8.0.3"
+ resolved "https://registry.yarnpkg.com/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz#830a35022fff28c3fea3697a98c2f4cc6b835a2e"
+ integrity sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==
+ dependencies:
+ "@types/hast" "^3.0.0"
+ "@types/unist" "^3.0.0"
+ devlop "^1.0.0"
+ hastscript "^9.0.0"
+ property-information "^7.0.0"
+ vfile "^6.0.0"
+ vfile-location "^5.0.0"
+ web-namespaces "^2.0.0"
+
hast-util-parse-selector@^2.0.0:
version "2.2.5"
resolved "https://registry.yarnpkg.com/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz#d57c23f4da16ae3c63b3b6ca4616683313499c3a"
integrity sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==
+hast-util-parse-selector@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz#352879fa86e25616036037dd8931fb5f34cb4a27"
+ integrity sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==
+ dependencies:
+ "@types/hast" "^3.0.0"
+
+hast-util-raw@^9.0.0:
+ version "9.1.0"
+ resolved "https://registry.yarnpkg.com/hast-util-raw/-/hast-util-raw-9.1.0.tgz#79b66b26f6f68fb50dfb4716b2cdca90d92adf2e"
+ integrity sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==
+ dependencies:
+ "@types/hast" "^3.0.0"
+ "@types/unist" "^3.0.0"
+ "@ungap/structured-clone" "^1.0.0"
+ hast-util-from-parse5 "^8.0.0"
+ hast-util-to-parse5 "^8.0.0"
+ html-void-elements "^3.0.0"
+ mdast-util-to-hast "^13.0.0"
+ parse5 "^7.0.0"
+ unist-util-position "^5.0.0"
+ unist-util-visit "^5.0.0"
+ vfile "^6.0.0"
+ web-namespaces "^2.0.0"
+ zwitch "^2.0.0"
+
hast-util-to-jsx-runtime@^2.0.0:
version "2.3.6"
resolved "https://registry.yarnpkg.com/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz#ff31897aae59f62232e21594eac7ef6b63333e98"
@@ -4209,6 +4271,19 @@ hast-util-to-jsx-runtime@^2.0.0:
unist-util-position "^5.0.0"
vfile-message "^4.0.0"
+hast-util-to-parse5@^8.0.0:
+ version "8.0.0"
+ resolved "https://registry.yarnpkg.com/hast-util-to-parse5/-/hast-util-to-parse5-8.0.0.tgz#477cd42d278d4f036bc2ea58586130f6f39ee6ed"
+ integrity sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==
+ dependencies:
+ "@types/hast" "^3.0.0"
+ comma-separated-tokens "^2.0.0"
+ devlop "^1.0.0"
+ property-information "^6.0.0"
+ space-separated-tokens "^2.0.0"
+ web-namespaces "^2.0.0"
+ zwitch "^2.0.0"
+
hast-util-whitespace@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz#7778ed9d3c92dd9e8c5c8f648a49c21fc51cb621"
@@ -4227,6 +4302,17 @@ hastscript@^6.0.0:
property-information "^5.0.0"
space-separated-tokens "^1.0.0"
+hastscript@^9.0.0:
+ version "9.0.1"
+ resolved "https://registry.yarnpkg.com/hastscript/-/hastscript-9.0.1.tgz#dbc84bef6051d40084342c229c451cd9dc567dff"
+ integrity sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==
+ dependencies:
+ "@types/hast" "^3.0.0"
+ comma-separated-tokens "^2.0.0"
+ hast-util-parse-selector "^4.0.0"
+ property-information "^7.0.0"
+ space-separated-tokens "^2.0.0"
+
highlight-words@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/highlight-words/-/highlight-words-2.0.0.tgz#06853d68f1f7c8e59d6ef2dd072fe2f64fc93936"
@@ -4284,6 +4370,11 @@ html-url-attributes@^3.0.0:
resolved "https://registry.yarnpkg.com/html-url-attributes/-/html-url-attributes-3.0.1.tgz#83b052cd5e437071b756cd74ae70f708870c2d87"
integrity sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==
+html-void-elements@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/html-void-elements/-/html-void-elements-3.0.0.tgz#fc9dbd84af9e747249034d4d62602def6517f1d7"
+ integrity sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==
+
html2canvas@^1.0.0-rc.5:
version "1.4.1"
resolved "https://registry.yarnpkg.com/html2canvas/-/html2canvas-1.4.1.tgz#7cef1888311b5011d507794a066041b14669a543"
@@ -4447,6 +4538,11 @@ is-boolean-object@^1.2.1:
call-bound "^1.0.3"
has-tostringtag "^1.0.2"
+is-buffer@^2.0.0:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191"
+ integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==
+
is-bun-module@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/is-bun-module/-/is-bun-module-2.0.0.tgz#4d7859a87c0fcac950c95e666730e745eae8bddd"
@@ -4790,6 +4886,11 @@ kind-of@^6.0.0, kind-of@^6.0.2:
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd"
integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
+kleur@^4.0.3:
+ version "4.1.5"
+ resolved "https://registry.yarnpkg.com/kleur/-/kleur-4.1.5.tgz#95106101795f7050c6c650f350c683febddb1780"
+ integrity sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==
+
language-subtag-registry@^0.3.20:
version "0.3.23"
resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz#23529e04d9e3b74679d70142df3fd2eb6ec572e7"
@@ -4928,6 +5029,11 @@ markdown-it@^14.0.0:
punycode.js "^2.3.1"
uc.micro "^2.1.0"
+markdown-table@^3.0.0:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/markdown-table/-/markdown-table-3.0.4.tgz#fe44d6d410ff9d6f2ea1797a3f60aa4d2b631c2a"
+ integrity sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==
+
material-react-table@^3.0.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/material-react-table/-/material-react-table-3.2.1.tgz#56f595755cab3b669b399999fed9eb305fbb6dd7"
@@ -4943,6 +5049,34 @@ math-intrinsics@^1.1.0:
resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9"
integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==
+mdast-util-find-and-replace@^2.0.0:
+ version "2.2.2"
+ resolved "https://registry.yarnpkg.com/mdast-util-find-and-replace/-/mdast-util-find-and-replace-2.2.2.tgz#cc2b774f7f3630da4bd592f61966fecade8b99b1"
+ integrity sha512-MTtdFRz/eMDHXzeK6W3dO7mXUlF82Gom4y0oOgvHhh/HXZAGvIQDUvQ0SuUx+j2tv44b8xTHOm8K/9OoRFnXKw==
+ dependencies:
+ "@types/mdast" "^3.0.0"
+ escape-string-regexp "^5.0.0"
+ unist-util-is "^5.0.0"
+ unist-util-visit-parents "^5.0.0"
+
+mdast-util-from-markdown@^1.0.0:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.1.tgz#9421a5a247f10d31d2faed2a30df5ec89ceafcf0"
+ integrity sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww==
+ dependencies:
+ "@types/mdast" "^3.0.0"
+ "@types/unist" "^2.0.0"
+ decode-named-character-reference "^1.0.0"
+ mdast-util-to-string "^3.1.0"
+ micromark "^3.0.0"
+ micromark-util-decode-numeric-character-reference "^1.0.0"
+ micromark-util-decode-string "^1.0.0"
+ micromark-util-normalize-identifier "^1.0.0"
+ micromark-util-symbol "^1.0.0"
+ micromark-util-types "^1.0.0"
+ unist-util-stringify-position "^3.0.0"
+ uvu "^0.5.0"
+
mdast-util-from-markdown@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz#4850390ca7cf17413a9b9a0fbefcd1bc0eb4160a"
@@ -4961,6 +5095,64 @@ mdast-util-from-markdown@^2.0.0:
micromark-util-types "^2.0.0"
unist-util-stringify-position "^4.0.0"
+mdast-util-gfm-autolink-literal@^1.0.0:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-1.0.3.tgz#67a13abe813d7eba350453a5333ae1bc0ec05c06"
+ integrity sha512-My8KJ57FYEy2W2LyNom4n3E7hKTuQk/0SES0u16tjA9Z3oFkF4RrC/hPAPgjlSpezsOvI8ObcXcElo92wn5IGA==
+ dependencies:
+ "@types/mdast" "^3.0.0"
+ ccount "^2.0.0"
+ mdast-util-find-and-replace "^2.0.0"
+ micromark-util-character "^1.0.0"
+
+mdast-util-gfm-footnote@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-1.0.2.tgz#ce5e49b639c44de68d5bf5399877a14d5020424e"
+ integrity sha512-56D19KOGbE00uKVj3sgIykpwKL179QsVFwx/DCW0u/0+URsryacI4MAdNJl0dh+u2PSsD9FtxPFbHCzJ78qJFQ==
+ dependencies:
+ "@types/mdast" "^3.0.0"
+ mdast-util-to-markdown "^1.3.0"
+ micromark-util-normalize-identifier "^1.0.0"
+
+mdast-util-gfm-strikethrough@^1.0.0:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-1.0.3.tgz#5470eb105b483f7746b8805b9b989342085795b7"
+ integrity sha512-DAPhYzTYrRcXdMjUtUjKvW9z/FNAMTdU0ORyMcbmkwYNbKocDpdk+PX1L1dQgOID/+vVs1uBQ7ElrBQfZ0cuiQ==
+ dependencies:
+ "@types/mdast" "^3.0.0"
+ mdast-util-to-markdown "^1.3.0"
+
+mdast-util-gfm-table@^1.0.0:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/mdast-util-gfm-table/-/mdast-util-gfm-table-1.0.7.tgz#3552153a146379f0f9c4c1101b071d70bbed1a46"
+ integrity sha512-jjcpmNnQvrmN5Vx7y7lEc2iIOEytYv7rTvu+MeyAsSHTASGCCRA79Igg2uKssgOs1i1po8s3plW0sTu1wkkLGg==
+ dependencies:
+ "@types/mdast" "^3.0.0"
+ markdown-table "^3.0.0"
+ mdast-util-from-markdown "^1.0.0"
+ mdast-util-to-markdown "^1.3.0"
+
+mdast-util-gfm-task-list-item@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-1.0.2.tgz#b280fcf3b7be6fd0cc012bbe67a59831eb34097b"
+ integrity sha512-PFTA1gzfp1B1UaiJVyhJZA1rm0+Tzn690frc/L8vNX1Jop4STZgOE6bxUhnzdVSB+vm2GU1tIsuQcA9bxTQpMQ==
+ dependencies:
+ "@types/mdast" "^3.0.0"
+ mdast-util-to-markdown "^1.3.0"
+
+mdast-util-gfm@^2.0.0:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/mdast-util-gfm/-/mdast-util-gfm-2.0.2.tgz#e92f4d8717d74bdba6de57ed21cc8b9552e2d0b6"
+ integrity sha512-qvZ608nBppZ4icQlhQQIAdc6S3Ffj9RGmzwUKUWuEICFnd1LVkN3EktF7ZHAgfcEdvZB5owU9tQgt99e2TlLjg==
+ dependencies:
+ mdast-util-from-markdown "^1.0.0"
+ mdast-util-gfm-autolink-literal "^1.0.0"
+ mdast-util-gfm-footnote "^1.0.0"
+ mdast-util-gfm-strikethrough "^1.0.0"
+ mdast-util-gfm-table "^1.0.0"
+ mdast-util-gfm-task-list-item "^1.0.0"
+ mdast-util-to-markdown "^1.0.0"
+
mdast-util-mdx-expression@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz#43f0abac9adc756e2086f63822a38c8d3c3a5096"
@@ -5003,6 +5195,14 @@ mdast-util-mdxjs-esm@^2.0.0:
mdast-util-from-markdown "^2.0.0"
mdast-util-to-markdown "^2.0.0"
+mdast-util-phrasing@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/mdast-util-phrasing/-/mdast-util-phrasing-3.0.1.tgz#c7c21d0d435d7fb90956038f02e8702781f95463"
+ integrity sha512-WmI1gTXUBJo4/ZmSk79Wcb2HcjPJBzM1nlI/OUWA8yk2X9ik3ffNbBGsU+09BFmXaL1IBb9fiuvq6/KMiNycSg==
+ dependencies:
+ "@types/mdast" "^3.0.0"
+ unist-util-is "^5.0.0"
+
mdast-util-phrasing@^4.0.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz#7cc0a8dec30eaf04b7b1a9661a92adb3382aa6e3"
@@ -5026,6 +5226,20 @@ mdast-util-to-hast@^13.0.0:
unist-util-visit "^5.0.0"
vfile "^6.0.0"
+mdast-util-to-markdown@^1.0.0, mdast-util-to-markdown@^1.3.0:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/mdast-util-to-markdown/-/mdast-util-to-markdown-1.5.0.tgz#c13343cb3fc98621911d33b5cd42e7d0731171c6"
+ integrity sha512-bbv7TPv/WC49thZPg3jXuqzuvI45IL2EVAr/KxF0BSdHsU0ceFHOmwQn6evxAh1GaoK/6GQ1wp4R4oW2+LFL/A==
+ dependencies:
+ "@types/mdast" "^3.0.0"
+ "@types/unist" "^2.0.0"
+ longest-streak "^3.0.0"
+ mdast-util-phrasing "^3.0.0"
+ mdast-util-to-string "^3.0.0"
+ micromark-util-decode-string "^1.0.0"
+ unist-util-visit "^4.0.0"
+ zwitch "^2.0.0"
+
mdast-util-to-markdown@^2.0.0:
version "2.1.2"
resolved "https://registry.yarnpkg.com/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz#f910ffe60897f04bb4b7e7ee434486f76288361b"
@@ -5041,6 +5255,13 @@ mdast-util-to-markdown@^2.0.0:
unist-util-visit "^5.0.0"
zwitch "^2.0.0"
+mdast-util-to-string@^3.0.0, mdast-util-to-string@^3.1.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz#66f7bb6324756741c5f47a53557f0cbf16b6f789"
+ integrity sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==
+ dependencies:
+ "@types/mdast" "^3.0.0"
+
mdast-util-to-string@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz#7a5121475556a04e7eddeb67b264aae79d312814"
@@ -5083,6 +5304,28 @@ merge2@^1.3.0:
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
+micromark-core-commonmark@^1.0.0, micromark-core-commonmark@^1.0.1:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/micromark-core-commonmark/-/micromark-core-commonmark-1.1.0.tgz#1386628df59946b2d39fb2edfd10f3e8e0a75bb8"
+ integrity sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw==
+ dependencies:
+ decode-named-character-reference "^1.0.0"
+ micromark-factory-destination "^1.0.0"
+ micromark-factory-label "^1.0.0"
+ micromark-factory-space "^1.0.0"
+ micromark-factory-title "^1.0.0"
+ micromark-factory-whitespace "^1.0.0"
+ micromark-util-character "^1.0.0"
+ micromark-util-chunked "^1.0.0"
+ micromark-util-classify-character "^1.0.0"
+ micromark-util-html-tag-name "^1.0.0"
+ micromark-util-normalize-identifier "^1.0.0"
+ micromark-util-resolve-all "^1.0.0"
+ micromark-util-subtokenize "^1.0.0"
+ micromark-util-symbol "^1.0.0"
+ micromark-util-types "^1.0.1"
+ uvu "^0.5.0"
+
micromark-core-commonmark@^2.0.0:
version "2.0.3"
resolved "https://registry.yarnpkg.com/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz#c691630e485021a68cf28dbc2b2ca27ebf678cd4"
@@ -5105,6 +5348,94 @@ micromark-core-commonmark@^2.0.0:
micromark-util-symbol "^2.0.0"
micromark-util-types "^2.0.0"
+micromark-extension-gfm-autolink-literal@^1.0.0:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-1.0.5.tgz#5853f0e579bbd8ef9e39a7c0f0f27c5a063a66e7"
+ integrity sha512-z3wJSLrDf8kRDOh2qBtoTRD53vJ+CWIyo7uyZuxf/JAbNJjiHsOpG1y5wxk8drtv3ETAHutCu6N3thkOOgueWg==
+ dependencies:
+ micromark-util-character "^1.0.0"
+ micromark-util-sanitize-uri "^1.0.0"
+ micromark-util-symbol "^1.0.0"
+ micromark-util-types "^1.0.0"
+
+micromark-extension-gfm-footnote@^1.0.0:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-1.1.2.tgz#05e13034d68f95ca53c99679040bc88a6f92fe2e"
+ integrity sha512-Yxn7z7SxgyGWRNa4wzf8AhYYWNrwl5q1Z8ii+CSTTIqVkmGZF1CElX2JI8g5yGoM3GAman9/PVCUFUSJ0kB/8Q==
+ dependencies:
+ micromark-core-commonmark "^1.0.0"
+ micromark-factory-space "^1.0.0"
+ micromark-util-character "^1.0.0"
+ micromark-util-normalize-identifier "^1.0.0"
+ micromark-util-sanitize-uri "^1.0.0"
+ micromark-util-symbol "^1.0.0"
+ micromark-util-types "^1.0.0"
+ uvu "^0.5.0"
+
+micromark-extension-gfm-strikethrough@^1.0.0:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-1.0.7.tgz#c8212c9a616fa3bf47cb5c711da77f4fdc2f80af"
+ integrity sha512-sX0FawVE1o3abGk3vRjOH50L5TTLr3b5XMqnP9YDRb34M0v5OoZhG+OHFz1OffZ9dlwgpTBKaT4XW/AsUVnSDw==
+ dependencies:
+ micromark-util-chunked "^1.0.0"
+ micromark-util-classify-character "^1.0.0"
+ micromark-util-resolve-all "^1.0.0"
+ micromark-util-symbol "^1.0.0"
+ micromark-util-types "^1.0.0"
+ uvu "^0.5.0"
+
+micromark-extension-gfm-table@^1.0.0:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/micromark-extension-gfm-table/-/micromark-extension-gfm-table-1.0.7.tgz#dcb46074b0c6254c3fc9cc1f6f5002c162968008"
+ integrity sha512-3ZORTHtcSnMQEKtAOsBQ9/oHp9096pI/UvdPtN7ehKvrmZZ2+bbWhi0ln+I9drmwXMt5boocn6OlwQzNXeVeqw==
+ dependencies:
+ micromark-factory-space "^1.0.0"
+ micromark-util-character "^1.0.0"
+ micromark-util-symbol "^1.0.0"
+ micromark-util-types "^1.0.0"
+ uvu "^0.5.0"
+
+micromark-extension-gfm-tagfilter@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-1.0.2.tgz#aa7c4dd92dabbcb80f313ebaaa8eb3dac05f13a7"
+ integrity sha512-5XWB9GbAUSHTn8VPU8/1DBXMuKYT5uOgEjJb8gN3mW0PNW5OPHpSdojoqf+iq1xo7vWzw/P8bAHY0n6ijpXF7g==
+ dependencies:
+ micromark-util-types "^1.0.0"
+
+micromark-extension-gfm-task-list-item@^1.0.0:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-1.0.5.tgz#b52ce498dc4c69b6a9975abafc18f275b9dde9f4"
+ integrity sha512-RMFXl2uQ0pNQy6Lun2YBYT9g9INXtWJULgbt01D/x8/6yJ2qpKyzdZD3pi6UIkzF++Da49xAelVKUeUMqd5eIQ==
+ dependencies:
+ micromark-factory-space "^1.0.0"
+ micromark-util-character "^1.0.0"
+ micromark-util-symbol "^1.0.0"
+ micromark-util-types "^1.0.0"
+ uvu "^0.5.0"
+
+micromark-extension-gfm@^2.0.0:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/micromark-extension-gfm/-/micromark-extension-gfm-2.0.3.tgz#e517e8579949a5024a493e49204e884aa74f5acf"
+ integrity sha512-vb9OoHqrhCmbRidQv/2+Bc6pkP0FrtlhurxZofvOEy5o8RtuuvTq+RQ1Vw5ZDNrVraQZu3HixESqbG+0iKk/MQ==
+ dependencies:
+ micromark-extension-gfm-autolink-literal "^1.0.0"
+ micromark-extension-gfm-footnote "^1.0.0"
+ micromark-extension-gfm-strikethrough "^1.0.0"
+ micromark-extension-gfm-table "^1.0.0"
+ micromark-extension-gfm-tagfilter "^1.0.0"
+ micromark-extension-gfm-task-list-item "^1.0.0"
+ micromark-util-combine-extensions "^1.0.0"
+ micromark-util-types "^1.0.0"
+
+micromark-factory-destination@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/micromark-factory-destination/-/micromark-factory-destination-1.1.0.tgz#eb815957d83e6d44479b3df640f010edad667b9f"
+ integrity sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg==
+ dependencies:
+ micromark-util-character "^1.0.0"
+ micromark-util-symbol "^1.0.0"
+ micromark-util-types "^1.0.0"
+
micromark-factory-destination@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz#8fef8e0f7081f0474fbdd92deb50c990a0264639"
@@ -5114,6 +5445,16 @@ micromark-factory-destination@^2.0.0:
micromark-util-symbol "^2.0.0"
micromark-util-types "^2.0.0"
+micromark-factory-label@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/micromark-factory-label/-/micromark-factory-label-1.1.0.tgz#cc95d5478269085cfa2a7282b3de26eb2e2dec68"
+ integrity sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w==
+ dependencies:
+ micromark-util-character "^1.0.0"
+ micromark-util-symbol "^1.0.0"
+ micromark-util-types "^1.0.0"
+ uvu "^0.5.0"
+
micromark-factory-label@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz#5267efa97f1e5254efc7f20b459a38cb21058ba1"
@@ -5124,6 +5465,14 @@ micromark-factory-label@^2.0.0:
micromark-util-symbol "^2.0.0"
micromark-util-types "^2.0.0"
+micromark-factory-space@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/micromark-factory-space/-/micromark-factory-space-1.1.0.tgz#c8f40b0640a0150751d3345ed885a080b0d15faf"
+ integrity sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==
+ dependencies:
+ micromark-util-character "^1.0.0"
+ micromark-util-types "^1.0.0"
+
micromark-factory-space@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz#36d0212e962b2b3121f8525fc7a3c7c029f334fc"
@@ -5132,6 +5481,16 @@ micromark-factory-space@^2.0.0:
micromark-util-character "^2.0.0"
micromark-util-types "^2.0.0"
+micromark-factory-title@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/micromark-factory-title/-/micromark-factory-title-1.1.0.tgz#dd0fe951d7a0ac71bdc5ee13e5d1465ad7f50ea1"
+ integrity sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ==
+ dependencies:
+ micromark-factory-space "^1.0.0"
+ micromark-util-character "^1.0.0"
+ micromark-util-symbol "^1.0.0"
+ micromark-util-types "^1.0.0"
+
micromark-factory-title@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz#237e4aa5d58a95863f01032d9ee9b090f1de6e94"
@@ -5142,6 +5501,16 @@ micromark-factory-title@^2.0.0:
micromark-util-symbol "^2.0.0"
micromark-util-types "^2.0.0"
+micromark-factory-whitespace@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/micromark-factory-whitespace/-/micromark-factory-whitespace-1.1.0.tgz#798fb7489f4c8abafa7ca77eed6b5745853c9705"
+ integrity sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ==
+ dependencies:
+ micromark-factory-space "^1.0.0"
+ micromark-util-character "^1.0.0"
+ micromark-util-symbol "^1.0.0"
+ micromark-util-types "^1.0.0"
+
micromark-factory-whitespace@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz#06b26b2983c4d27bfcc657b33e25134d4868b0b1"
@@ -5152,6 +5521,14 @@ micromark-factory-whitespace@^2.0.0:
micromark-util-symbol "^2.0.0"
micromark-util-types "^2.0.0"
+micromark-util-character@^1.0.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/micromark-util-character/-/micromark-util-character-1.2.0.tgz#4fedaa3646db249bc58caeb000eb3549a8ca5dcc"
+ integrity sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==
+ dependencies:
+ micromark-util-symbol "^1.0.0"
+ micromark-util-types "^1.0.0"
+
micromark-util-character@^2.0.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/micromark-util-character/-/micromark-util-character-2.1.1.tgz#2f987831a40d4c510ac261e89852c4e9703ccda6"
@@ -5160,6 +5537,13 @@ micromark-util-character@^2.0.0:
micromark-util-symbol "^2.0.0"
micromark-util-types "^2.0.0"
+micromark-util-chunked@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/micromark-util-chunked/-/micromark-util-chunked-1.1.0.tgz#37a24d33333c8c69a74ba12a14651fd9ea8a368b"
+ integrity sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ==
+ dependencies:
+ micromark-util-symbol "^1.0.0"
+
micromark-util-chunked@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz#47fbcd93471a3fccab86cff03847fc3552db1051"
@@ -5167,6 +5551,15 @@ micromark-util-chunked@^2.0.0:
dependencies:
micromark-util-symbol "^2.0.0"
+micromark-util-classify-character@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/micromark-util-classify-character/-/micromark-util-classify-character-1.1.0.tgz#6a7f8c8838e8a120c8e3c4f2ae97a2bff9190e9d"
+ integrity sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw==
+ dependencies:
+ micromark-util-character "^1.0.0"
+ micromark-util-symbol "^1.0.0"
+ micromark-util-types "^1.0.0"
+
micromark-util-classify-character@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz#d399faf9c45ca14c8b4be98b1ea481bced87b629"
@@ -5176,6 +5569,14 @@ micromark-util-classify-character@^2.0.0:
micromark-util-symbol "^2.0.0"
micromark-util-types "^2.0.0"
+micromark-util-combine-extensions@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.1.0.tgz#192e2b3d6567660a85f735e54d8ea6e3952dbe84"
+ integrity sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA==
+ dependencies:
+ micromark-util-chunked "^1.0.0"
+ micromark-util-types "^1.0.0"
+
micromark-util-combine-extensions@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz#2a0f490ab08bff5cc2fd5eec6dd0ca04f89b30a9"
@@ -5184,6 +5585,13 @@ micromark-util-combine-extensions@^2.0.0:
micromark-util-chunked "^2.0.0"
micromark-util-types "^2.0.0"
+micromark-util-decode-numeric-character-reference@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.1.0.tgz#b1e6e17009b1f20bc652a521309c5f22c85eb1c6"
+ integrity sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw==
+ dependencies:
+ micromark-util-symbol "^1.0.0"
+
micromark-util-decode-numeric-character-reference@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz#fcf15b660979388e6f118cdb6bf7d79d73d26fe5"
@@ -5191,6 +5599,16 @@ micromark-util-decode-numeric-character-reference@^2.0.0:
dependencies:
micromark-util-symbol "^2.0.0"
+micromark-util-decode-string@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/micromark-util-decode-string/-/micromark-util-decode-string-1.1.0.tgz#dc12b078cba7a3ff690d0203f95b5d5537f2809c"
+ integrity sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ==
+ dependencies:
+ decode-named-character-reference "^1.0.0"
+ micromark-util-character "^1.0.0"
+ micromark-util-decode-numeric-character-reference "^1.0.0"
+ micromark-util-symbol "^1.0.0"
+
micromark-util-decode-string@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz#6cb99582e5d271e84efca8e61a807994d7161eb2"
@@ -5201,16 +5619,33 @@ micromark-util-decode-string@^2.0.0:
micromark-util-decode-numeric-character-reference "^2.0.0"
micromark-util-symbol "^2.0.0"
+micromark-util-encode@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/micromark-util-encode/-/micromark-util-encode-1.1.0.tgz#92e4f565fd4ccb19e0dcae1afab9a173bbeb19a5"
+ integrity sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==
+
micromark-util-encode@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz#0d51d1c095551cfaac368326963cf55f15f540b8"
integrity sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==
+micromark-util-html-tag-name@^1.0.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.2.0.tgz#48fd7a25826f29d2f71479d3b4e83e94829b3588"
+ integrity sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q==
+
micromark-util-html-tag-name@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz#e40403096481986b41c106627f98f72d4d10b825"
integrity sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==
+micromark-util-normalize-identifier@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.1.0.tgz#7a73f824eb9f10d442b4d7f120fecb9b38ebf8b7"
+ integrity sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q==
+ dependencies:
+ micromark-util-symbol "^1.0.0"
+
micromark-util-normalize-identifier@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz#c30d77b2e832acf6526f8bf1aa47bc9c9438c16d"
@@ -5218,6 +5653,13 @@ micromark-util-normalize-identifier@^2.0.0:
dependencies:
micromark-util-symbol "^2.0.0"
+micromark-util-resolve-all@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/micromark-util-resolve-all/-/micromark-util-resolve-all-1.1.0.tgz#4652a591ee8c8fa06714c9b54cd6c8e693671188"
+ integrity sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA==
+ dependencies:
+ micromark-util-types "^1.0.0"
+
micromark-util-resolve-all@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz#e1a2d62cdd237230a2ae11839027b19381e31e8b"
@@ -5225,6 +5667,15 @@ micromark-util-resolve-all@^2.0.0:
dependencies:
micromark-util-types "^2.0.0"
+micromark-util-sanitize-uri@^1.0.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.2.0.tgz#613f738e4400c6eedbc53590c67b197e30d7f90d"
+ integrity sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==
+ dependencies:
+ micromark-util-character "^1.0.0"
+ micromark-util-encode "^1.0.0"
+ micromark-util-symbol "^1.0.0"
+
micromark-util-sanitize-uri@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz#ab89789b818a58752b73d6b55238621b7faa8fd7"
@@ -5234,6 +5685,16 @@ micromark-util-sanitize-uri@^2.0.0:
micromark-util-encode "^2.0.0"
micromark-util-symbol "^2.0.0"
+micromark-util-subtokenize@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/micromark-util-subtokenize/-/micromark-util-subtokenize-1.1.0.tgz#941c74f93a93eaf687b9054aeb94642b0e92edb1"
+ integrity sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A==
+ dependencies:
+ micromark-util-chunked "^1.0.0"
+ micromark-util-symbol "^1.0.0"
+ micromark-util-types "^1.0.0"
+ uvu "^0.5.0"
+
micromark-util-subtokenize@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz#d8ade5ba0f3197a1cf6a2999fbbfe6357a1a19ee"
@@ -5244,16 +5705,49 @@ micromark-util-subtokenize@^2.0.0:
micromark-util-symbol "^2.0.0"
micromark-util-types "^2.0.0"
+micromark-util-symbol@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz#813cd17837bdb912d069a12ebe3a44b6f7063142"
+ integrity sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==
+
micromark-util-symbol@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz#e5da494e8eb2b071a0d08fb34f6cefec6c0a19b8"
integrity sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==
+micromark-util-types@^1.0.0, micromark-util-types@^1.0.1:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/micromark-util-types/-/micromark-util-types-1.1.0.tgz#e6676a8cae0bb86a2171c498167971886cb7e283"
+ integrity sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==
+
micromark-util-types@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/micromark-util-types/-/micromark-util-types-2.0.2.tgz#f00225f5f5a0ebc3254f96c36b6605c4b393908e"
integrity sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==
+micromark@^3.0.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/micromark/-/micromark-3.2.0.tgz#1af9fef3f995ea1ea4ac9c7e2f19c48fd5c006e9"
+ integrity sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==
+ dependencies:
+ "@types/debug" "^4.0.0"
+ debug "^4.0.0"
+ decode-named-character-reference "^1.0.0"
+ micromark-core-commonmark "^1.0.1"
+ micromark-factory-space "^1.0.0"
+ micromark-util-character "^1.0.0"
+ micromark-util-chunked "^1.0.0"
+ micromark-util-combine-extensions "^1.0.0"
+ micromark-util-decode-numeric-character-reference "^1.0.0"
+ micromark-util-encode "^1.0.0"
+ micromark-util-normalize-identifier "^1.0.0"
+ micromark-util-resolve-all "^1.0.0"
+ micromark-util-sanitize-uri "^1.0.0"
+ micromark-util-subtokenize "^1.0.0"
+ micromark-util-symbol "^1.0.0"
+ micromark-util-types "^1.0.1"
+ uvu "^0.5.0"
+
micromark@^4.0.0:
version "4.0.2"
resolved "https://registry.yarnpkg.com/micromark/-/micromark-4.0.2.tgz#91395a3e1884a198e62116e33c9c568e39936fdb"
@@ -5323,6 +5817,11 @@ monaco-editor@^0.53.0:
dependencies:
"@types/trusted-types" "^1.0.6"
+mri@^1.1.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b"
+ integrity sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==
+
ms@^2.1.1, ms@^2.1.3:
version "2.1.3"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
@@ -5611,6 +6110,13 @@ parse-svg-path@^0.1.2:
resolved "https://registry.yarnpkg.com/parse-svg-path/-/parse-svg-path-0.1.2.tgz#7a7ec0d1eb06fa5325c7d3e009b859a09b5d49eb"
integrity sha512-JyPSBnkTJ0AI8GGJLfMXvKq42cj5c006fnLz6fXy6zfoVjJizi8BNTpu8on8ziI1cKy9d9DGNuY17Ce7wuejpQ==
+parse5@^7.0.0:
+ version "7.3.0"
+ resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.3.0.tgz#d7e224fa72399c7a175099f45fc2ad024b05ec05"
+ integrity sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==
+ dependencies:
+ entities "^6.0.0"
+
path-exists@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
@@ -5711,6 +6217,11 @@ property-information@^5.0.0:
dependencies:
xtend "^4.0.0"
+property-information@^6.0.0:
+ version "6.5.0"
+ resolved "https://registry.yarnpkg.com/property-information/-/property-information-6.5.0.tgz#6212fbb52ba757e92ef4fb9d657563b933b7ffec"
+ integrity sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==
+
property-information@^7.0.0:
version "7.1.0"
resolved "https://registry.yarnpkg.com/property-information/-/property-information-7.1.0.tgz#b622e8646e02b580205415586b40804d3e8bfd5d"
@@ -6319,11 +6830,30 @@ regjsparser@^0.12.0:
dependencies:
jsesc "~3.0.2"
+rehype-raw@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/rehype-raw/-/rehype-raw-7.0.0.tgz#59d7348fd5dbef3807bbaa1d443efd2dd85ecee4"
+ integrity sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==
+ dependencies:
+ "@types/hast" "^3.0.0"
+ hast-util-raw "^9.0.0"
+ vfile "^6.0.0"
+
relative-time-format@^1.1.6:
version "1.1.6"
resolved "https://registry.yarnpkg.com/relative-time-format/-/relative-time-format-1.1.6.tgz#724a5fbc3794b8e0471b6b61419af2ce699eb9f1"
integrity sha512-aCv3juQw4hT1/P/OrVltKWLlp15eW1GRcwP1XdxHrPdZE9MtgqFpegjnTjLhi2m2WI9MT/hQQtE+tjEWG1hgkQ==
+remark-gfm@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/remark-gfm/-/remark-gfm-3.0.1.tgz#0b180f095e3036545e9dddac0e8df3fa5cfee54f"
+ integrity sha512-lEFDoi2PICJyNrACFOfDD3JlLkuSbOa5Wd8EPt06HUdptv8Gn0bxYTdbU/XXQ3swAPkEaGxxPN9cbnMHvVu1Ig==
+ dependencies:
+ "@types/mdast" "^3.0.0"
+ mdast-util-gfm "^2.0.0"
+ micromark-extension-gfm "^2.0.0"
+ unified "^10.0.0"
+
remark-parse@^11.0.0:
version "11.0.0"
resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-11.0.0.tgz#aa60743fcb37ebf6b069204eb4da304e40db45a1"
@@ -6420,6 +6950,13 @@ run-parallel@^1.1.9:
dependencies:
queue-microtask "^1.2.2"
+sade@^1.7.3:
+ version "1.8.1"
+ resolved "https://registry.yarnpkg.com/sade/-/sade-1.8.1.tgz#0a78e81d658d394887be57d2a409bf703a3b2701"
+ integrity sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==
+ dependencies:
+ mri "^1.1.0"
+
safe-array-concat@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.1.3.tgz#c9e54ec4f603b0bbb8e7e5007a5ee7aecd1538c3"
@@ -7090,6 +7627,19 @@ unicode-trie@^2.0.0:
pako "^0.2.5"
tiny-inflate "^1.0.0"
+unified@^10.0.0:
+ version "10.1.2"
+ resolved "https://registry.yarnpkg.com/unified/-/unified-10.1.2.tgz#b1d64e55dafe1f0b98bb6c719881103ecf6c86df"
+ integrity sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==
+ dependencies:
+ "@types/unist" "^2.0.0"
+ bail "^2.0.0"
+ extend "^3.0.0"
+ is-buffer "^2.0.0"
+ is-plain-obj "^4.0.0"
+ trough "^2.0.0"
+ vfile "^5.0.0"
+
unified@^11.0.0:
version "11.0.5"
resolved "https://registry.yarnpkg.com/unified/-/unified-11.0.5.tgz#f66677610a5c0a9ee90cab2b8d4d66037026d9e1"
@@ -7103,6 +7653,13 @@ unified@^11.0.0:
trough "^2.0.0"
vfile "^6.0.0"
+unist-util-is@^5.0.0:
+ version "5.2.1"
+ resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-5.2.1.tgz#b74960e145c18dcb6226bc57933597f5486deae9"
+ integrity sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==
+ dependencies:
+ "@types/unist" "^2.0.0"
+
unist-util-is@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-6.0.0.tgz#b775956486aff107a9ded971d996c173374be424"
@@ -7117,6 +7674,13 @@ unist-util-position@^5.0.0:
dependencies:
"@types/unist" "^3.0.0"
+unist-util-stringify-position@^3.0.0:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz#03ad3348210c2d930772d64b489580c13a7db39d"
+ integrity sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==
+ dependencies:
+ "@types/unist" "^2.0.0"
+
unist-util-stringify-position@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz#449c6e21a880e0855bf5aabadeb3a740314abac2"
@@ -7124,6 +7688,14 @@ unist-util-stringify-position@^4.0.0:
dependencies:
"@types/unist" "^3.0.0"
+unist-util-visit-parents@^5.0.0, unist-util-visit-parents@^5.1.1:
+ version "5.1.3"
+ resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz#b4520811b0ca34285633785045df7a8d6776cfeb"
+ integrity sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==
+ dependencies:
+ "@types/unist" "^2.0.0"
+ unist-util-is "^5.0.0"
+
unist-util-visit-parents@^6.0.0:
version "6.0.1"
resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz#4d5f85755c3b8f0dc69e21eca5d6d82d22162815"
@@ -7132,6 +7704,15 @@ unist-util-visit-parents@^6.0.0:
"@types/unist" "^3.0.0"
unist-util-is "^6.0.0"
+unist-util-visit@^4.0.0:
+ version "4.1.2"
+ resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-4.1.2.tgz#125a42d1eb876283715a3cb5cceaa531828c72e2"
+ integrity sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==
+ dependencies:
+ "@types/unist" "^2.0.0"
+ unist-util-is "^5.0.0"
+ unist-util-visit-parents "^5.1.1"
+
unist-util-visit@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-5.0.0.tgz#a7de1f31f72ffd3519ea71814cccf5fd6a9217d6"
@@ -7205,6 +7786,32 @@ utrie@^1.0.2:
dependencies:
base64-arraybuffer "^1.0.2"
+uvu@^0.5.0:
+ version "0.5.6"
+ resolved "https://registry.yarnpkg.com/uvu/-/uvu-0.5.6.tgz#2754ca20bcb0bb59b64e9985e84d2e81058502df"
+ integrity sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==
+ dependencies:
+ dequal "^2.0.0"
+ diff "^5.0.0"
+ kleur "^4.0.3"
+ sade "^1.7.3"
+
+vfile-location@^5.0.0:
+ version "5.0.3"
+ resolved "https://registry.yarnpkg.com/vfile-location/-/vfile-location-5.0.3.tgz#cb9eacd20f2b6426d19451e0eafa3d0a846225c3"
+ integrity sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==
+ dependencies:
+ "@types/unist" "^3.0.0"
+ vfile "^6.0.0"
+
+vfile-message@^3.0.0:
+ version "3.1.4"
+ resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-3.1.4.tgz#15a50816ae7d7c2d1fa87090a7f9f96612b59dea"
+ integrity sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==
+ dependencies:
+ "@types/unist" "^2.0.0"
+ unist-util-stringify-position "^3.0.0"
+
vfile-message@^4.0.0:
version "4.0.3"
resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-4.0.3.tgz#87b44dddd7b70f0641c2e3ed0864ba73e2ea8df4"
@@ -7213,6 +7820,16 @@ vfile-message@^4.0.0:
"@types/unist" "^3.0.0"
unist-util-stringify-position "^4.0.0"
+vfile@^5.0.0:
+ version "5.3.7"
+ resolved "https://registry.yarnpkg.com/vfile/-/vfile-5.3.7.tgz#de0677e6683e3380fafc46544cfe603118826ab7"
+ integrity sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==
+ dependencies:
+ "@types/unist" "^2.0.0"
+ is-buffer "^2.0.0"
+ unist-util-stringify-position "^3.0.0"
+ vfile-message "^3.0.0"
+
vfile@^6.0.0:
version "6.0.3"
resolved "https://registry.yarnpkg.com/vfile/-/vfile-6.0.3.tgz#3652ab1c496531852bf55a6bac57af981ebc38ab"
@@ -7240,6 +7857,11 @@ w3c-keyname@^2.2.0:
resolved "https://registry.yarnpkg.com/w3c-keyname/-/w3c-keyname-2.2.8.tgz#7b17c8c6883d4e8b86ac8aba79d39e880f8869c5"
integrity sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==
+web-namespaces@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/web-namespaces/-/web-namespaces-2.0.1.tgz#1010ff7c650eccb2592cebeeaf9a1b253fd40692"
+ integrity sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==
+
which-boxed-primitive@^1.1.0, which-boxed-primitive@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz#d76ec27df7fa165f18d5808374a5fe23c29b176e"