From 14a063e61ec253c087163ee8b234328d0bbd9145 Mon Sep 17 00:00:00 2001 From: Moustapha Pemy <59936247+mpemy@users.noreply.github.com> Date: Tue, 17 Oct 2023 10:56:42 -0400 Subject: [PATCH] feat (Catalog): Manage Controls Hierarchy (#947) --- .../oscal-react-library/createPalette.d.ts | 4 + .../src/components/OSCALCatalogBaseline.tsx | 477 +++---- .../components/OSCALCatalogManageControl.tsx | 1126 +++++++++++++++++ .../components/OSCALCatalogManageGroup.tsx | 1005 +++++++++++++++ .../src/components/images/icons/Uploading.svg | 3 + .../src/components/images/icons/XinCircle.svg | 4 + .../src/components/images/icons/code.svg | 5 + .../src/components/images/icons/delete.svg | 8 + .../src/components/images/icons/expand.svg | 4 + .../src/components/images/icons/indent.svg | 7 + .../src/components/images/icons/insert.svg | 5 + .../images/icons/orangeCircleChecked.svg | 5 + .../src/components/images/icons/outdent.svg | 7 + .../src/components/images/icons/quote.svg | 4 + packages/oscal-viewer/src/themes/AppTheme.js | 6 + 15 files changed, 2446 insertions(+), 224 deletions(-) create mode 100644 packages/oscal-react-library/src/components/OSCALCatalogManageControl.tsx create mode 100644 packages/oscal-react-library/src/components/OSCALCatalogManageGroup.tsx create mode 100644 packages/oscal-react-library/src/components/images/icons/Uploading.svg create mode 100644 packages/oscal-react-library/src/components/images/icons/XinCircle.svg create mode 100644 packages/oscal-react-library/src/components/images/icons/code.svg create mode 100644 packages/oscal-react-library/src/components/images/icons/delete.svg create mode 100644 packages/oscal-react-library/src/components/images/icons/expand.svg create mode 100644 packages/oscal-react-library/src/components/images/icons/indent.svg create mode 100644 packages/oscal-react-library/src/components/images/icons/insert.svg create mode 100644 packages/oscal-react-library/src/components/images/icons/orangeCircleChecked.svg create mode 100644 packages/oscal-react-library/src/components/images/icons/outdent.svg create mode 100644 packages/oscal-react-library/src/components/images/icons/quote.svg diff --git a/packages/oscal-react-library/createPalette.d.ts b/packages/oscal-react-library/createPalette.d.ts index db06e1cb..d33a6466 100644 --- a/packages/oscal-react-library/createPalette.d.ts +++ b/packages/oscal-react-library/createPalette.d.ts @@ -23,6 +23,8 @@ declare module "@mui/material/styles/createPalette" { shadyGray: PaletteColor; shadyBlue: PaletteColor; smokyWhite: PaletteColor; + darkWhite: PaletteColor; + simpleBlue: PaletteColor; } interface PaletteOptions { backgroundGray?: PaletteColorOptions; @@ -45,5 +47,7 @@ declare module "@mui/material/styles/createPalette" { shadyGray?: PaletteColorOptions; shadyBlue?: PaletteColorOptions; smokyWhite?: PaletteColorOptions; + darkWhite?: PaletteColorOptions; + simpleBlue?: PaletteColorOptions; } } diff --git a/packages/oscal-react-library/src/components/OSCALCatalogBaseline.tsx b/packages/oscal-react-library/src/components/OSCALCatalogBaseline.tsx index 98a078b0..e9cf8235 100644 --- a/packages/oscal-react-library/src/components/OSCALCatalogBaseline.tsx +++ b/packages/oscal-react-library/src/components/OSCALCatalogBaseline.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect } from "react"; import { EditableFieldProps } from "./OSCALEditableTextField"; -import { Button, ButtonGroup, NativeSelect, TextField, Tooltip } from "@mui/material"; +import { Button, NativeSelect, TextField, Tooltip } from "@mui/material"; import BreadCrumbs from "@mui/material/Breadcrumbs"; import Box from "@mui/material/Box"; import Dialog from "@mui/material/Dialog"; @@ -8,6 +8,8 @@ import { Divider, TooltipProps } from "@mui/material"; import { Stack, styled } from "@mui/system"; import { useFetchers } from "./Fetchers"; import { OSCALDialogTitle, OSCALEditingDialog } from "./styles/OSCALDialog"; +import { ReactComponent as QuoteIcon } from "./images/icons/quote.svg"; +import { ReactComponent as CodeIcon } from "./images/icons/code.svg"; import Tab from "@mui/material/Tab"; import TabContext from "@mui/lab/TabContext"; @@ -45,9 +47,9 @@ import { } from "./styles/OSCALButtons"; import { OSCALTextField, OSCALRadio, OSCALFormLabel } from "./styles/OSCALInputs"; -import { FormatBold, FormatItalic, FormatQuote, Subscript, Superscript } from "@mui/icons-material"; +import { FormatBold, FormatItalic, Subscript, Superscript } from "@mui/icons-material"; -import { CodeOffSharp } from "@mui/icons-material"; +import GroupDrawer from "./OSCALCatalogManageGroup"; import { OSCALError } from "./styles/OSCALAlerts"; const MainImage = styled("img")` @@ -215,7 +217,7 @@ export interface ProjectUUIDs { readonly ProfileUUIDS: Array; } -export function CatalogBaselineTabs(_data: OSCALModel) { +export function CatalogBaselineTabs(data: OSCALModel) { const [value, setValue] = React.useState("1"); const handleChange = (event: React.SyntheticEvent, newValue: string) => { @@ -284,7 +286,9 @@ export function CatalogBaselineTabs(_data: OSCALModel) { /> - Control + + + Catalog Details Directory Back Matter @@ -679,75 +683,196 @@ export default function OSCALCatalogBaseline() { console.log("In FilledBoxItem: Operation fail ", e.statusText); } } - const zeroCatalogBaseline = - catalogIds.length === 0 && baselineIds.length === 0 && newOSCALModel === undefined; - return ( - <> - {!uploadSuccessful && zeroCatalogBaseline && ( + const InitUpload: React.FC = () => { + return ( + + + + - - - - + + + + UPLOAD + + theme.palette.secondary.main, }} + onClick={handleAddNewCatalogBaseline} > - No catalogs or baselines defined! + CREATE NEW + - - + + + ); + }; + const ChooseUploadMethod: React.FC = () => { + return ( + + + + + + + Drag and Drop Your File Here + + + theme.palette.primary.main, + }} > - - UPLOAD - - - theme.palette.secondary.main, - }} - onClick={handleAddNewCatalogBaseline} - > - CREATE NEW + - - - + OR + - )} + + + + + ); + }; + const SuccessfullUpload: React.FC = () => { + return ( + + + + + + + + + Upload Complete + + + {fileName} has been successfully uploaded. + + + + GO BACK + + + UPLOAD MORE FILES + + + + {" "} + GO TO FILE + + + + ); + }; + + const zeroCatalogBaseline = + catalogIds.length === 0 && baselineIds.length === 0 && newOSCALModel === undefined; + return ( + <> + {!uploadSuccessful && zeroCatalogBaseline && } {(upload || uploadNewCatalogBaseline) && ( {!startDropping && !endUploading && !uploadSuccessful && ( - - - - - - - Drag and Drop Your File Here - - - theme.palette.primary.main, - }} - > - OR - - - - - - + )} {startDropping && ( @@ -874,68 +934,7 @@ export default function OSCALCatalogBaseline() { )} - {uploadSuccessful && ( - - - - - - - - - Upload Complete - - - - {fileName} has been successfully uploaded. - - - - - GO BACK - - - UPLOAD MORE FILES - - - - {" "} - GO TO FILE - - - - )} + {uploadSuccessful && } {!uploadSuccessful && endUploading && ( @@ -982,11 +981,12 @@ export default function OSCALCatalogBaseline() { alignItems="center" sx={{ height: 50, width: "100%" }} > - + {" "} GO BACK - + + UPLOAD MORE FILES @@ -1024,7 +1024,7 @@ export default function OSCALCatalogBaseline() { {showAlert && ( ); }; - + const Item = styled(Box)(({ theme }) => ({ + backgroundColor: "#ffffff", + padding: theme.spacing(1), + textAlign: "center", + color: theme.palette.text.secondary, + height: 9, + width: 15, + justifyContent: "center", + border: "1px solid", + })); function ButtonBar() { return ( - theme.palette.white.main, height: 20, }} - container - spacing={0} > - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); } const ToolBarMenu: React.FC<{ hasDropdown: boolean | null }> = (item) => { @@ -1385,10 +1409,10 @@ export default function OSCALCatalogBaseline() { }} > - + - + - + @@ -1422,13 +1446,15 @@ export default function OSCALCatalogBaseline() { justifyContent="center" sx={{ height: 12, + top: 8, + position: "relative", }} > - + - + @@ -1934,6 +1960,9 @@ export default function OSCALCatalogBaseline() { + {/* + Add information about the {Model.toLowerCase()}: + */} Add information about the {Model.toLowerCase()}: diff --git a/packages/oscal-react-library/src/components/OSCALCatalogManageControl.tsx b/packages/oscal-react-library/src/components/OSCALCatalogManageControl.tsx new file mode 100644 index 00000000..300a9127 --- /dev/null +++ b/packages/oscal-react-library/src/components/OSCALCatalogManageControl.tsx @@ -0,0 +1,1126 @@ +import React, { useState, MutableRefObject } from "react"; +import MoreVertIcon from "@mui/icons-material/MoreVert"; +import EditIcon from "@mui/icons-material/Edit"; +import { useFetchers } from "./Fetchers"; +import { ReactComponent as DeleteIcon } from "./images/icons/delete.svg"; +import { ReactComponent as FormatIndentDecreaseIcon } from "./images/icons/outdent.svg"; +import { ReactComponent as FormatIndentIncreaseIcon } from "./images/icons/indent.svg"; +import { ReactComponent as InsertIcon } from "./images/icons/insert.svg"; +import { ReactComponent as QuoteIcon } from "./images/icons/quote.svg"; +import { ReactComponent as CodeIcon } from "./images/icons/code.svg"; +import { ReactComponent as OrangeCheckedIcon } from "./images/icons/orangeCircleChecked.svg"; +import { ReactComponent as CancelIcon } from "./images/icons/XinCircle.svg"; +import { FormatBold, FormatItalic, Subscript, Superscript } from "@mui/icons-material"; +import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; +import { + Box, + Card, + CardContent, + Container, + Grid, + Stack, + IconButton, + ListItemText, + MenuItem, + MenuList, + TextField, + Tooltip, + TooltipProps, + Typography, + styled, + Paper, + DialogContent, +} from "@mui/material"; + +import DragIndicatorIcon from "@mui/icons-material/DragIndicator"; +import { EditableFieldProps } from "./OSCALEditableTextField"; +import { + OSCALPrimaryDestructiveButton, + OSCALSecondaryButton, + OSCALTertiaryButton, +} from "./styles/OSCALButtons"; +import { OSCALDialogActions, OSCALDialogTitle, OSCALWarningDialog } from "./styles/OSCALDialog"; +import { OSCALTextField } from "./styles/OSCALInputs"; + +const ControlTooltip = styled(({ className, ...props }: TooltipProps) => ( + +))(({ theme }) => ({ + fontSize: 20, + boxShadow: `0px 0px 10px 0px ${theme.palette.smokyWhite.main}`, +})); + +export interface Group extends EditableFieldProps { + groupTitle: string; + groupID: string; + projectUUID: string; + controls?: Array; + subGroups: Array; + parentID: string; + indent: number; + rightSibling?: Group; +} + +export interface Control extends EditableFieldProps { + controlTitle: string; + controlID: string; + projectUUID: string; + subControls: Array; + parentID: string; + indent: number; + rightSibling?: Control; + index: number; +} +export interface OSCALGroup extends EditableFieldProps { + previousGroupTitle?: MutableRefObject; + group: Group; + open?: boolean; + inputRef?: React.MutableRefObject; +} + +interface OSCALControl extends EditableFieldProps { + control: Control; + open?: boolean; +} + +export function ControlManager(data: OSCALGroup) { + const [init, setInit] = useState(true); + const [selectedControl, setSelectedControl] = useState(); + const [baseControls, setBaseControls] = useState>([]); + const [selectedItemName, setSelectedItemName] = useState(""); + const [showCardMenu, setShowCardMenu] = useState(false); + const [itemYcoordinate, setItemYCoordinate] = useState(0); + const [edit, setEdit] = useState(false); + const [editControl, seteditControl] = useState(); + const [addBelow, setAddBelow] = useState(false); + const [newControlParent, setNewControlParent] = useState(); + const [openDeleteDialog, setOpenDeleteDialog] = useState(false); + const [addNewControl, setAddNewControl] = useState(false); + + const fetchers = useFetchers(); + const fetchTransaction = fetchers["fetchTransaction"]; + + let draggedControls: Array = []; + let draggedInsertBetween: Array = []; + let draggedInsertBottom: Array = []; + let Ids: string[] = []; + const Data: OSCALGroup = data; + let loadControls = false; + const previous = data.previousGroupTitle?.current; + if (previous && previous.length > 0) { + loadControls = previous !== data.group.groupTitle; + } + if (loadControls) { + getData(); + } + + const orderControls: Array = []; + const controlsAndSubs: Array = baseControls.map((x) => ({ + controlTitle: x.title, + controlID: x.id, + projectUUID: data.group.projectUUID, + parentID: x.parent_id, + subControls: [], + indent: 0, + index: 0, + })); + + function reorderControlsAndSetIndent() { + const parent_ids: Array = []; + controlsAndSubs.forEach((elt) => { + if (!orderControls.includes(elt)) { + if (elt.parentID === data.group.groupID) { + elt.indent = 0; + orderControls.push(elt); + parent_ids.push(elt.controlID); + } + } + }); + + let depth = 0; + while (depth < 10) { + controlsAndSubs.forEach((elt) => { + if (!orderControls.includes(elt)) { + if (parent_ids.includes(elt.parentID)) { + const index = orderControls.findIndex((parent) => parent.controlID === elt.parentID); + if (index >= 0) { + let siblingIndex = 0; + elt.indent = orderControls[index].indent + 20; + //Find last sibling + const orderedSiblings = orderControls.filter( + (control) => control.parentID === elt.parentID + ); + if (orderedSiblings.length > 0) { + const lastSibling = orderedSiblings[orderedSiblings.length - 1]; + siblingIndex = orderControls.findIndex( + (control) => control.controlID === lastSibling.controlID + ); + } + const mainIndex = siblingIndex > 0 ? siblingIndex : index; + orderControls[index].subControls.push(elt); + if (index + 1 >= orderControls.length) { + orderControls.push(elt); + parent_ids.push(elt.controlID); + } else { + orderControls.splice(mainIndex + 1, 0, elt); + parent_ids.push(elt.controlID); + } + } + } + } + }); + depth = depth + 1; + } + } + function setSiblings(controls: Array) { + if (controls.length === 0) return; + + for (let i = 0; i < controls.length - 1; i++) { + controls[i].rightSibling = controls[i + 1]; + controls[i].index = i; + } + controls[controls.length - 1].index = controls.length - 1; + } + const defaultControl: Control = { + controlID: "", + controlTitle: "", + projectUUID: data.group.projectUUID, + subControls: [], + indent: 0, + parentID: data.group.groupID, + index: 0, + }; + function getControlsAndSubs() { + if (!data.group.projectUUID) return; + const rootFile = "projects/catalog_" + data.group.projectUUID + "/oscal_data.json"; + const request_json = { + oscal_file: rootFile, + parent_id: data.group.groupID, + }; + + fetchTransaction( + "/list_all_controls", + request_json, + getCatalogControlsSuccess, + getCatalogControlsFail + ); + function getCatalogControlsSuccess(response: any) { + console.log( + "In ControlManager: Successfull Transaction Call to get controls for Group ", + data.group.groupID + ); + setBaseControls(response.controls); + } + function getCatalogControlsFail(e: any) { + console.log("In ControlManager: Operation fail ", e.statusText, " request: ", request_json); + } + } + //TODO This code will be used to implement Control editing + // function editControlTitle(ID: string, Name: string) { + // const rootFile = "projects/catalog_" + data.group.projectUUID + "/oscal_data.json"; + // const request_json = { + // oscal_file: rootFile, + // id: ID, + // title: Name, + // }; + // function addNewControlSuccess(response: any) { + // console.log("successful addition of a new Control", response); + // } + // function addNewControlFail(e: any) { + // console.log("Fail to create a new Control", e.statusText); + // } + // fetchTransaction("/edit_Control_title", request_json, addNewControlSuccess, addNewControlFail); + // } + //TODO this code will be use to implement control deletion + function deleteControl(ID: string) { + const id = ID === "" ? selectedControl?.controlID : ID; + const rootFile = "projects/catalog_" + data.group.projectUUID + "/oscal_data.json"; + const request_json = { + oscal_file: rootFile, + id: id, + }; + function deleteControlSuccess(response: any) { + console.log("successful deletion of the Control", id, response); + } + function deleteControlFail(e: any) { + console.log("Fail to delete the Control", id, e.statusText); + } + fetchTransaction("/delete_id", request_json, deleteControlSuccess, deleteControlFail); + } + function moveControl(ID: string, newParentID: string) { + const rootFile = "projects/catalog_" + data.group.projectUUID + "/oscal_data.json"; + const request_json = { + oscal_file: rootFile, + id: ID, + new_parent_id: newParentID, + }; + function moveControlSuccess(response: any) { + console.log("Successful addition of a sub-control", response); + getData(); + } + function moveControlFail(e: any) { + console.log("Fail to create a new control", e.statusText, "with request ", request_json); + } + fetchTransaction("/move_control", request_json, moveControlSuccess, moveControlFail); + } + function moveControlSibling(ID: string, newSiblingID: string) { + const rootFile = "projects/catalog_" + data.group.projectUUID + "/oscal_data.json"; + const request_json = { + oscal_file: rootFile, + id: ID, + new_sibling_id: newSiblingID, + }; + function moveControlSuccess(response: any) { + console.log("Successful addition of a sub-control", response); + } + function moveControlFail(e: any) { + console.log("Fail to create a new control", e.statusText, "with request ", request_json); + } + fetchTransaction("/move_control_sibling", request_json, moveControlSuccess, moveControlFail); + } + function insertControlAt( + ID: string, + parentID: string, + insert_index: number, + delete_index: number + ) { + const rootFile = "projects/catalog_" + data.group.projectUUID + "/oscal_data.json"; + const request_json = { + oscal_file: rootFile, + id: ID, + parent_id: parentID, + insert_index: insert_index.toString(), + delete_index: delete_index.toString(), + }; + function insertControlSuccess(response: any) { + console.log("Successful insertion of a sub-control", response); + } + function insertControlFail(e: any) { + console.log("Fail to insert a control", e.statusText, " with request ", request_json); + } + fetchTransaction("/insert_control_at", request_json, insertControlSuccess, insertControlFail); + } + const NewControlButton: React.FC<{ disabled?: boolean; noControls?: boolean }> = (input) => { + function handleClick() { + setAddNewControl(true); + } + return ( + theme.palette.lightGray.main, + width: "99%", + }} + > + {!input.noControls && ( + <> + + + + NEW CONTROL + + + + + )} + {input.noControls && !addNewControl && ( + <> + + + + NEW CONTROL + + {" "} + + + )} + + ); + }; + const ControlDialog: React.FC = (data) => { + const [hasTitle, setHasTitle] = useState(false); + const [preID, setPreID] = useState(""); + const [title, setTitle] = useState(""); + const index = data.control.controlID.indexOf("_"); + const idValue = index > 0 ? data.control.controlID.substring(0, index) : data.control.controlID; + + const ID = ""; + function SaveNewControl(ID: string, Name: string, parentID: string) { + if (!data.control.projectUUID) { + console.log( + "Fail to create a new Control", + ID, + " title ", + title, + " because the project uuid is undefined" + ); + return; + } + const rootFile = "projects/catalog_" + data.control.projectUUID + "/oscal_data.json"; + let tempID = preID; + if (preID === "") { + tempID = Name.substring(0, 2); + } + ID = tempID + "_" + window.self.crypto.randomUUID(); + const request_json = { + oscal_file: rootFile, + id: ID, + title: Name, + parent_id: parentID, + }; + function addNewControlSuccess(response: any) { + console.log("Successful addition of a new Control", response); + } + function addNewControlFail(e: any) { + console.log("Fail to create a new Control", e.statusText, "request_json: ", request_json); + } + fetchTransaction("/add_control", request_json, addNewControlSuccess, addNewControlFail); + } + const Item = styled(Box)(({ theme }) => ({ + backgroundColor: theme.palette.white.main, + ...theme.typography.body2, + padding: theme.spacing(1), + textAlign: "center", + color: theme.palette.text.secondary, + alignContent: "center", + justifyItems: "center", + height: 9, + width: 20, + justifyContent: "center", + border: "1px solid", + })); + + function handleEditIDChange(event: { target: { value: string | undefined } }) { + setPreID(event.target.value ?? ""); + } + function handleEditControlTitleChange(event: { target: { value: string } }) { + let cTitle = event.target.value; + cTitle = cTitle.trim(); + if (cTitle.length >= 2) { + setHasTitle(true); + setTitle(cTitle); + } + } + function handleCancel() { + setAddNewControl(false); + } + function handleSaveControl() { + if (addBelow) { + SaveNewControl(ID, title, newControlParent?.controlID ?? data.control.parentID); + } + + SaveNewControl(ID, title, data.control.parentID); + //Reload all Data after saving new control + getData(); + setAddBelow(false); + setAddBelow(false); + + ///TODO these lines below are only to fix the linting issue + setInit(true); + } + if (Data.previousGroupTitle?.current !== Data.group.groupTitle) { + setAddNewControl(false); + setAddBelow(false); + return null; + } + return ( + theme.palette.primary.main, + }} + > + + theme.palette.white.main, + }} + > + {!init && } + + theme.palette.primary.main, + "& .MuiOutlinedInput-root": { + "& > fieldset": { + borderColor: (theme) => theme.palette.white.main, + }, + }, + "&:hover .MuiOutlinedInput-root": { + "& > fieldset": { + borderColor: (theme) => theme.palette.white.main, + }, + }, + input: { color: (theme) => theme.palette.white.main }, + defaultValue: { color: (theme) => theme.palette.white.main }, + label: { color: (theme) => theme.palette.white.main }, + }} + onChange={handleEditIDChange} + > + + theme.palette.primary.main, + "& .MuiOutlinedInput-root": { + "& > fieldset": { + borderColor: (theme) => theme.palette.white.main, + }, + }, + "&:hover .MuiOutlinedInput-root": { + "& > fieldset": { + borderColor: (theme) => theme.palette.white.main, + }, + }, + input: { color: (theme) => theme.palette.white.main }, + defaultValue: { color: (theme) => theme.palette.white.main }, + label: { color: (theme) => theme.palette.white.main }, + }} + onChange={handleEditControlTitleChange} + > + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); + }; + const DeleteDialog: React.FC = (data) => { + const [deleteText, setDeleteText] = useState(""); + function handleClose() { + setOpenDeleteDialog(false); + } + function handleDeleteText(event: { target: { value: string | undefined } }) { + setDeleteText(event.target.value ?? ""); + } + function handleDelete() { + if (deleteText.toLowerCase() === "delete") { + deleteControl(data.control.controlID); + //reload Data + getData(); + } + setOpenDeleteDialog(false); + } + return ( + + + + + + If deleted, all associated controls will be permanently deleted. You cannot + undo this action. + + + Please enter {"'delete'"} to confirm. + + + + + + Cancel + + Delete Control + + + + ); + }; + function itemWidth(indent: any): string { + const percent = 100 - (1 * indent) / 5; + const result = percent.toString() + "%"; + return result; + } + function handleDragInsertLeave(event: any) { + event.preventDefault(); + event.stopPropagation(); + event.target.style.height = 0; + event.target.style.background = "#FFFFFF"; //TODO for some reason, theme.palette.white.main does not work here + } + + function handleDragInsertEnd(event: any) { + event.preventDefault(); + event.stopPropagation(); + draggedControls = []; + draggedInsertBetween = []; + } + function handleInsert(event: any) { + event.preventDefault(); + event.stopPropagation(); + event.target.style.height = 10; + event.target.style.background = "#FF6600"; //TODO for some reason, theme.palette.primaryAccent.main does not work here + } + + function changeFirstOrLastControl(newControl: Control, control: Control, first: boolean) { + if (draggedInsertBetween.length > 0) { + if (first) { + console.log("Start Moving control ", newControl.controlID, " to the top of the list "); + if (newControl.parentID === data.group.groupID) + insertControlAt(newControl.controlID, newControl.parentID, 0, newControl.index + 1); + else moveControl(newControl.controlID, control.controlID); + } else { + console.log("Start Moving control ", newControl.controlID, " to the bottom of the list "); + if (newControl.parentID === control.parentID) + insertControlAt( + newControl.controlID, + newControl.parentID, + orderControls.length, + newControl.index + ); + else + insertControlAt( + newControl.controlID, + data.group.groupID, + orderControls.length, + newControl.index + ); + } + } + } + const ControlItem: React.FC = (data) => { + const [isDragged, setIsDragged] = useState(false); + const [doneDropping, setEndDropping] = useState(false); + const [draggingOver, setDraggingOver] = useState(false); + + const handleCardMenu = (event: any) => { + setShowCardMenu(true); + setItemYCoordinate(event.clientY); + }; + + function allowDrop(event: any) { + event.preventDefault(); + const id = data.control.controlID; + if (!Ids.includes(id)) { + Ids.push(id); + } + setDraggingOver(true); + } + function handleDragLeave(event: any) { + event.preventDefault(); + event.stopPropagation(); + event.target.style.border = "1px solid #D2D2D2"; + setDraggingOver(false); + } + const endDragNDrop: React.DragEventHandler | undefined = (event: any) => { + event.preventDefault(); + draggedControls = []; + draggedInsertBetween = []; + }; + const startDrag = (event: any) => { + draggedControls = []; + Ids = []; + event.preventDefault(); + + const id = data.control.controlID; + if (!Ids.includes(id)) { + Ids.push(id); + draggedControls.push(data.control); + } + setIsDragged(true); + }; + const handleDrop = (event: any) => { + event.preventDefault(); + event.stopPropagation(); + event.target.style.border = "1px solid #D2D2D2"; + setEndDropping(true); + }; + + if (data.control.indent > 240) { + /// TODO These lines are just to fix linting issues. These parameters will be used when implementing Edit control and delete control + if (selectedItemName === "" && openDeleteDialog && draggingOver) setInit(true); + ///End TODO + return null; + } + let between = false; + between = draggedInsertBetween.length > 0 ? draggedInsertBetween[0] : false; + if (between && draggedControls?.length > 0) { + const newControl = draggedControls[0]; + console.log("Start Moving control ", newControl.controlID, "before ", data.control.controlID); + if ( + data.control.controlID === firstControlID || + (data.control.controlID === lastControlID && draggedInsertBottom.length > 0) + ) { + changeFirstOrLastControl( + newControl, + data.control, + data.control.controlID === firstControlID + ); + } else { + if (newControl.parentID !== data.control.parentID) { + moveControlSibling(newControl.controlID, data.control.controlID); + } else { + if (newControl.index < data.control.index) + moveControlSibling(newControl.controlID, data.control.controlID); + else { + insertControlAt( + newControl.controlID, + newControl.parentID, + data.control.index, + newControl.index + 1 + ); + } + } + } + draggedInsertBottom = []; + draggedInsertBetween = []; + draggedControls = []; + getData(); + } else { + if (doneDropping && draggedControls?.length > 0) { + const subControl = draggedControls[0]; + console.log("Start Moving control ", subControl.controlID, "into ", data.control.controlID); + moveControl(subControl.controlID, data.control.controlID); + subControl.parentID = data.control.parentID; + data.control.subControls?.push(subControl); + setSiblings(data.control.subControls); + draggedInsertBetween = []; + draggedControls = []; + getData(); + } + } + function handleDropInsert(event: any) { + event.preventDefault(); + event.stopPropagation(); + draggedInsertBetween.push(true); + setEndDropping(true); + } + function handleBottomDropInsert(event: any) { + event.preventDefault(); + event.stopPropagation(); + draggedInsertBetween.push(true); + draggedInsertBottom.push(true); + setEndDropping(true); + } + if (isDragged) return null; + const sepIndex = data.control.controlID.indexOf("_"); + const shownControlID = + sepIndex === -1 + ? data.control.controlID.toUpperCase() + : data.control.controlID.substring(0, sepIndex).toUpperCase(); + const barWidth = itemWidth(data.control.indent); + const leftPoint = (data.control.indent / 5).toString() + "%"; + const cardID = "card" + data.control.controlID; + return ( + <> + 0 ? 20 : 0, + width: barWidth, + left: leftPoint, + position: "relative", + }} + onDragOver={handleInsert} + onDrop={handleDropInsert} + onDrag={handleInsert} + draggable={true} + onDragLeave={handleDragInsertLeave} + onDragEnd={handleDragInsertEnd} + > + + + + theme.palette.white.main, + background: (theme) => theme.palette.white.main, + boxShadow: `0px 0px 6px 0px #00000040`, //TODO (theme) => theme.palette.darkWhite.main + }} + key={data.control.controlID.toUpperCase()} + draggable={true} + onDrag={startDrag} + onDragOver={allowDrop} + onDragLeave={handleDragLeave} + onDrop={handleDrop} + onDragEnd={endDragNDrop} + onClick={() => { + setSelectedItemName(data.control.controlTitle); + setSelectedControl(data.control); + }} + > + + + {draggingOver && ( + theme.palette.primaryAccent.main, + }} + > + )} + theme.palette.primary.main, + }} + > + + + theme.palette.simpleBlue.main, + padding: "7px, 31px, 7px, 31px", + borderRadius: "2px", + gap: "8px", + }} + > + theme.typography.fontWeightBold, + fontSize: "1.25rem", + color: (theme) => theme.palette.black.main, + top: 5, + left: 10, + position: "relative", + }} + > + {shownControlID} + + + + theme.palette.black.main, + fontWeight: (theme) => theme.typography.fontWeightRegular, + position: "relative", + width: data.open ? 460 - data.control.indent : 710 - data.control.indent, + fontSize: 16, + }} + > + {data.control.controlTitle.length < 100 + ? data.control.controlTitle + : data.control.controlTitle.substring(0, 99) + "..."} + + + theme.palette.primary.main, + position: "absolute", + }} + onClick={handleCardMenu} + > + + + theme.palette.white.main, + color: (theme) => theme.palette.primary.main, + position: "absolute", + }} + > + + + + + + {data.control.controlID === lastControlID && ( + 0 ? 20 : 0, + width: barWidth, + left: leftPoint, + position: "relative", + }} + onDragOver={handleInsert} + onDrop={handleBottomDropInsert} + onDrag={handleInsert} + draggable={true} + onDragLeave={handleDragInsertLeave} + onDragEnd={handleDragInsertEnd} + > + )} + + {showCardMenu && } + {editControl?.controlID === data.control.controlID && (edit || addBelow) && ( + + )} + + ); + }; + const ControlItemMenuBar: React.FC = (data) => { + function handleEdit() { + setEdit(true); + setAddNewControl(true); + seteditControl(data.control); + setShowCardMenu(false); + } + function handleDelete() { + setOpenDeleteDialog(true); + setAddNewControl(false); + getData(); + setShowCardMenu(false); + } + function handleAddBelow() { + setAddNewControl(false); + setAddBelow(true); + setNewControlParent(data.control); + seteditControl(data.control); + getData(); + setShowCardMenu(false); + } + function handleIncreaseIndent() { + if (data.control === undefined) { + setShowCardMenu(false); + return; + } + + const sibling = data.control.rightSibling; + if (sibling === undefined) { + setShowCardMenu(false); + return; + } + moveControl(data.control.controlID, sibling.controlID); + // Reload the main data + getData(); + setShowCardMenu(false); + } + function handleDecreaseIndent() { + if (data.control === undefined) { + setShowCardMenu(false); + return; + } + + const parentID = data.control.parentID; + if (parentID === undefined) { + setShowCardMenu(false); + return; + } + const parent = orderControls.find((x) => x.controlID === parentID) ?? defaultControl; + const new_parent_id = parent.parentID; + moveControl(data.control.controlID, new_parent_id); + // Reload the main data + getData(); + setShowCardMenu(false); + } + return ( + + + + + + + + Edit Control + + + + + + Delete Control + + + + + + Add Control Below + + + + + + Increase Indent + + + + + + Decrease Indent + + + + + ); + }; + function getData() { + getControlsAndSubs(); + } + + //Now let us process the data collected before rendering the component. + + orderControls.forEach((elt) => { + setSiblings(elt.subControls); + }); + reorderControlsAndSetIndent(); + + const orphans = orderControls.filter((elt) => elt.parentID === data.group.groupID); + if (orphans.length > 0) { + let sibling = orphans[0]; + for (let i = 1; i < orphans.length; i++) { + orphans[i].rightSibling = sibling; + sibling = orphans[i]; + orphans[i].index = i; + } + } + const noControl = orderControls.length === 0; + const lastControlID = + orderControls.length > 0 ? orderControls[orderControls.length - 1].controlID : ""; + const firstControlID = orderControls.length > 0 ? orderControls[0].controlID : ""; + return ( + + { + getData(); + }} + > + {addNewControl && } + {orderControls.map((item) => ( + + ))} + {} + {showCardMenu && } + + + ); +} + +export default ControlManager; diff --git a/packages/oscal-react-library/src/components/OSCALCatalogManageGroup.tsx b/packages/oscal-react-library/src/components/OSCALCatalogManageGroup.tsx new file mode 100644 index 00000000..e889a3bd --- /dev/null +++ b/packages/oscal-react-library/src/components/OSCALCatalogManageGroup.tsx @@ -0,0 +1,1005 @@ +import React, { useState, useEffect, useRef } from "react"; +import MoreVertIcon from "@mui/icons-material/MoreVert"; +import SaveIcon from "@mui/icons-material/Save"; +import EditIcon from "@mui/icons-material/Edit"; +import { useFetchers } from "./Fetchers"; +import { ReactComponent as DeleteIcon } from "./images/icons/delete.svg"; +import { ReactComponent as FormatIndentDecreaseIcon } from "./images/icons/outdent.svg"; +import { ReactComponent as FormatIndentIncreaseIcon } from "./images/icons/indent.svg"; +import { ReactComponent as InsertIcon } from "./images/icons/insert.svg"; +import { + Box, + Button, + Card, + CardContent, + Container, + DialogContent, + Divider, + Grid, + Stack, + IconButton, + ListItemText, + MenuItem, + MenuList, + TextField, + Tooltip, + TooltipProps, + Typography, + styled, +} from "@mui/material"; +import { Group, OSCALGroup, ControlManager } from "./OSCALCatalogManageControl"; +import KeyboardDoubleArrowLeftIcon from "@mui/icons-material/KeyboardDoubleArrowLeft"; +import DragIndicatorIcon from "@mui/icons-material/DragIndicator"; +import { OSCALPrimaryDestructiveButton, OSCALTertiaryButton } from "./styles/OSCALButtons"; +import { OSCALDialogActions, OSCALDialogTitle, OSCALWarningDialog } from "./styles/OSCALDialog"; +import { OSCALTextField } from "./styles/OSCALInputs"; + +const GroupTooltip = styled(({ className, ...props }: TooltipProps) => ( + +))(({ theme }) => ({ + fontSize: 20, + boxShadow: `0px 0px 10px 0px ${theme.palette.smokyWhite.main}`, +})); + +const DrawerHeader = styled("div")(({ theme }) => ({ + display: "flex", + alignItems: "center", + justifyContent: "flex-end", + padding: theme.spacing(0, 1), + // necessary for content to be below app bar + ...theme.mixins.toolbar, +})); + +export interface OSCALProject { + projectUUID: string; +} +interface RefObject { + clickMe: () => void; +} + +export function GroupDrawer(data: OSCALProject) { + const [baseGroups, setBaseGroups] = useState>([]); + + const fetchers = useFetchers(); + const fetchTransaction = fetchers["fetchTransaction"]; + + const closedDrawerWidth = 55; + const OpenDrawerWidth = 312; + + const [open, setOpen] = React.useState(false); + const handleDrawerOpen = () => { + setOpen(true); + }; + let draggedGroups: Array = []; + let Ids: string[] = []; + const handleDrawerClose = () => { + setOpen(false); + }; + const handleDrawerOpenClose = () => { + if (open) return handleDrawerClose(); + else return handleDrawerOpen(); + }; + + const defaultGroup: Group = { + groupID: "", + groupTitle: "Root", + projectUUID: data.projectUUID, + subGroups: [], + indent: 0, + parentID: "", + }; + const drawerWidth = open ? OpenDrawerWidth : closedDrawerWidth; + const [addNewGroup, setAddNewGroup] = useState(false); + const [selectedItemName, setSelectedItemName] = useState(""); + const [selectedItemGroup, getSelectedGroup] = useState(defaultGroup); + const [showCardMenu, setShowCardMenu] = useState(false); + const [itemYcoordinate, setItemYCoordinate] = useState(0); + const [edit, setEdit] = useState(false); + const [editGroup, setEditGroup] = useState(); + const [addBelow, setAddBelow] = useState(false); + const [newGroupParent, setNewGroupParent] = useState(); + const [openDeleteDialog, setOpenDeleteDialog] = useState(false); + + const previousSelectedItemName = useRef(""); + useEffect(() => { + previousSelectedItemName.current = selectedItemName; + }, [selectedItemName]); + + useEffect(() => { + getData(); + }, []); + + const inputElement = useRef(); + + const groupsAndSubs: Array = baseGroups.map((x) => ({ + groupTitle: x.title, + groupID: x.id, + projectUUID: data.projectUUID, + parentID: x.parent_id, + subGroups: [], + indent: 0, + others: [], + })); + + const orderGroups: Array = []; + function reorderGroupsAndSetIndent() { + const parent_ids: Array = []; + groupsAndSubs.forEach((elt) => { + if (!orderGroups.includes(elt)) { + if (elt.parentID === "") { + elt.indent = 0; + orderGroups.push(elt); + parent_ids.push(elt.groupID); + } + } + }); + + let depth = 0; + while (depth < 8) { + groupsAndSubs.forEach((elt) => { + if (!orderGroups.includes(elt)) { + if (parent_ids.includes(elt.parentID)) { + const index = orderGroups.findIndex((parent) => parent.groupID === elt.parentID); + if (index >= 0) { + elt.indent = orderGroups[index].indent + 10; + orderGroups[index].subGroups.push(elt); + if (index + 1 >= orderGroups.length) { + orderGroups.push(elt); + parent_ids.push(elt.groupID); + } else { + orderGroups.splice(index + 1, 0, elt); + parent_ids.push(elt.groupID); + } + } + } + } + }); + depth = depth + 1; + } + } + + reorderGroupsAndSetIndent(); + + orderGroups.forEach((elt) => { + setSiblings(elt.subGroups); + }); + + const orphans = orderGroups.filter((elt) => elt.parentID === ""); + if (orphans.length > 0) { + let sibling = orphans[0]; + for (let i = 1; i < orphans.length; i++) { + orphans[i].rightSibling = sibling; + sibling = orphans[i]; + } + } + + function editGroupTitle(ID: string, Name: string) { + const rootFile = "projects/catalog_" + data.projectUUID + "/oscal_data.json"; + const request_json = { + oscal_file: rootFile, + id: ID, + title: Name, + }; + function addNewGroupSuccess(response: any) { + console.log("Successful addition of a new group", response); + } + function addNewGroupFailed(e: any) { + console.log("Failed to create a new group", e.statusText, " with request ", request_json); + } + fetchTransaction("/edit_group_title", request_json, addNewGroupSuccess, addNewGroupFailed); + } + function deleteGroup(ID: string) { + const id = ID === "" ? selectedItemGroup?.groupID : ID; + const rootFile = "projects/catalog_" + data.projectUUID + "/oscal_data.json"; + const request_json = { + oscal_file: rootFile, + id: id, + }; + function deleteGroupSuccess(response: any) { + console.log("Successful deletion of the group", id, response); + } + function deleteGroupFailed(e: any) { + console.log("Failed to delete the group", id, e.statusText, " with request ", request_json); + } + fetchTransaction("/delete_id", request_json, deleteGroupSuccess, deleteGroupFailed); + } + function saveNewGroup(ID: string, Name: string, parentID: string) { + const rootFile = "projects/catalog_" + data.projectUUID + "/oscal_data.json"; + const request_json = { + oscal_file: rootFile, + id: ID, + title: Name, + parent_id: parentID, + }; + function addNewGroupSuccess(response: any) { + console.log("Successful addition of a new group", response); + } + function addNewGroupFailed(e: any) { + console.log("Failed to create a new group", e.statusText, "request ", request_json); + } + fetchTransaction("/add_group", request_json, addNewGroupSuccess, addNewGroupFailed); + } + function moveGroup(ID: string, Name: string, newParentID: string) { + const rootFile = "projects/catalog_" + data.projectUUID + "/oscal_data.json"; + const request_json = { + oscal_file: rootFile, + id: ID, + new_parent_id: newParentID, + }; + function moveGroupSuccess(response: any) { + console.log("Successful addition of a new group", response); + } + function moveGroupFailed(e: any) { + console.log("Failed to create a new group", e.statusText, "with request ", request_json); + } + fetchTransaction("/move_group", request_json, moveGroupSuccess, moveGroupFailed); + } + + function getGroupsAndSubs() { + const rootFile = "projects/catalog_" + data.projectUUID + "/oscal_data.json"; + const request_json = { + oscal_file: rootFile, + }; + + fetchTransaction( + "/list_all_groups", + request_json, + getCatalogGroupsSuccess, + getCatalogGroupsFailed + ); + function getCatalogGroupsSuccess(response: any) { + console.log("In GroupDrawer: Successfull Transaction Call to get groups"); + setBaseGroups(response.groups); + } + function getCatalogGroupsFailed(e: any) { + console.log( + "In GroupDrawer: Operation list_all_groups failed ", + e.statusText, + " with request ", + request_json + ); + } + } + + function setSiblings(groups: Array) { + if (groups.length === 0) return; + + for (let i = 0; i < groups.length - 1; i++) { + groups[i].rightSibling = groups[i + 1]; + } + } + function getData() { + getGroupsAndSubs(); + } + + function handleAddNewGroup() { + setAddNewGroup(true); + } + + const DeleteDialog: React.FC = (data) => { + const [deleteText, setDeleteText] = useState(""); + function handleClose() { + setOpenDeleteDialog(false); + } + function handleDeleteText(event: { target: { value: string | undefined } }) { + setDeleteText(event.target.value ?? ""); + } + function handleDelete() { + if (deleteText.toLowerCase() === "delete") { + deleteGroup(data.group.groupID); + //reload Data + getData(); + } + setOpenDeleteDialog(false); + } + return ( + + + + + + If deleted, all associated subgroups and controls will be permanently deleted. + You cannot undo this action. + + + Please enter {"'delete'"} to confirm. + + + + + + Cancel + + Delete Group + + + + ); + }; + const GroupDialog: React.FC = (data) => { + function handleNewGroupClick() { + setSelectedItemName("NewGroup"); + } + const selectedGroup = data.group; + let ID = ""; + let Name = ""; + let preID = ""; + function handleEditIDChange(event: { target: { value: string | undefined } }) { + preID = event.target.value ?? ""; + ID = preID + "_" + window.self.crypto.randomUUID(); + } + function handleEditgroupTitleChange(event: { target: { value: string } }) { + Name = event.target.value; + } + function handleAddBelow() { + setAddNewGroup(true); + setAddBelow(true); + setNewGroupParent(selectedGroup); + handleSaveNewGroup(); + setShowCardMenu(false); + } + function handleIncreaseIndent() { + const sibling = selectedGroup.rightSibling; + if (sibling === undefined) { + setShowCardMenu(false); + return; + } + moveGroup(selectedGroup.groupID, selectedGroup.groupTitle, sibling.groupID); + // Reload the main data + getData(); + setShowCardMenu(false); + } + function handleDecreaseIndent() { + const parentID = selectedGroup.parentID; + if (parentID === undefined) { + setShowCardMenu(false); + return; + } + + const parent = orderGroups.find((x) => x.groupID === parentID) ?? defaultGroup; + const new_parent_id = parent.parentID; + + moveGroup(selectedGroup.groupID, selectedGroup.groupTitle, new_parent_id); + // Reload the main data + getData(); + setShowCardMenu(false); + } + function handleSaveNewGroup() { + if (edit && !addBelow) { + editGroupTitle(selectedGroup.groupID, Name); + setAddNewGroup(false); + getData(); + setEditGroup(defaultGroup); + return; + } + if (Name.length < 1) return; + if (preID === "") { + ID = Name.substring(0, 1) + ID; + } + if (addBelow) { + const parent_id = newGroupParent === undefined ? "" : newGroupParent.groupID; + saveNewGroup(ID, Name, parent_id); + setAddNewGroup(false); + getData(); + setAddBelow(false); + return; + } + saveNewGroup(ID, Name, ""); + setAddNewGroup(false); + getData(); + } + function handleDeleteGroup() { + setOpenDeleteDialog(true); + setAddNewGroup(false); + } + + const isSelected = selectedItemName === "NewGroup" ? true : false; + return ( + theme.palette.lightGray.main, + background: (theme) => theme.palette.gray.main, + width: 312, + }} + > + theme.palette.backgroundGray.main, + border: "1px solid", + borderColor: (theme) => theme.palette.lightGray.main, + width: 300, + }} + > + + + + + + theme.palette.white.main, + "& .MuiOutlinedInput-root": { + "& > fieldset": { + borderColor: (theme: { palette: { secondary: { main: any } } }) => + theme.palette.secondary.main, + }, + }, + }} + > + theme.palette.white.main, + "& .MuiOutlinedInput-root": { + "& > fieldset": { + borderColor: (theme) => theme.palette.secondary.main, + }, + }, + }} + > + + + + + theme.palette.secondary.main, + color: (theme) => theme.palette.white.main, + }, + }} + > + + + theme.palette.secondary.main, + color: (theme) => theme.palette.white.main, + }, + }} + > + + + theme.palette.secondary.main, + color: (theme) => theme.palette.white.main, + }, + }} + > + + + theme.palette.white.main, + color: (theme) => theme.palette.white.main, + }, + }} + onClick={handleDeleteGroup} + > + + + theme.palette.secondary.main, + ":hover": { + backgroundColor: (theme) => theme.palette.secondary.main, + color: (theme) => theme.palette.white.main, + }, + }} + onClick={handleSaveNewGroup} + > + + + + + + + theme.palette.primaryAccent.main + : (theme) => theme.palette.backgroundGray.main, + }} + > + + ); + }; + const RootLevel: React.FC = () => { + function handleRootLevelClick() { + setSelectedItemName("Root"); + getSelectedGroup(defaultGroup); + } + const rootSelected = selectedItemName === "Root"; + const text = open ? "Root Level Controls" : "RC"; + return ( + theme.palette.backgroundGray.main, + border: "1px solid", + borderColor: (theme) => theme.palette.lightGray.main, + height: 48, + }} + onClick={handleRootLevelClick} + > + + {text} + + + theme.palette.primaryAccent.main + : (theme) => theme.palette.backgroundGray.main, + }} + > + + ); + }; + const GroupItemMenuBar: React.FC = (data) => { + function handleEdit() { + setEdit(true); + setAddNewGroup(true); + setEditGroup(data.group); + setShowCardMenu(false); + } + function handleDelete() { + setOpenDeleteDialog(true); + setAddNewGroup(false); + getData(); + setShowCardMenu(false); + } + function handleAddBelow() { + setAddNewGroup(true); + setAddBelow(true); + setNewGroupParent(data.group); + setEditGroup(data.group); + getData(); + setShowCardMenu(false); + } + function handleIncreaseIndent() { + if (data.group === undefined) { + setShowCardMenu(false); + return; + } + + const sibling = data.group.rightSibling; + if (sibling === undefined) { + setShowCardMenu(false); + return; + } + moveGroup(data.group.groupID, data.group.groupTitle, sibling.groupID); + // Reload the main data + getData(); + setShowCardMenu(false); + } + function handleDecreaseIndent() { + if (data.group === undefined) { + setShowCardMenu(false); + return; + } + + const parentID = data.group.parentID; + if (parentID === undefined) { + setShowCardMenu(false); + return; + } + const parent = orderGroups.find((x) => x.groupID === parentID) ?? defaultGroup; + const new_parent_id = parent.parentID; + moveGroup(data.group.groupID, data.group.groupTitle, new_parent_id); + // Reload the main data + getData(); + setShowCardMenu(false); + } + return ( + + + + + + + + Edit Group + + + + + + Delete Group + + + + + + Add Group Below + + + + + + Increase Indent + + + + + + Decrease Indent + + + + + ); + }; + + const GroupItem: React.FC = (data) => { + const [isDragged, setIsDragged] = useState(false); + const [doneDropping, setEndDropping] = useState(false); + const [draggingOver, setDraggingOver] = useState(false); + + const unitHeight = 48; + + const itemHeight = unitHeight; + + const handleCardMenu = (event: any) => { + setShowCardMenu(true); + setItemYCoordinate(event.clientY); + }; + + function allowDrop(event: any) { + event.preventDefault(); + const id = data.group.groupID; + if (!Ids.includes(id)) { + Ids.push(id); + } + setDraggingOver(true); + } + function handleDragLeave(event: any) { + event.preventDefault(); + event.stopPropagation(); + setDraggingOver(false); + } + const endDragNDrop: React.DragEventHandler | undefined = (event: any) => { + event.preventDefault(); + draggedGroups = []; + }; + const startDrag = (event: any) => { + draggedGroups = []; + Ids = []; + event.preventDefault(); + + const id = data.group.groupID; + if (!Ids.includes(id)) { + Ids.push(id); + draggedGroups.push(data.group); + } + setIsDragged(true); + }; + const handleDrop = (event: any) => { + event.preventDefault(); + event.stopPropagation(); + setEndDropping(true); + }; + + if (data.group.indent > 90) { + return null; + } + if (doneDropping && draggedGroups?.length > 0) { + const subGroup = draggedGroups[0]; + moveGroup(subGroup.groupID, subGroup.groupTitle, data.group.groupID); + // update groups + getData(); + subGroup.parentID = data.group.parentID; + data.group.subGroups?.push(subGroup); + setSiblings(data.group.subGroups); + } + if (isDragged) return null; + + return ( + <> + theme.palette.lightGray.main, + background: (theme) => theme.palette.gray.main, + width: drawerWidth, + }} + draggable={true} + onDrag={startDrag} + onDrop={handleDrop} + > + + theme.palette.backgroundGray.main, + border: "1px solid", + borderColor: (theme) => theme.palette.lightGray.main, + width: open ? 300 - data.group.indent : 55, + ":hover": { + backgroundColor: (theme) => theme.palette.gray.main, + }, + }} + key={data.group.groupID.toUpperCase()} + draggable="true" + onDrag={startDrag} + onDragOver={allowDrop} + onDragLeave={handleDragLeave} + onDrop={handleDrop} + onDragEnd={endDragNDrop} + onClick={() => { + setSelectedItemName(data.group.groupTitle); + getSelectedGroup(data.group); + }} + > + {draggingOver && ( + theme.palette.primaryAccent.main, + }} + > + )} + {open && ( + + + + )} + theme.palette.primary.main, + }} + > + theme.palette.white.main, + left: 5, + top: 5, + position: "absolute", + }} + > + {data.group.groupID.toUpperCase().substring(0, 2)} + + + {open && ( + + + {data.group.groupTitle.length < 20 + ? data.group.groupTitle + : data.group.groupTitle.substring(0, 19) + "..."} + + + )} + {open && ( + + + + )} + theme.palette.primaryAccent.main + : (theme) => theme.palette.backgroundGray.main, + }} + >{" "} + + + + + {showCardMenu && } + {open && editGroup?.groupID === data.group.groupID && (edit || addBelow) && ( + + )} + + ); + }; + const NewGroupButton: React.FC = () => { + const text = open ? " NEW GROUP +" : "+"; + return ( + <> + + + + + + + + + ); + }; + function mainClick() { + if (showCardMenu) setShowCardMenu(false); + } + + return ( + <> + + + + theme.palette.primary.main, + borderRadius: "3.5px 3.5px 0px 0px", + }} + > + {open && ( + theme.palette.white.main, + left: 20, + position: "absolute", + }} + > + GROUPS + + )} + theme.palette.white.main }} + > + {" "} + + + + + {orderGroups.map((item) => ( + + ))} + {addNewGroup && open && !edit && !addBelow && ( + + )} + + + + + + + {" "} + {selectedItemName} + + + + + + + + {showCardMenu && } + + + ); +} + +export default GroupDrawer; diff --git a/packages/oscal-react-library/src/components/images/icons/Uploading.svg b/packages/oscal-react-library/src/components/images/icons/Uploading.svg new file mode 100644 index 00000000..cf9cfc2e --- /dev/null +++ b/packages/oscal-react-library/src/components/images/icons/Uploading.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/oscal-react-library/src/components/images/icons/XinCircle.svg b/packages/oscal-react-library/src/components/images/icons/XinCircle.svg new file mode 100644 index 00000000..35570f91 --- /dev/null +++ b/packages/oscal-react-library/src/components/images/icons/XinCircle.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/oscal-react-library/src/components/images/icons/code.svg b/packages/oscal-react-library/src/components/images/icons/code.svg new file mode 100644 index 00000000..6c08d017 --- /dev/null +++ b/packages/oscal-react-library/src/components/images/icons/code.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/oscal-react-library/src/components/images/icons/delete.svg b/packages/oscal-react-library/src/components/images/icons/delete.svg new file mode 100644 index 00000000..8ca2144a --- /dev/null +++ b/packages/oscal-react-library/src/components/images/icons/delete.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/packages/oscal-react-library/src/components/images/icons/expand.svg b/packages/oscal-react-library/src/components/images/icons/expand.svg new file mode 100644 index 00000000..47ac237e --- /dev/null +++ b/packages/oscal-react-library/src/components/images/icons/expand.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/oscal-react-library/src/components/images/icons/indent.svg b/packages/oscal-react-library/src/components/images/icons/indent.svg new file mode 100644 index 00000000..b426b20e --- /dev/null +++ b/packages/oscal-react-library/src/components/images/icons/indent.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/packages/oscal-react-library/src/components/images/icons/insert.svg b/packages/oscal-react-library/src/components/images/icons/insert.svg new file mode 100644 index 00000000..ea01c752 --- /dev/null +++ b/packages/oscal-react-library/src/components/images/icons/insert.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/oscal-react-library/src/components/images/icons/orangeCircleChecked.svg b/packages/oscal-react-library/src/components/images/icons/orangeCircleChecked.svg new file mode 100644 index 00000000..4986286a --- /dev/null +++ b/packages/oscal-react-library/src/components/images/icons/orangeCircleChecked.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/oscal-react-library/src/components/images/icons/outdent.svg b/packages/oscal-react-library/src/components/images/icons/outdent.svg new file mode 100644 index 00000000..90419b7f --- /dev/null +++ b/packages/oscal-react-library/src/components/images/icons/outdent.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/packages/oscal-react-library/src/components/images/icons/quote.svg b/packages/oscal-react-library/src/components/images/icons/quote.svg new file mode 100644 index 00000000..7d4d5795 --- /dev/null +++ b/packages/oscal-react-library/src/components/images/icons/quote.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/oscal-viewer/src/themes/AppTheme.js b/packages/oscal-viewer/src/themes/AppTheme.js index 1ec1680b..bedd7780 100644 --- a/packages/oscal-viewer/src/themes/AppTheme.js +++ b/packages/oscal-viewer/src/themes/AppTheme.js @@ -84,6 +84,12 @@ export const appTheme = createTheme({ smokyWhite: { main: "#00000029", }, + darkWhite: { + main: "#00000040", + }, + simpleBlue: { + main: "#00286754", + }, }, typography: { h1: {