Files
shuffle-cracked/frontend/src/components/PartnersUsecasesTab.jsx
T
2025-12-19 18:05:04 +05:30

2430 lines
78 KiB
React

import {
Box,
Tooltip,
Typography,
Switch,
Button,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
TextField,
Select,
MenuItem,
FormControl,
InputLabel,
IconButton,
Menu,
ListItemIcon,
ListItemText,
Checkbox,
CircularProgress,
Skeleton,
} from "@mui/material";
import React, { useContext, useEffect, useState } from "react";
import { getTheme } from "../theme.jsx";
import { Context } from "../context/ContextApi.jsx";
import { triggers } from "../views/AngularWorkflow.jsx";
import AddIcon from "@mui/icons-material/Add";
import CloseIcon from "@mui/icons-material/Close";
import { toast } from "react-toastify";
import MoreVertIcon from "@mui/icons-material/MoreVert";
import EditIcon from "@mui/icons-material/Edit";
import DeleteIcon from "@mui/icons-material/Delete";
import AddCircleOutlineIcon from "@mui/icons-material/AddCircleOutline";
import OpenInNewIcon from '@mui/icons-material/OpenInNew';
import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline";
import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
import InfoIcon from "@mui/icons-material/Info";
import { Link, useNavigate } from "react-router-dom";
import { grey } from "../views/AngularWorkflow.jsx"
const IMAGE_UPLOAD_PLACEHOLDER = "Uploading image...";
// Helper to extract the first markdown image URL from navigation content
export const getFirstImageFromNavigation = (navigation) => {
if (!navigation || !Array.isArray(navigation.items)) {
return "";
}
for (const item of navigation.items) {
if (!item || !Array.isArray(item.content)) {
continue;
}
for (const paragraph of item.content) {
if (typeof paragraph !== "string") {
continue;
}
const match = paragraph.match(/!\[[^\]]*]\(([^)]+)\)/);
if (match && match[1]) {
return match[1];
}
}
}
return "";
};
// Helper function to get the correct image path based on app category
export const getCategoryImagePath = (category) => {
if (!category) return "/images/appCategories/other.svg";
const lowerCategory = category.toLowerCase();
if (lowerCategory.includes("communication")) {
return "/images/appCategories/cases.svg";
} else if (lowerCategory.includes("cases")) {
return "/images/appCategories/cases.svg";
} else if (lowerCategory.includes("network")) {
return "/images/appCategories/network.svg";
} else if (lowerCategory.includes("siem")) {
return "/images/appCategories/siem.svg";
} else if (lowerCategory.includes("edr") || lowerCategory.includes("eradication")) {
return "/images/appCategories/edr.svg";
} else if (lowerCategory.includes("iam")) {
return "/images/appCategories/iam.svg";
} else if (lowerCategory.includes("assets")) {
return "/images/appCategories/assets.svg";
} else if (lowerCategory.includes("intel")) {
return "/images/appCategories/intel.svg";
} else if (lowerCategory.includes("email")) {
return "/images/appCategories/email.svg";
} else if (lowerCategory.includes("webhook")) {
return triggers.find((trigger) => trigger?.name.toLowerCase() === "webhook")?.large_image;
} else if (lowerCategory.includes("schedule")) {
return triggers.find((trigger) => trigger?.name.toLowerCase() === "schedule")?.large_image;
} else if (lowerCategory.includes("pipelines")) {
return triggers.find((trigger) => trigger?.name.toLowerCase() === "pipelines")?.large_image;
} else if (lowerCategory.includes("shuffle workflow")) {
return triggers.find((trigger) => trigger?.name.toLowerCase() === "shuffle workflow")?.large_image;
} else if (lowerCategory.includes("user input")) {
return triggers.find((trigger) => trigger?.name.toLowerCase() === "user input")?.large_image;
} else {
return "/images/appCategories/other.svg";
}
};
// Skeleton component for loading state
const UsecaseCardSkeleton = ({ isArticlesTab }) => {
const { themeMode } = useContext(Context);
const theme = getTheme(themeMode);
// Skeleton for Articles Tab
if (isArticlesTab) {
return (
<Box
sx={{
textDecoration: "none",
backgroundColor: theme.palette.usecaseCardColor,
borderRadius: "16px",
width: {
xs: "230px",
sm: "320px",
},
minWidth: {
xs: "230px",
sm: "320px",
},
overflow: "hidden",
display: "flex",
flexDirection: "column",
}}
>
{/* Header image skeleton */}
<Skeleton
variant="rectangular"
height={160}
sx={{
width: "100%",
bgcolor:
themeMode === "dark"
? "rgba(255, 255, 255, 0.08)"
: "rgba(0, 0, 0, 0.06)",
}}
/>
{/* Body skeleton */}
<Box
sx={{
p: 2,
pt: 2.5,
display: "flex",
flexDirection: "column",
gap: 1.5,
}}
>
<Skeleton
variant="text"
width="80%"
height={26}
sx={{
bgcolor:
themeMode === "dark"
? "rgba(255, 255, 255, 0.1)"
: "rgba(0, 0, 0, 0.08)",
}}
/>
<Skeleton
variant="text"
width="40%"
height={20}
sx={{
bgcolor:
themeMode === "dark"
? "rgba(255, 255, 255, 0.08)"
: "rgba(0, 0, 0, 0.06)",
}}
/>
<Box
sx={{
mt: "auto",
display: "flex",
justifyContent: "flex-end",
alignItems: "center",
gap: 1,
}}
>
<Skeleton
variant="rectangular"
width={38}
height={22}
sx={{
borderRadius: "12px",
bgcolor:
themeMode === "dark"
? "rgba(255, 255, 255, 0.1)"
: "rgba(0, 0, 0, 0.06)",
}}
/>
<Skeleton
variant="circular"
width={24}
height={24}
sx={{
bgcolor:
themeMode === "dark"
? "rgba(255, 255, 255, 0.1)"
: "rgba(0, 0, 0, 0.06)",
}}
/>
</Box>
</Box>
</Box>
);
}
// Skeleton for Usecases Tab
return (
<Box
sx={{
textDecoration: "none",
backgroundColor: theme.palette.usecaseCardColor,
borderRadius: "16px",
padding: {
xs: "15px 20px",
sm: "20px 20px",
lg: "28px 20px",
},
paddingRight: {
xs: "12px",
lg: "14px",
},
paddingLeft: {
xs: "12px",
lg: "24px",
},
display: "flex",
alignItems: "start",
flexDirection: "column",
width: {
xs: "130px",
sm: "300px",
},
minWidth: {
xs: "130px",
sm: "300px",
},
gap: "20px",
position: "relative",
}}
>
<Box
sx={{
width: "100%",
height: "100%",
display: "flex",
alignItems: "flex-start",
justifyContent: "space-between",
}}
>
<div
style={{
display: "flex",
gap: "0px",
position: "relative",
}}
>
{/* Source App Icon Skeleton */}
<Skeleton
variant="circular"
width={40}
height={40}
sx={{
bgcolor: themeMode === "dark" ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)',
position: "relative",
zIndex: 1,
}}
/>
{/* Destination App Icon Skeleton */}
<Skeleton
variant="circular"
width={40}
height={40}
sx={{
bgcolor: themeMode === "dark" ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)',
marginLeft: "-12px",
}}
/>
</div>
<div style={{ display: "flex", alignItems: "center", gap: "5px" }}>
<Skeleton
variant="rectangular"
width={40}
height={20}
sx={{
bgcolor: themeMode === "dark" ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)',
borderRadius: "16px"
}}
/>
<Skeleton
variant="circular"
width={24}
height={24}
sx={{
bgcolor: themeMode === "dark" ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'
}}
/>
</div>
</Box>
<Skeleton
variant="text"
width="80%"
height={30}
sx={{
bgcolor: themeMode === "dark" ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'
}}
/>
</Box>
);
};
// Card renderer for both usecases and articles
const UsecaseCard = ({ usecase, handleToggle, handleOpenDialog, handleDeleteUsecase, isArticlesTab, partnerData }) => {
const [anchorEl, setAnchorEl] = useState(null);
const open = Boolean(anchorEl);
const { themeMode } = useContext(Context);
const theme = getTheme(themeMode);
const navigate = useNavigate();
const handleClick = (event) => {
event.stopPropagation();
event.preventDefault();
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
const handleEdit = () => {
console.log("Edit usecase:", usecase.id);
handleClose();
};
const handleStar = () => {
console.log("Star usecase:", usecase.id);
handleClose();
};
const handleCardClick = (e) => {
// Navigate to usecase detail page
navigate(`/usecases/${usecase.id}`);
};
// Article Card Layout
if (isArticlesTab) {
return (
<Box
sx={{
textDecoration: "none",
backgroundColor: theme.palette.usecaseCardColor,
borderRadius: "16px",
width: {
xs: "230px",
sm: "320px",
},
minWidth: {
xs: "230px",
sm: "320px",
},
overflow: "hidden",
display: "flex",
flexDirection: "column",
"&:hover": {
backgroundColor: theme.palette.usecaseCardHoverColor,
},
}}
>
<Box
component="img"
src={usecase.headerImage || "/images/no_image.png"}
alt={usecase.name}
sx={{
width: "100%",
height: 160,
objectFit: "cover",
}}
/>
<Box
sx={{
p: 2,
pt: 2.5,
display: "flex",
flexDirection: "column",
gap: 1.5,
height: "100%",
}}
>
<Typography
sx={{
color: theme.palette.text.primary,
fontSize: {
xs: "15px",
lg: "16px",
},
fontWeight: 600,
fontFamily: theme.typography.fontFamily,
display: "-webkit-box",
WebkitLineClamp: 2,
WebkitBoxOrient: "vertical",
overflow: "hidden",
}}
>
{usecase.name}
</Typography>
{usecase.created && (
<Typography
sx={{
color: theme.palette.text.secondary,
fontSize: "13px",
}}
>
{new Date(usecase.created * 1000).toLocaleDateString("en-US", {
year: "numeric",
month: "short",
day: "numeric",
})}
</Typography>
)}
<Box
sx={{
mt: -1,
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
gap: 0.5,
}}
onClick={(e) => e.stopPropagation()}
>
<OpenInNewIcon
fontSize="small"
onClick={(e) => {
e.stopPropagation();
window.open(`${window.location.origin}/partners/${partnerData?.name.toLowerCase().replaceAll(" ", "_")}/articles/${usecase.name.toLowerCase().replaceAll(" ", "_")}`, "_blank");
}}
sx={{
color: theme.palette.primary.main,
cursor: "pointer",
}}
/>
<Switch
checked={usecase.public}
onClick={(e) => {
e.stopPropagation();
}}
onChange={(e) => {
e.preventDefault();
e.stopPropagation();
handleToggle(usecase.id);
}}
size="medium"
sx={{
"& .MuiSwitch-switchBase.Mui-checked": {
color: "#4CAF50",
},
"& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track": {
backgroundColor: "#4CAF50",
},
}}
/>
<IconButton
onClick={(e) => {
e.stopPropagation();
handleClick(e);
}}
size="small"
sx={{
color: theme.palette.text.primary,
"&:hover": {
backgroundColor:
themeMode === "dark"
? "rgba(255, 255, 255, 0.1)"
: "rgba(0, 0, 0, 0.05)",
},
}}
>
<MoreVertIcon fontSize="small" />
</IconButton>
</Box>
<Menu
anchorEl={anchorEl}
open={open}
onClose={handleClose}
onClick={(e) => e.stopPropagation()}
PaperProps={{
sx: {
backgroundColor: theme.palette.DialogStyle.backgroundColor,
border: theme.palette.defaultBorder,
borderRadius: "8px",
boxShadow: theme.palette.DialogStyle.boxShadow,
"& .MuiMenuItem-root": {
fontSize: "14px",
color: theme.palette.text.primary,
"&:hover": {
backgroundColor:
themeMode === "dark"
? "rgba(255, 255, 255, 0.1)"
: "rgba(0, 0, 0, 0.05)",
},
},
},
}}
transformOrigin={{ horizontal: "right", vertical: "top" }}
anchorOrigin={{ horizontal: "right", vertical: "bottom" }}
>
<MenuItem
onClick={(e) => {
e.stopPropagation();
handleOpenDialog(usecase);
setAnchorEl(null);
}}
>
<ListItemIcon>
<EditIcon
fontSize="small"
sx={{
color: theme.palette.text.primary,
fontFamily: theme.typography.fontFamily,
}}
/>
</ListItemIcon>
<ListItemText>Edit Article</ListItemText>
</MenuItem>
<MenuItem
onClick={(e) => {
e.stopPropagation();
handleDeleteUsecase(usecase.id);
}}
>
<ListItemIcon>
<DeleteIcon
fontSize="small"
sx={{
color: theme.palette.text.primary,
fontFamily: theme.typography.fontFamily,
}}
/>
</ListItemIcon>
<ListItemText>Delete Article</ListItemText>
</MenuItem>
</Menu>
</Box>
</Box>
);
}
// Usecase Card Layout
return (
<Box
sx={{
textDecoration: "none",
backgroundColor: theme.palette.usecaseCardColor,
borderRadius: "16px",
padding: {
xs: "15px 20px",
sm: "20px 20px",
lg: "28px 20px",
},
paddingRight: {
xs: "12px",
lg: "14px",
},
paddingLeft: {
xs: "12px",
lg: "24px",
},
display: "flex",
alignItems: "start",
flexDirection: "column",
width: {
xs: "130px",
sm: "300px",
},
minWidth: {
xs: "130px",
sm: "300px",
},
gap: "20px",
"&:hover": {
backgroundColor: theme.palette.usecaseCardHoverColor,
},
position: "relative",
}}
>
<Box
sx={{
width: "100%",
height: "100%",
display: "flex",
alignItems: "flex-start",
justifyContent: "space-between",
}}
>
<div
style={{
display: "flex",
gap: "0px",
position: "relative",
}}
>
{/* Source App Icon */}
<Tooltip
title={usecase.srcapp}
placement="top"
>
<img
src={usecase.srcImg}
alt="Source"
style={{
width: "40px",
height: "40px",
borderRadius: "100%",
position: "relative",
border: `1.5px solid ${themeMode === "dark" ? "#f1f1f1" : "#333333"}`,
zIndex: 1,
}}
/>
</Tooltip>
{/* Destination App Icon */}
<Tooltip
title={usecase.dstapp}
placement="top"
>
<img
src={usecase.dstImg || getCategoryImagePath(usecase.dstapp)}
alt="Destination"
style={{
width: "40px",
height: "40px",
borderRadius: "100%",
marginLeft: "-12px",
border: `1.5px solid ${themeMode === "dark" ? "#f1f1f1" : "#333333"}`,
}}
/>
</Tooltip>
</div>
<div style={{ display: "flex", alignItems: "center", gap: "5px" }}>
<OpenInNewIcon
fontSize="small"
onClick={(e) => {
e.stopPropagation();
window.open(`${window.location.origin}/usecases/${usecase.name.toLowerCase().replaceAll(" ", "_")}`, "_blank");
}}
sx={{
color: theme.palette.primary.main,
cursor: "pointer",
}}
/>
<Switch
checked={usecase.public}
onClick={(e) => {
e.stopPropagation();
}}
onChange={(e) => {
e.preventDefault();
e.stopPropagation();
handleToggle(usecase.id);
}}
size="medium"
sx={{
"& .MuiSwitch-switchBase.Mui-checked": {
color: "#4CAF50",
},
"& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track": {
backgroundColor: "#4CAF50",
},
}}
/>
<IconButton
onClick={(e) => {
e.stopPropagation();
handleClick(e);
}}
size="small"
sx={{
color: theme.palette.text.primary,
zIndex: 20,
"&:hover": {
backgroundColor: themeMode === "dark" ? "rgba(255, 255, 255, 0.1)" : "rgba(0, 0, 0, 0.05)",
},
}}
>
<MoreVertIcon fontSize="small" />
</IconButton>
<Menu
anchorEl={anchorEl}
open={open}
onClose={handleClose}
onClick={(e) => e.stopPropagation()}
PaperProps={{
sx: {
backgroundColor: theme.palette.DialogStyle.backgroundColor,
border: theme.palette.defaultBorder,
borderRadius: "8px",
boxShadow: theme.palette.DialogStyle.boxShadow,
"& .MuiMenuItem-root": {
fontSize: "14px",
color: theme.palette.text.primary,
"&:hover": {
backgroundColor: themeMode === "dark" ? "rgba(255, 255, 255, 0.1)" : "rgba(0, 0, 0, 0.05)",
},
},
},
}}
transformOrigin={{ horizontal: "right", vertical: "top" }}
anchorOrigin={{ horizontal: "right", vertical: "bottom" }}
>
<MenuItem
onClick={(e) => {
e.stopPropagation();
handleOpenDialog(usecase);
setAnchorEl(null);
}}
>
<ListItemIcon>
<EditIcon
fontSize="small"
sx={{
color: theme.palette.text.primary,
fontFamily: theme.typography.fontFamily,
}}
/>
</ListItemIcon>
<ListItemText>Edit Usecase</ListItemText>
</MenuItem>
<MenuItem onClick={(e) => {
e.stopPropagation();
handleDeleteUsecase(usecase.id);
}}>
<ListItemIcon>
<DeleteIcon
fontSize="small"
sx={{
color: theme.palette.text.primary,
fontFamily: theme.typography.fontFamily,
}}
/>
</ListItemIcon>
<ListItemText>Delete Usecase</ListItemText>
</MenuItem>
</Menu>
</div>
</Box>
<Typography
sx={{
color: theme.palette.text.primary,
fontSize: {
xs: "14px",
lg: "16px",
},
fontWeight: 600,
fontFamily: theme.typography.fontFamily,
}}
>
{usecase.name}
</Typography>
</Box>
);
};
const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPartnerData, selectedTab }) => {
const { themeMode } = useContext(Context);
const theme = getTheme(themeMode);
const isArticlesTab = selectedTab?.toLowerCase() === "articles";
const entityLabel = isArticlesTab ? "Article" : "Usecase";
// Dialog state
const [openDialog, setOpenDialog] = useState(false);
const [publicWorkflows, setPublicWorkflows] = useState([]);
const [usecaseData, setUsecaseData] = useState([]);
const [isSubmitting, setIsSubmitting] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [isWorkflowLoading, setIsWorkflowLoading] = useState(false);
const [formData, setFormData] = useState({
id: "",
mainContent: {
title: "",
description: "",
categories: [],
publicWorkflowId: "",
sourceAppType: "",
destinationAppType: "",
},
navigation: {
items: [
{
name: `About ${entityLabel}`,
content: [""],
},
],
},
public: false,
created: null,
});
// App categories for dropdowns
const appCategories = [
{ value: "communication", label: "Communication" },
{ value: "cases", label: "Cases" },
{ value: "network", label: "Network" },
{ value: "siem", label: "SIEM" },
{ value: "edr", label: "EDR" },
{ value: "eradication", label: "Eradication" },
{ value: "iam", label: "IAM" },
{ value: "assets", label: "Assets" },
{ value: "intel", label: "Intel" },
{ value: "email", label: "Email" },
{value: "webhook", label: "Webhook"},
{value: "schedule", label: "Schedule"},
{value: "pipelines", label: "Pipelines"},
{value: "shuffle workflow", label: "Shuffle Workflow"},
{value: "user input", label: "User Input"},
{ value: "other", label: "Other" }
];
// Sample workflow options
const workflowOptions = [
"Email Analysis Workflow",
"Incident Response Workflow",
"Alert Triage Workflow",
"Threat Hunting Workflow",
"Vulnerability Management",
];
useEffect(() => {
// Set loading state to true when fetching starts
setIsLoading(true);
if(!isCloud || !userdata?.active_org?.is_partner) {
// If the user is not a partner or if it's not a cloud environment do not make api call :)
return;
}
// Load usecase data from API
fetch(`${globalUrl}/api/v1/partners/${partnerData?.id}/usecases`, {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for PARTNER USECASES :O!");
}
return response.json();
})
.then((responseJson) => {
if (responseJson.success !== false) {
const usecases = responseJson.usecases.map((usecase) => ({
id: usecase.id,
sourceAppType: usecase.mainContent?.sourceAppType || "",
destinationAppType: usecase.mainContent?.destinationAppType || "",
srcImg: getCategoryImagePath(usecase.mainContent?.sourceAppType),
dstImg: getCategoryImagePath(usecase.mainContent?.destinationAppType),
publicWorkflowId: usecase.mainContent?.publicWorkflowId || "",
name: usecase.mainContent?.title,
description: usecase.mainContent?.description,
navigation: usecase.navigation || { items: [] },
categories: usecase.mainContent?.categories || [],
public: usecase?.public || false,
created: usecase?.created || null,
headerImage: getFirstImageFromNavigation(usecase.navigation),
}));
if (usecases.length > 0) {
setUsecaseData(usecases);
} else {
setUsecaseData([]);
}
}
})
.catch((error) => {
console.log(error);
})
.finally(() => {
// Set loading state to false when fetching completes (success or error)
setIsLoading(false);
});
}, [partnerData?.id]);
// Sample categories
const categoryOptions = ["Collect","Enrich", "Detect", "Respond", "Verify"];
// Uploading image to the public bucket
const handleImageUpload = async (imageData) => {
const folderName = "usecase_images";
const usecaseId = formData?.id || partnerData?.id || "usecase";
try {
const response = await fetch(`${globalUrl}/api/v1/image_upload`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
body: JSON.stringify({
imageData,
folder: folderName,
id: usecaseId,
}),
});
// if (response.status !== 200) {
// toast.error("Failed to upload image" + response?.reason);
// return null;
// }
const responseJson = await response.json();
if (!responseJson?.success || !responseJson?.url) {
toast.error("Failed to upload image: " + responseJson?.reason);
return null;
}
return responseJson.url;
} catch (error) {
toast.error("Failed to upload image" + error?.message);
return null;
}
};
// This function is used to update the content with the public image url
const updateContentWithImageUrl = (itemIndex, contentIndex, imageUrl) => {
setFormData((prevData) => {
const newItems = [...(prevData.navigation.items || [])];
const item = newItems[itemIndex];
if (!item) {
return prevData;
}
const contents = [...(item.content || [])];
const existingContent = contents[contentIndex] || "";
const markdownImage = imageUrl ? `![image](${imageUrl})` : "";
const placeholderIndex = existingContent.indexOf(IMAGE_UPLOAD_PLACEHOLDER);
if (placeholderIndex !== -1) {
const before = existingContent.slice(0, placeholderIndex);
const after = existingContent.slice(
placeholderIndex + IMAGE_UPLOAD_PLACEHOLDER.length,
);
const trimmedBefore = before.replace(/\s*$/, "");
const needsNewline =
trimmedBefore.length > 0 && !trimmedBefore.endsWith("\n");
const separator = needsNewline ? "\n" : "";
contents[contentIndex] = `${trimmedBefore}${separator}${markdownImage}${after}`;
} else {
const needsNewline =
existingContent.length > 0 && !existingContent.endsWith("\n");
const separator = needsNewline ? "\n" : "";
contents[contentIndex] = `${existingContent}${separator}${markdownImage}`;
}
newItems[itemIndex] = {
...item,
content: contents,
};
return {
...prevData,
navigation: {
items: newItems,
},
};
});
};
// This function is used to insert the image placeholder (Uploading image...) in the content to show that the image is being uploaded
const insertImagePlaceholder = (itemIndex, contentIndex) => {
setFormData((prevData) => {
const newItems = [...(prevData.navigation.items || [])];
const item = newItems[itemIndex];
if (!item) {
return prevData;
}
const contents = [...(item.content || [])];
const existingContent = contents[contentIndex] || "";
const needsNewline =
existingContent.length > 0 && !existingContent.endsWith("\n");
const separator = needsNewline ? "\n" : "";
contents[contentIndex] = `${existingContent}${separator}${IMAGE_UPLOAD_PLACEHOLDER}`;
newItems[itemIndex] = {
...item,
content: contents,
};
return {
...prevData,
navigation: {
items: newItems,
},
};
});
};
// This function is used to process the image file and upload it to the public bucket
const processImageFile = (imageFile, itemIndex, contentIndex) => {
const reader = new FileReader();
reader.onload = async (loadEvent) => {
const imageData = loadEvent.target?.result;
if (!imageData) {
toast.error("Failed to read image");
updateContentWithImageUrl(itemIndex, contentIndex, "");
return;
}
const imageUrl = await handleImageUpload(imageData);
if (!imageUrl) {
// Clean up the placeholder if upload failed
updateContentWithImageUrl(itemIndex, contentIndex, "");
return;
}
updateContentWithImageUrl(itemIndex, contentIndex, imageUrl);
};
reader.readAsDataURL(imageFile);
};
// This function is used to handle the image paste event
const handleContentPaste = async (event, itemIndex, contentIndex) => {
const items = event.clipboardData?.items;
if (!items || items.length === 0) {
return;
}
let imageFile = null;
for (let i = 0; i < items.length; i += 1) {
const item = items[i];
if (item.kind === "file" && item.type.startsWith("image/")) {
imageFile = item.getAsFile();
break;
}
}
if (!imageFile) {
return;
}
event.preventDefault();
insertImagePlaceholder(itemIndex, contentIndex);
processImageFile(imageFile, itemIndex, contentIndex);
};
// This function is used to handle the image drop event
const handleContentDrop = async (event, itemIndex, contentIndex) => {
event.preventDefault();
const files = event.dataTransfer?.files;
if (!files || files.length === 0) {
return;
}
const imageFile = Array.from(files).find((file) => file.type.startsWith("image/"));
if (!imageFile) {
toast.error("Failed to upload image, only images are supported");
return;
}
insertImagePlaceholder(itemIndex, contentIndex);
processImageFile(imageFile, itemIndex, contentIndex);
};
const getUserProfileWorkflows = (orgId) => {
if (selectedTab === "articles") {
return;
}
setIsWorkflowLoading(true);
fetch(`${globalUrl}/api/v1/partners/${orgId}/workflows`, {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for WORKFLOW EXECUTION :O!");
}
return response.json();
})
.then((responseJson) => {
if (responseJson.success !== false) {
setPublicWorkflows(responseJson || []);
setIsWorkflowLoading(false);
}else {
toast.info(responseJson.message || "No public workflows found for this organization.");
setPublicWorkflows([]);
}
})
.catch((error) => {
console.log(error);
setIsWorkflowLoading(false);
});
}
useEffect(() => {
const orgId = userdata?.active_org?.id;
if(!isCloud || !userdata?.active_org?.is_partner) {
// If the user is not a partner or if it's not a cloud environment do not make api call :)
return;
}
getUserProfileWorkflows(orgId);
}, []);
const handleUpdateUsecase = async (usecaseId, updatedData) => {
try {
// Transform the frontend data structure to match the backend expected structure
const backendUsecaseData = {
id: updatedData.id,
companyInfo: {
id: partnerData?.id.trim(),
name: partnerData?.name.trim(),
},
mainContent: {
title: updatedData.name.trim(),
description: updatedData.description.trim(),
categories: updatedData.categories,
publicWorkflowId: updatedData.publicWorkflowId.trim(),
sourceAppType: updatedData.sourceAppType.trim(),
destinationAppType: updatedData.destinationAppType.trim(),
},
navigation: updatedData.navigation,
public: updatedData.public,
edited: Date.now(),
};
// return
const response = await fetch(`${globalUrl}/api/v1/usecases/${usecaseId}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
body: JSON.stringify(backendUsecaseData),
});
if (response.status !== 200) {
console.error("Failed to update usecase");
toast.error("Failed to update usecase");
return false;
}
const responseData = await response.json();
toast.success("Usecase published status updated successfully");
return true;
} catch (error) {
console.error("Error updating usecase:", error);
toast.error("Error updating usecase");
return false;
}
};
const handleDeleteUsecase = async (usecaseId) => {
try {
const response = await fetch(
`${globalUrl}/api/v1/usecases/${usecaseId}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
}
);
if (response.status !== 200) {
console.error("Failed to delete usecase");
toast.error("Failed to delete usecase");
return false;
}
const responseData = await response.json();
toast.success("Usecase deleted successfully");
// Remove the deleted usecase from the state
setUsecaseData((prevData) =>
prevData?.filter((usecase) => usecase.id !== usecaseId)
);
// Remove the usecase ID from partnerData
setPartnerData((prevData) => ({
...prevData,
usecases: prevData.usecases?.filter((id) => id !== usecaseId),
}));
return true;
} catch (error) {
console.error("Error deleting usecase:", error);
toast.error("Error deleting usecase");
return false;
}
};
const handleToggle = async (currentId) => {
// Find the current usecase
const currentUsecase = usecaseData.find(usecase => usecase.id === currentId);
if (!currentUsecase) return;
// Optimistically update the UI
setUsecaseData((prevData) =>
prevData.map((usecase) =>
usecase.id === currentId
? { ...usecase, public: !usecase.public }
: usecase
)
);
// Prepare the updated data
const updatedPublishedStatus = !currentUsecase.public;
// Create a copy of the usecase with updated published status
const updatedUsecase = {
...currentUsecase,
public: updatedPublishedStatus
};
toast.info(`Updating usecase ${updatedUsecase.name}...`,{
autoClose: 1000,
})
// Send the update to the server
const success = await handleUpdateUsecase(currentId, updatedUsecase);
// If the update failed, revert the UI change
if (!success) {
setUsecaseData((prevData) =>
prevData.map((usecase) =>
usecase.id === currentId
? { ...usecase, public: currentUsecase.public }
: usecase
)
);
}
};
const handleOpenDialog = (usecase) => {
setFormData({
id: usecase?.id || undefined,
companyInfo: {
id: usecase.companyInfo?.id || "",
name: usecase.companyInfo?.name || "",
},
mainContent: {
title: usecase?.name,
description: usecase?.description,
categories: usecase?.categories || [],
publicWorkflowId: usecase?.publicWorkflowId || "",
sourceAppType: usecase?.sourceAppType || "",
destinationAppType: usecase?.destinationAppType || "",
},
navigation: usecase?.navigation || {
items: [
{
name: `About ${entityLabel}`,
content: [""],
},
],
},
public: usecase?.public || false,
created: usecase?.created || Math.floor(Date.now() / 1000),
});
setOpenDialog(true);
};
const handleCloseDialog = () => {
setOpenDialog(false);
setIsSubmitting(false); // Reset loading state when closing dialog
setFormData({
id: "",
mainContent: {
title: "",
description: "",
categories: [],
publicWorkflowId: "",
sourceAppType: "",
destinationAppType: "",
},
navigation: {
items: [
{
name: "About Usecase",
content: [""],
},
],
},
public: false,
});
};
const handleSubmit = () => {
// Validate required fields
const validationErrors = [];
// Check for public workflow selection
if (!isArticlesTab && (!formData.mainContent.publicWorkflowId || formData.mainContent.publicWorkflowId.trim() === "")) {
validationErrors.push("Please select a public workflow");
}
if (!isArticlesTab && (!formData.mainContent.sourceAppType || formData.mainContent.sourceAppType.trim() === "")) {
validationErrors.push("Source app type is required");
}
if (!isArticlesTab && (!formData.mainContent.destinationAppType || formData.mainContent.destinationAppType.trim() === "")) {
validationErrors.push("Destination app type is required");
}
// Validate that source and destination are different
if (formData.mainContent.sourceAppType && formData.mainContent.destinationAppType &&
formData.mainContent.sourceAppType.trim() === formData.mainContent.destinationAppType.trim()) {
validationErrors.push("Source and destination app types must be different");
}
// Check main content fields
if (!formData.mainContent.title || formData.mainContent.title.trim() === "") {
validationErrors.push("Title is required");
} else if (formData.mainContent.title.length < 5) {
validationErrors.push("Title must be not be less than 5 characters");
}
if (!formData.mainContent.description || formData.mainContent.description.trim() === "") {
validationErrors.push("Description is required");
}
// Check categories
if (!isArticlesTab && (!formData.mainContent.categories || !Array.isArray(formData.mainContent.categories) || formData.mainContent.categories.length === 0)) {
validationErrors.push("At least one category must be selected");
}
// Check navigation items
if (!formData.navigation.items || !Array.isArray(formData.navigation.items) || formData.navigation.items.length === 0) {
validationErrors.push("Please add at least one section");
} else {
// Validate each section
for (let i = 0; i < formData.navigation.items.length; i++) {
const item = formData.navigation.items[i];
if (!item.name || item.name.trim() === "") {
validationErrors.push(`Section ${i+1} must have a name`);
}
if (!item.content || !Array.isArray(item.content) || item.content.length === 0 ||
!item.content.some(content => content && content.trim() !== "")) {
validationErrors.push(`Section "${item.name || i+1}" must have content`);
}
}
}
// If there are validation errors, show the first one and return
if (validationErrors.length > 0) {
toast.error(validationErrors[0]);
return;
}
// Set loading state to true
setIsSubmitting(true);
// Get the image paths for source and destination app types
const srcImg = getCategoryImagePath(formData.mainContent.sourceAppType);
const dstImg = getCategoryImagePath(formData.mainContent.destinationAppType);
const response = {
...formData,
companyInfo: {
id: partnerData?.id,
name: partnerData?.name,
},
}
fetch(`${globalUrl}/api/v1/usecases/${formData.id}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
body: JSON.stringify(response),
})
.then((res) => {
if (res.status !== 200) {
console.error("Failed to submit usecase");
toast.error("Failed to submit usecase");
setIsSubmitting(false);
return null;
}
return res.json();
})
.then((responseData) => {
// Reset loading state
setIsSubmitting(false);
if (responseData) {
// Only close the dialog if the request was successful
const usecaseData = {
id: responseData.usecaseId.trim(),
sourceAppType: formData.mainContent?.sourceAppType.trim() || "",
destinationAppType: formData.mainContent?.destinationAppType.trim() || "",
srcImg: getCategoryImagePath(formData.mainContent?.sourceAppType.trim()),
dstImg: getCategoryImagePath(formData.mainContent?.destinationAppType.trim()),
publicWorkflowId: formData.mainContent?.publicWorkflowId.trim() || "",
name: formData.mainContent?.title.trim(),
description: formData.mainContent?.description.trim(),
categories: formData.mainContent?.categories || [],
navigation: formData.navigation || {},
public: formData?.public || false,
created: formData?.created || null,
headerImage: getFirstImageFromNavigation(formData.navigation),
}
setUsecaseData((prevData) => {
// Handle case when prevData is null or undefined
if (!prevData) {
return [usecaseData];
}
// Check if the usecase already exists
const existingIndex = prevData.findIndex(
(usecase) => usecase.id === responseData.usecaseId
);
if (existingIndex !== -1) {
// Update existing usecase
const updatedData = [...prevData];
updatedData[existingIndex] = usecaseData;
return updatedData;
} else {
// Add new usecase
return [...prevData, usecaseData];
}
});
setPartnerData((prevData) => {
// Handle case when prevData is null or undefined
if (!prevData) {
return {
usecases: [responseData.usecaseId.trim()]
};
}
return {
...prevData,
usecases: [
...(prevData.usecases || []),
responseData.usecaseId.trim(),
],
};
});
// Show success message
if(formData?.id){
toast.success("Usecase updated successfully");
}else{
toast.success("Usecase created successfully, publish it with toggle button");
}
handleCloseDialog();
}
})
.catch((error) => {
console.error("Error submitting usecase:", error);
toast.error("Error submitting usecase");
// Reset loading state on error
setIsSubmitting(false);
});
};
// This is used to filter the usecases based on the selected tab
const filteredUsecases = Array.isArray(usecaseData)
? usecaseData.filter((usecase) => {
const hasWorkflowId =
!!usecase.publicWorkflowId &&
usecase.publicWorkflowId.toString().trim() !== "";
if (selectedTab === "articles") {
// Articles: only items without a public workflow ID
return !hasWorkflowId;
}
if (selectedTab === "usecases") {
// Usecases: only items with a public workflow ID
return hasWorkflowId;
}
// Fallback: show all
return true;
})
: [];
return (
<Box
sx={{
width: "100%",
minHeight: "500px",
display: "flex",
flexDirection: "column",
alignItems: "flex-start",
padding: "20px",
paddingLeft: "40px",
justifyContent: "flex-start",
}}
>
{/* Add Usecase / Article Button */}
<Box
sx={{
width: "100%",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
mb: 2,
pr: 4,
}}
>
<Typography sx={{ fontSize: 20, fontWeight: 500, color: "#FFFFFF" }}>
{selectedTab === "usecases" && "Usecases"}
{selectedTab === "articles" && "Articles"}
</Typography>
<Button
variant="contained"
color="primary"
startIcon={<AddIcon />}
onClick={handleOpenDialog}
sx={{
px: 3,
textTransform: "none",
fontSize: "16px",
fontWeight: 600,
}}
>
{isArticlesTab ? "Add Article" : "Add Usecase"}
</Button>
</Box>
{/* Usecases / Articles Grid */}
{isLoading ? (
// Skeleton loading state
<Box
sx={{
display: "flex",
flexDirection: "row",
flexWrap: "wrap",
alignItems: "flex-start",
justifyContent: "flex-start",
gap: 2,
pt: 4,
}}
>
{/* Display 4 skeleton cards while loading */}
{[...Array(4)].map((_, index) => (
<UsecaseCardSkeleton key={index} isArticlesTab={isArticlesTab} partnerData={partnerData} />
))}
</Box>
) : filteredUsecases.length > 0 ? (
// Actual data display (filtered based on public workflow ID)
<Box
sx={{
display: "flex",
flexDirection: "row",
flexWrap: "wrap",
alignItems: "flex-start",
justifyContent: "flex-start",
gap: 2,
pt: 4,
}}
>
{filteredUsecases.map((usecase) => (
<UsecaseCard
handleOpenDialog={handleOpenDialog}
key={usecase.id}
usecase={usecase}
handleToggle={handleToggle}
handleDeleteUsecase={handleDeleteUsecase}
isArticlesTab={isArticlesTab}
partnerData={partnerData}
/>
))}
</Box>
) : (
// Empty state
<Box
sx={{
width: "100%",
height: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
gap: 2,
}}
>
<Typography
sx={{
fontSize: 18,
fontWeight: 500,
color: "#FFFFFF",
}}
>
{isArticlesTab ? "No articles found" : "No usecases found"}
</Typography>
</Box>
)}
{/* Add Usecase Dialog */}
<Dialog
open={openDialog}
onClose={(event, reason) => {
if (reason !== "backdropClick") {
handleCloseDialog();
}
}}
disableEscapeKeyDown
maxWidth="md"
fullWidth
PaperProps={{
style: {
background: theme.palette.backgroundColor,
overflow: "hidden",
margin: 0,
borderRadius: 8,
border: theme.palette.defaultBorder,
},
}}
>
<DialogTitle
sx={{
background: theme.palette.backgroundColor,
borderBottom: theme.palette.defaultBorder,
p: "24px",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
}}
>
<Typography sx={{ fontSize: 20, fontWeight: 500, color: theme.palette.text.primary }}>
{formData?.id ? "Update" : "Add New"} {entityLabel} : {formData?.public ? "Published" : "Draft"}
</Typography>
<IconButton
onClick={handleCloseDialog}
sx={{
color: theme.palette.text.secondary,
"&:hover": {
background: themeMode === "dark" ? "rgba(255, 255, 255, 0.1)" : "rgba(0, 0, 0, 0.05)",
},
}}
>
<CloseIcon fontSize="small" />
</IconButton>
</DialogTitle>
<DialogContent sx={{ background: theme.palette.backgroundColor, p: "24px" }}>
<Box
sx={{
display: "flex",
flexDirection: "column",
gap: 3,
paddingTop: 3,
}}
>
{!isArticlesTab && (
<FormControl>
<Box
sx={{
display: "flex",
flexDirection: "row",
gap: 1,
mb: 2,
alignItems: "center",
}}
>
<Typography
sx={{
color: theme.palette.accentColor,
fontSize: "16px",
fontWeight: 600,
}}
>
Public Workflow
</Typography>
<IconButton
size="small"
sx={{
color: theme.palette.primary.main,
"&:hover": {
backgroundColor: theme.palette.action.hover,
},
}}
onClick={() => {
// Open workflow in new tab - adjust URL as needed
window.open(`${window.location.origin}/workflows/${formData.mainContent.publicWorkflowId}`, "_blank");
}}
>
<OpenInNewIcon fontSize="small" />
</IconButton>
</Box>
<Select
displayEmpty
value={
publicWorkflows?.find(
(workflow) =>
workflow.objectID ===
formData.mainContent.publicWorkflowId
)?.name || ""
}
onChange={(e) => {
setFormData({
...formData,
mainContent: {
...formData.mainContent,
publicWorkflowId: e.target.value,
},
});
}}
renderValue={(selected) => {
if (!selected) {
return (
<span style={{ color: themeMode === "dark" ? "#666666" : "#999999" }}>
Select Public Workflow
</span>
);
}
// Find the workflow with the matching ID and display its name
if (
Array.isArray(publicWorkflows) &&
publicWorkflows.length > 0
) {
const selectedWorkflow = publicWorkflows.find(
(w) => w.id === selected
);
return selectedWorkflow ? selectedWorkflow.name : selected;
}
return selected;
}}
sx={{
backgroundColor: theme.palette.usecaseDialogFieldColor,
height: 40,
color: theme.palette.text.primary,
"& .MuiOutlinedInput-notchedOutline": {
border: theme.palette.defaultBorder,
},
"& .MuiSelect-icon": {
color: theme.palette.text.secondary,
},
"& .MuiSelect-select": {
"&.MuiInputBase-input": {
color: theme.palette.text.primary,
},
"&.Mui-focused": {
backgroundColor: "transparent",
},
},
}}
>
{Array.isArray(publicWorkflows) &&
publicWorkflows.length > 0 ? (
publicWorkflows.map((workflow) => (
<MenuItem
key={workflow.id}
value={workflow.objectID}
sx={{
fontSize: "16px",
"&.Mui-selected": {
backgroundColor: themeMode === "dark" ? "rgba(255, 133, 68, 0.08)" : "rgba(255, 133, 68, 0.15)",
},
"&:hover": {
backgroundColor: themeMode === "dark" ? "rgba(255, 133, 68, 0.04)" : "rgba(255, 133, 68, 0.1)",
},
}}
>
{workflow.name}
</MenuItem>
))
) : (
<MenuItem disabled value="" sx={{ fontSize: "16px" }}>
No workflows available
</MenuItem>
)}
</Select>
</FormControl>
)}
{/* Other data selection (use all data types) */}
{!isArticlesTab && (
<Box sx={{ display: "flex", flexDirection: "row", gap: 2, mt: 2 }}>
<FormControl fullWidth>
<Typography
sx={{
color: grey,
mb: 1,
fontSize: "16px",
}}
>
<b>Coming soon:</b> Files (detection)
</Typography>
</FormControl>
<FormControl fullWidth>
<Typography
sx={{
color: grey,
mb: 1,
fontSize: "16px",
}}
>
<b>Coming soon:</b> Datastore category (threatlists)
</Typography>
</FormControl>
</Box>
)}
{/* App Type Selection */}
{!isArticlesTab && (
<Box sx={{ display: "flex", flexDirection: "row", gap: 2, mt: isArticlesTab ? 0 : 2 }}>
<FormControl fullWidth>
<Typography
sx={{
color: "#ff8544",
mb: 1,
fontSize: "16px",
fontWeight: 600,
}}
>
Source App Type
</Typography>
<Select
displayEmpty
value={formData.mainContent.sourceAppType}
onChange={(e) => {
setFormData({
...formData,
mainContent: {
...formData.mainContent,
sourceAppType: e.target.value,
},
});
}}
renderValue={(selected) => {
if (!selected) {
return (
<span style={{ color: themeMode === "dark" ? "#666666" : "#999999" }}>
Select Source App Type
</span>
);
}
const selectedCategory = appCategories.find(
(cat) => cat.value === selected
);
return selectedCategory ? selectedCategory.label : selected;
}}
sx={{
backgroundColor: theme.palette.usecaseDialogFieldColor,
height: 40,
color: theme.palette.text.primary,
"& .MuiSelect-icon": {
color: "#ff8544",
},
"& .MuiSelect-select": {
"&.MuiInputBase-input": {
color: theme.palette.text.primary,
},
"&.Mui-focused": {
backgroundColor: "transparent",
},
},
}}
>
<MenuItem value="" sx={{ fontSize: "16px" }}>
<em>None</em>
</MenuItem>
{appCategories.map((category) => (
<MenuItem
key={category.value}
value={category.value}
sx={{
fontSize: "16px",
"&.Mui-selected": {
backgroundColor: "rgba(255, 133, 68, 0.08)",
},
"&:hover": {
backgroundColor: "rgba(255, 133, 68, 0.04)",
},
}}
>
{category.label}
</MenuItem>
))}
</Select>
</FormControl>
<FormControl fullWidth>
<Typography
sx={{
color: "#ff8544",
mb: 1,
fontSize: "16px",
fontWeight: 600,
}}
>
Destination App Type
</Typography>
<Select
displayEmpty
value={formData.mainContent.destinationAppType}
onChange={(e) => {
setFormData({
...formData,
mainContent: {
...formData.mainContent,
destinationAppType: e.target.value,
},
});
}}
renderValue={(selected) => {
if (!selected) {
return (
<span style={{ color: themeMode === "dark" ? "#666666" : "#999999" }}>
Select Destination App Type
</span>
);
}
const selectedCategory = appCategories.find(
(cat) => cat.value === selected
);
return selectedCategory ? selectedCategory.label : selected;
}}
sx={{
backgroundColor: theme.palette.usecaseDialogFieldColor,
height: 40,
color: theme.palette.text.primary,
"& .MuiSelect-icon": {
color: "#ff8544",
},
"& .MuiSelect-select": {
"&.MuiInputBase-input": {
color: theme.palette.text.primary,
},
"&.Mui-focused": {
backgroundColor: "transparent",
},
},
}}
>
<MenuItem value="" sx={{ fontSize: "16px" }}>
<em>None</em>
</MenuItem>
{appCategories.map((category) => (
<MenuItem
key={category.value}
value={category.value}
sx={{
fontSize: "16px",
"&.Mui-selected": {
backgroundColor: "rgba(255, 133, 68, 0.08)",
},
"&:hover": {
backgroundColor: "rgba(255, 133, 68, 0.04)",
},
}}
>
{category.label}
</MenuItem>
))}
</Select>
</FormControl>
</Box>
)}
<Box sx={{ mb: 2, mt: 2 }}>
<Typography
sx={{
color: "#ff8544",
mb: 2,
fontSize: "16px",
fontWeight: 600,
}}
>
Main Content
</Typography>
<Box sx={{ display: "flex", flexDirection: "column", gap: 2 }}>
<FormControl>
<Typography
sx={{ color: theme.palette.text.primary, mb: 1, fontSize: "14px" }}
>
Title
</Typography>
<TextField
placeholder="Enter Title"
value={formData.mainContent.title}
onChange={(e) =>
setFormData({
...formData,
mainContent: {
...formData.mainContent,
title: e.target.value,
},
})
}
sx={{
"& .MuiOutlinedInput-root": {
height: 40,
backgroundColor: theme.palette.usecaseDialogFieldColor,
color: theme.palette.text.primary,
"& fieldset": { border: theme.palette.defaultBorder },
},
}}
/>
</FormControl>
<FormControl>
<Typography
sx={{ color: theme.palette.text.primary, mb: 1, fontSize: "14px" }}
>
Description
</Typography>
<TextField
placeholder="Enter Description"
multiline
rows={5}
value={formData.mainContent.description}
onChange={(e) =>
setFormData({
...formData,
mainContent: {
...formData.mainContent,
description: e.target.value,
},
})
}
sx={{
"& .MuiOutlinedInput-root": {
backgroundColor: theme.palette.usecaseDialogFieldColor,
p: 1.5,
color: theme.palette.text.primary,
"& fieldset": { border: theme.palette.defaultBorder },
},
}}
/>
</FormControl>
{!isArticlesTab && (
<FormControl>
<Typography
sx={{ color: theme.palette.text.primary, mb: 1, fontSize: "14px" }}
>
Categories
</Typography>
<Select
multiple
displayEmpty
value={formData.mainContent.categories || []}
onChange={(e) =>
setFormData({
...formData,
mainContent: {
...formData.mainContent,
categories: e.target.value,
},
})
}
renderValue={(selected) => {
if (
!selected ||
(Array.isArray(selected) && selected.length === 0)
) {
return (
<span style={{ color: themeMode === "dark" ? "#666666" : "#999999" }}>
Select Categories
</span>
);
}
return selected.join(", ");
}}
sx={{
backgroundColor: theme.palette.usecaseDialogFieldColor,
height: 40,
color: theme.palette.text.primary,
"& .MuiSelect-icon": {
color: "#ff8544",
},
"& .MuiChip-root": {
color: theme.palette.text.primary,
backgroundColor: theme.palette.chipStyle.backgroundColor,
margin: "2px",
},
"& .MuiSelect-select": {
"&.MuiInputBase-input": {
color: theme.palette.text.primary,
},
"&.Mui-focused": {
backgroundColor: "transparent",
},
},
}}
>
{categoryOptions.map((category) => (
<MenuItem
key={category}
value={category}
sx={{
fontSize: "14px",
"&.Mui-selected": {
backgroundColor: "rgba(255, 133, 68, 0.08)",
},
"&:hover": {
backgroundColor: "rgba(255, 133, 68, 0.04)",
},
}}
>
<Checkbox
size="small"
checked={
Array.isArray(formData.mainContent.categories) &&
formData.mainContent.categories.includes(category)
}
/>
{category}
</MenuItem>
))}
</Select>
</FormControl>
)}
</Box>
</Box>
{/* Navigation Items Section */}
<Box>
<Typography
sx={{
color: "#ff8544",
mb: 2,
fontSize: "16px",
fontWeight: 600,
}}
>
Navigation Items
</Typography>
{formData.navigation.items.map((item, itemIndex) => (
<Box
key={itemIndex}
sx={{
mb: 2,
p: 2,
backgroundColor: theme.palette.usecaseDialogFieldColor,
borderRadius: 1,
border: theme.palette.defaultBorder,
}}
>
<Box
sx={{
display: "flex",
alignItems: "center",
mb: 2,
gap: 1,
}}
>
<TextField
value={item.name}
onChange={(e) => {
const newItems = [...formData.navigation.items];
newItems[itemIndex].name = e.target.value;
setFormData({
...formData,
navigation: { items: newItems },
});
}}
placeholder="Section Name"
sx={{
flex: 1,
"& .MuiOutlinedInput-root": {
height: 40,
backgroundColor: theme.palette.usecaseDialogFieldColor,
color: theme.palette.text.primary,
"& fieldset": { border: theme.palette.defaultBorder },
},
}}
/>
<Box sx={{ display: "flex", gap: 0.5 }}>
<IconButton
onClick={() => {
if (itemIndex > 0) {
const newItems = [...formData.navigation.items];
[newItems[itemIndex - 1], newItems[itemIndex]] = [
newItems[itemIndex],
newItems[itemIndex - 1],
];
setFormData({
...formData,
navigation: { items: newItems },
});
}
}}
disabled={itemIndex === 0}
sx={{
color: theme.palette.text.secondary,
"&:not(:disabled):hover": {
backgroundColor: themeMode === "dark" ? "rgba(255, 255, 255, 0.08)" : "rgba(0, 0, 0, 0.05)",
},
}}
>
<KeyboardArrowUpIcon />
</IconButton>
<IconButton
onClick={() => {
if (
itemIndex <
formData.navigation.items.length - 1
) {
const newItems = [...formData.navigation.items];
[newItems[itemIndex], newItems[itemIndex + 1]] = [
newItems[itemIndex + 1],
newItems[itemIndex],
];
setFormData({
...formData,
navigation: { items: newItems },
});
}
}}
disabled={
itemIndex === formData.navigation.items.length - 1
}
sx={{
color: theme.palette.text.secondary,
"&:not(:disabled):hover": {
backgroundColor: themeMode === "dark" ? "rgba(255, 255, 255, 0.08)" : "rgba(0, 0, 0, 0.05)",
},
}}
>
<KeyboardArrowDownIcon />
</IconButton>
<IconButton
onClick={() => {
const newItems = formData.navigation.items?.filter(
(_, i) => i !== itemIndex
);
setFormData({
...formData,
navigation: { items: newItems },
});
}}
sx={{
color: "#ff4444",
"&:hover": {
backgroundColor: themeMode === "dark" ? "rgba(255, 68, 68, 0.08)" : "rgba(255, 68, 68, 0.15)",
},
}}
>
<DeleteOutlineIcon fontSize="small" />
</IconButton>
</Box>
</Box>
{item.content.map((content, contentIndex) => (
<Box key={contentIndex} sx={{ mb: 1 }}>
<TextField
multiline
rows={14}
value={content}
onChange={(e) => {
const newItems = [...formData.navigation.items];
newItems[itemIndex].content[contentIndex] =
e.target.value;
setFormData({
...formData,
navigation: { items: newItems },
});
}}
onPaste={(event) => handleContentPaste(event, itemIndex, contentIndex)}
onDrop={(event) => handleContentDrop(event, itemIndex, contentIndex)}
onDragOver={(event) => event.preventDefault()}
placeholder="Content (Markdown supported)"
helperText={
<Box sx={{ display: "flex", alignItems: "center", gap: 0.5, mt: 0.5 }}>
<InfoIcon sx={{ fontSize: "14px", opacity: 0.7 }} />
<Typography variant="caption" sx={{ opacity: 0.7 }}>
Markdown syntax is supported and Use ### for subItem for Table of Contents. Drag and drop or paste images to the content to upload them.
</Typography>
</Box>
}
fullWidth
sx={{
"& .MuiOutlinedInput-root": {
backgroundColor: theme.palette.usecaseDialogFieldColor,
color: theme.palette.text.primary,
"& fieldset": { border: theme.palette.defaultBorder },
},
"& .MuiOutlinedInput-root textarea": {
overflow: "auto",
resize: "vertical",
},
}}
/>
{item.content.length > 1 && (
<Button
size="small"
onClick={() => {
const newItems = [...formData.navigation.items];
newItems[itemIndex].content = newItems[
itemIndex
].content?.filter((_, i) => i !== contentIndex);
setFormData({
...formData,
navigation: { items: newItems },
});
}}
sx={{
color: "#ff4444",
"&:hover": {
textDecoration: "underline",
textUnderlineOffset: "4px",
backgroundColor: "transparent",
},
}}
>
Remove Paragraph
</Button>
)}
</Box>
))}
{/*
<Button
startIcon={<AddCircleOutlineIcon />}
onClick={() => {
const newItems = [...formData.navigation.items];
newItems[itemIndex].content.push("");
setFormData({
...formData,
navigation: { items: newItems },
});
}}
sx={{
color: "#4CAF50",
"&:hover": { backgroundColor: themeMode === "dark" ? "rgba(76, 175, 80, 0.08)" : "rgba(76, 175, 80, 0.15)" },
}}
>
Add Paragraph
</Button> */}
</Box>
))}
<Button
startIcon={<AddCircleOutlineIcon />}
onClick={() => {
setFormData({
...formData,
navigation: {
items: [
...formData.navigation.items,
{ name: "", content: [""] },
],
},
});
}}
sx={{
color: theme.palette.accentColor,
"&:hover": { backgroundColor: themeMode === "dark" ? "rgba(255, 133, 68, 0.08)" : "rgba(255, 133, 68, 0.15)" },
}}
>
Add New Section
</Button>
</Box>
</Box>
</DialogContent>
<DialogActions
sx={{
p: "16px 24px",
background: theme.palette.backgroundColor,
borderTop: theme.palette.defaultBorder,
gap: 2,
}}
>
<Button
onClick={handleCloseDialog}
sx={{
textTransform: "none",
color: theme.palette.text.secondary,
fontSize: "14px",
fontWeight: 500,
"&:hover": {
background: themeMode === "dark" ? "rgba(255, 255, 255, 0.1)" : "rgba(0, 0, 0, 0.05)",
},
}}
>
Cancel
</Button>
<Button
variant="contained"
color="primary"
startIcon={isSubmitting ? <CircularProgress size={16} sx={{color: "inherit"}}/> : <AddIcon />}
onClick={handleSubmit}
disabled={isSubmitting}
sx={{
px: 3,
textTransform: "none",
fontSize: "14px",
fontWeight: 600,
opacity: isSubmitting ? 0.7 : 1,
}}
>
{isSubmitting
? (formData?.id ? "Updating..." : "Adding...")
: (formData?.id ? `Update ${entityLabel}` : `Add ${entityLabel}`)
}
</Button>
</DialogActions>
</Dialog>
</Box>
);
};
export default PartnersUsecasesTab;