diff --git a/app/src/components/left/ComponentDrag.tsx b/app/src/components/left/ComponentDrag.tsx index 5f5b6e97..0f370e60 100644 --- a/app/src/components/left/ComponentDrag.tsx +++ b/app/src/components/left/ComponentDrag.tsx @@ -1,69 +1,73 @@ -import ComponentPanelItem from '../right/ComponentPanelItem'; -import Grid from '@mui/material/Grid'; -import React from 'react'; -import { RootState } from '../../redux/store'; -import makeStyles from '@mui/styles/makeStyles'; -import { useSelector } from 'react-redux'; - -const ComponentDrag = ({ isThemeLight }): JSX.Element => { - const classes = useStyles(); - const state = useSelector((store: RootState) => store.appState); - - const isFocus = (targetId: Number) => { - return state.canvasFocus.componentId === targetId ? true : false; - }; - - return ( -
-
-

- {state.projectType === 'Next.js' || state.projectType === 'Gatsby.js' - ? 'Pages' - : 'Root Components'} -

- - {state.components - .filter((comp) => state.rootComponents.includes(comp.id)) - .map((comp) => { - return ( - - ); - })} - -
-
- ); -}; - -const useStyles = makeStyles({ - panelWrapper: { - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - flexGrow: 1, - overflow: 'auto' - }, - panelWrapperList: { - minHeight: '120px' - }, - lightThemeFontColor: { - color: '#fff' - }, - darkThemeFontColor: { - color: '#fff' - } -}); - -export default ComponentDrag; +import React from 'react'; +import Grid from '@mui/material/Grid'; +import { RootState } from '../../redux/store'; +import makeStyles from '@mui/styles/makeStyles'; +import { useSelector } from 'react-redux'; +import ComponentPanelItem from '../right/ComponentPanelItem'; + + +const useStyles = makeStyles({ + panelWrapper: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + flexGrow: 1, + overflow: 'auto' + }, + panelWrapperList: { + minHeight: 'auto' + }, + lightThemeFontColor: { + color: '#fff' + }, + darkThemeFontColor: { + color: '#fff' + } +}); + +const ComponentDrag = ({ isVisible, isThemeLight }): JSX.Element | null => { + const classes = useStyles(); + const state = useSelector((store: RootState) => store.appState); + + const isFocus = (targetId: Number) => { + return state.canvasFocus.componentId === targetId ? true : false; + }; + + if (!isVisible) return null; + + return ( +
+
+

+ {state.projectType === 'Next.js' || state.projectType === 'Gatsby.js' + ? 'Pages' + : ''} +

+ + {state.components + .filter((comp) => state.rootComponents.includes(comp.id)) + .map((comp) => { + return ( + + ); + })} + +
+
+ ); +}; + +export default ComponentDrag; + diff --git a/app/src/components/left/ComponentsContainer.tsx b/app/src/components/left/ComponentsContainer.tsx index eb0600f9..d4e3c5fd 100644 --- a/app/src/components/left/ComponentsContainer.tsx +++ b/app/src/components/left/ComponentsContainer.tsx @@ -1,61 +1,61 @@ -import ComponentPanelItem from '../right/ComponentPanelItem'; -import Grid from '@mui/material/Grid'; -import React from 'react'; -import { RootState } from '../../redux/store'; -import makeStyles from '@mui/styles/makeStyles'; -import { useSelector } from 'react-redux'; - -const ComponentsContainer = () => { - const classes = useStyles(); - const state = useSelector((store: RootState) => store.appState); - - const isFocus = (targetId: Number) => { - return state.canvasFocus.componentId === targetId ? true : false; - }; - return ( -
-
-
-

Reusable Components

- - {state.components - .filter((comp) => !state.rootComponents.includes(comp.id)) - .map((comp) => { - return ( - - ); - })} - -
-
-
- ); -}; - -const useStyles = makeStyles({ - panelWrapper: { - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - flexGrow: 1, - overflow: 'auto' - }, - panelWrapperList: { - minHeight: '120px' - }, - lightThemeFontColor: { - color: '#fff' - }, - darkThemeFontColor: { - color: '#fff' - } -}); - -export default ComponentsContainer; +import ComponentPanelItem from '../right/ComponentPanelItem'; +import Grid from '@mui/material/Grid'; +import React from 'react'; +import { RootState } from '../../redux/store'; +import makeStyles from '@mui/styles/makeStyles'; +import { useSelector } from 'react-redux'; + +const ComponentsContainer = () => { + const classes = useStyles(); + const state = useSelector((store: RootState) => store.appState); + + const isFocus = (targetId: Number) => { + return state.canvasFocus.componentId === targetId ? true : false; + }; + return ( +
+
+
+

Reusable Components

+ + {state.components + .filter((comp) => !state.rootComponents.includes(comp.id)) + .map((comp) => { + return ( + + ); + })} + +
+
+
+ ); +}; + +const useStyles = makeStyles({ + panelWrapper: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + flexGrow: 1, + overflow: 'auto' + }, + panelWrapperList: { + minHeight: 'auto' + }, + lightThemeFontColor: { + color: '#fff' + }, + darkThemeFontColor: { + color: '#fff' + } +}); + +export default ComponentsContainer; diff --git a/app/src/components/left/DragDropPanel.tsx b/app/src/components/left/DragDropPanel.tsx index 97aa8835..6bb98eac 100644 --- a/app/src/components/left/DragDropPanel.tsx +++ b/app/src/components/left/DragDropPanel.tsx @@ -1,128 +1,190 @@ -import { useDispatch, useSelector } from 'react-redux'; -import Grid from '@mui/material/Grid'; -import HTMLItem from './HTMLItem'; -import React from 'react'; -import { RootState } from '../../redux/store'; -import { deleteElement } from '../../redux/reducers/slice/appStateSlice'; -import { emitEvent } from '../../helperFunctions/socket'; - -/* -DESCRIPTION: This is the top half of the left panel, starting from the 'HTML - Elements' header. The boxes containing each HTML element are rendered in - HTMLItem, which itself is rendered by this component. - -Central state contains all available HTML elements (stored in the HTMLTypes property). - The data for HTMLTypes is stored in HTMLTypes.tsx and is added to central state in - initialState.tsx. - -Hook state: - -tag: -*/ -// Extracted the drag and drop functionality from HTMLPanel to make a modular component that can hang wherever the future designers may choose. -const DragDropPanel = (props): JSX.Element => { - const dispatch = useDispatch(); - - const state = useSelector((store: RootState) => store.appState); // current state - const contextParam = useSelector((store: RootState) => store.contextSlice); // current contextParam - const roomCode = useSelector((store: RootState) => store.roomSlice.roomCode); // current roomCode - - // function for delete element - const handleDelete = (id: number): void => { - dispatch(deleteElement({ id: id, contextParam: contextParam })); - if (roomCode) { - // send delete element to server to broadcast to all users - emitEvent('deleteElementAction', roomCode, { - id, - contextParam - }); - } - }; - - // filter out separator so that it will not appear on the html panel - const htmlTypesToRender = state.HTMLTypes.filter( - (type) => type.name !== 'separator' - ); - return ( -
-
-

HTML Elements

- - {htmlTypesToRender.map((option) => { - if ( - !['Switch', 'LinkTo', 'LinkHref', 'Image', 'Route'].includes( - option.name - ) - ) { - return ( - - ); - } - })} - - - {state.projectType === 'Classic React' ? ( -

React Router

- ) : null} - - {htmlTypesToRender.map((option) => { - if ( - (option.name === 'Switch' || - option.name === 'LinkTo' || - option.name === 'Route') && - state.projectType === 'Classic React' - ) { - return ( - - ); - } - })} - - - {/* Next.js */} - {state.projectType === 'Next.js' ? ( -

Next.js

- ) : null} - {htmlTypesToRender.map((option) => { - if ( - option.framework === 'nextjs' && - state.projectType === 'Next.js' - ) { - return ( - - ); - } - })} -
-
- ); -}; - -export default DragDropPanel; +import { useDispatch, useSelector } from 'react-redux'; +import Grid from '@mui/material/Grid'; +import HTMLItem from './HTMLItem'; +import MUIItem from './MUIItem'; +import React from 'react'; +import { RootState } from '../../redux/store'; +import { deleteElement } from '../../redux/reducers/slice/appStateSlice'; +import { emitEvent } from '../../helperFunctions/socket'; +import Accordion from '@mui/material/Accordion'; +import AccordionSummary from '@mui/material/AccordionSummary'; +import AccordionDetails from '@mui/material/AccordionDetails'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import { makeStyles } from '@mui/styles'; +import ComponentDrag from './ComponentDrag'; + +const useStyles = makeStyles({ + accordion: { + backgroundColor: '#000000', // Set the background color to gray + color: '#ffffff' // Set the text color to white + }, + accordionSummary: { + backgroundColor: '#000000', // Set the background color of the summary to gray + color: '#ffffff' // Set the text color of the summary to white + } +}); + +const DragDropPanel = (props): JSX.Element => { + const classes = useStyles(); + const dispatch = useDispatch(); + + const state = useSelector((store: RootState) => store.appState); + const contextParam = useSelector((store: RootState) => store.contextSlice); + const roomCode = useSelector((store: RootState) => store.roomSlice.roomCode); + + const handleDelete = (id: number): void => { + dispatch(deleteElement({ id: id, contextParam: contextParam })); + if (roomCode) { + emitEvent('deleteElementAction', roomCode, { + id, + contextParam + }); + } + }; + + const htmlTypesToRender = state.HTMLTypes.filter( + (type) => type.name !== 'separator' + ); + + const muiTypesToRender = state.MUITypes.filter( + (type) => type.name !== 'separator' + ); + + return ( +
+
+ {/* Root Components */} + + } + aria-controls="panel1a-content" + id="panel1a-header" + className={classes.accordionSummary} + > +

Root Components

+
+ + + +
+ + {/* HTML Components */} + + } + aria-controls="panel1a-content" + id="panel1a-header" + className={classes.accordionSummary} + > +

HTML Elements

+
+ + + {htmlTypesToRender.map((option) => { + if ( + !['Switch', 'LinkTo', 'LinkHref', 'Image', 'Route'].includes( + option.name + ) + ) { + return ( + + ); + } + })} + + +
+ + {/* MUI Components */} + + } + aria-controls="panel2a-content" + id="panel2a-header" + className={classes.accordionSummary} + > +

MUI Components

+
+ + + {muiTypesToRender.map((option) => { + return ( + + ); + })} + + +
+ + {/* React Router */} + + } + aria-controls="panel1a-content" + id="panel1a-header" + className={classes.accordionSummary} + > +

React Router

+
+ + + {htmlTypesToRender.map((option) => { + if ( + (option.name === 'Switch' || + option.name === 'LinkTo' || + option.name === 'Route') && + state.projectType === 'Classic React' + ) { + return ( + + ); + } + })} + + +
+ + {/* Next.js */} + {state.projectType === 'Next.js' ? ( +

Next.js

+ ) : null} + {htmlTypesToRender.map((option) => { + if ( + option.framework === 'nextjs' && + state.projectType === 'Next.js' + ) { + return ( + + ); + } + })} +
+
+ ); +}; + +export default DragDropPanel; diff --git a/app/src/components/left/ElementsContainer.tsx b/app/src/components/left/ElementsContainer.tsx index 6d95bcf1..8680937c 100644 --- a/app/src/components/left/ElementsContainer.tsx +++ b/app/src/components/left/ElementsContainer.tsx @@ -1,65 +1,68 @@ -import { Box } from '@mui/material'; -import React, { useCallback, useEffect } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; - -import ComponentDrag from './ComponentDrag'; -import DragDropPanel from './DragDropPanel'; - -import { deleteChild } from '../../redux/reducers/slice/appStateSlice'; -import { RootState } from '../../redux/store'; -import { emitEvent } from '../../helperFunctions/socket'; - -// Left-hand portion of the app, where predefined component options are displayed -const ElementsContainer = (props): JSX.Element => { - const contextParam = useSelector((store: RootState) => store.contextSlice); - const roomCode = useSelector((store: RootState) => store.roomSlice.roomCode); - - const dispatch = useDispatch(); - - const handleDelete = () => { - dispatch(deleteChild({ id: {}, contextParam: contextParam })); - if (roomCode) { - emitEvent('deleteChildAction', roomCode, { - id: {}, - contextParam: contextParam - }); - } - }; - - const keyBindedFunc = useCallback((e) => { - if ( - e.key === 'Backspace' && - e.target.tagName !== 'TEXTAREA' && - e.target.tagName !== 'INPUT' - ) - handleDelete(); - }, []); - - useEffect(() => { - document.addEventListener('keydown', keyBindedFunc); - return () => { - document.removeEventListener('keydown', keyBindedFunc); - }; - }, []); - - return ( - - {' '} - -
- -
-
- ); -}; - -export default ElementsContainer; +import { Box } from '@mui/material'; +import React, { useCallback, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; + +import ComponentDrag from './ComponentDrag'; +import DragDropPanel from './DragDropPanel'; + +import { deleteChild } from '../../redux/reducers/slice/appStateSlice'; +import { RootState } from '../../redux/store'; +import { emitEvent } from '../../helperFunctions/socket'; + +// Left-hand portion of the app, where predefined component options are displayed +const ElementsContainer = (props): JSX.Element => { + const contextParam = useSelector((store: RootState) => store.contextSlice); + const roomCode = useSelector((store: RootState) => store.roomSlice.roomCode); + + const dispatch = useDispatch(); + + const handleDelete = () => { + dispatch(deleteChild({ id: {}, contextParam: contextParam })); + if (roomCode) { + emitEvent('deleteChildAction', roomCode, { + id: {}, + contextParam: contextParam + }); + } + }; + + const keyBindedFunc = useCallback((e) => { + if ( + e.key === 'Backspace' && + e.target.tagName !== 'TEXTAREA' && + e.target.tagName !== 'INPUT' + ) + handleDelete(); + }, []); + + useEffect(() => { + document.addEventListener('keydown', keyBindedFunc); + return () => { + document.removeEventListener('keydown', keyBindedFunc); + }; + }, []); + + return ( + + {' '} + + { + // moved ComponentDrag to DragDropPanel + /*
+ +
*/ + } +
+ ); +}; + +export default ElementsContainer; diff --git a/app/src/components/left/HTMLItem.tsx b/app/src/components/left/HTMLItem.tsx index 17e73817..cbe65eef 100644 --- a/app/src/components/left/HTMLItem.tsx +++ b/app/src/components/left/HTMLItem.tsx @@ -1,186 +1,187 @@ -import React, { useState } from 'react'; -import Grid from '@mui/material/Grid'; -import { ItemTypes } from '../../constants/ItemTypes'; -import List from '@mui/material/List'; -import ListItem from '@mui/material/ListItem'; -import ListItemText from '@mui/material/ListItemText'; -import createModal from '../right/createModal'; -import makeStyles from '@mui/styles/makeStyles'; -import { useDrag } from 'react-dnd'; -import CodeIcon from '@mui/icons-material/Code'; -import * as Icons from '@mui/icons-material'; -import { useDispatch, useSelector } from 'react-redux'; -import { addChild } from '../../redux/reducers/slice/appStateSlice'; -import { emitEvent } from '../../helperFunctions/socket'; - -const useStyles = makeStyles({ - HTMLPanelItem: { - height: 'auto', - width: 'auto', - fontSize: 'medium', - display: 'flex', - flexDirection: 'row', - justifyContent: 'space-evenly', - textAlign: 'center', - cursor: 'grab' - }, - lightThemeFontColor: { - color: '#8F8F8F' - }, - darkThemeFontColor: { - color: '#8F8F8F' - } -}); - -const HTMLItem: React.FC<{ - name: string; - id: number; - icon: any; - handleDelete: (id: number) => void; -}> = ({ name, id, icon, handleDelete }) => { - const IconComponent = Icons[icon]; - - - const roomCode = useSelector((store: RootState) => store.roomSlice.roomCode); // current roomCode - - - const classes = useStyles(); - const [modal, setModal] = useState(null); - const [{ isDragging }, drag] = useDrag({ - item: { - type: ItemTypes.INSTANCE, - newInstance: true, - instanceType: 'HTML Element', - name, - icon, - instanceTypeId: id - }, - collect: (monitor: any) => ({ - isDragging: !!monitor.isDragging() - }) - }); - - const closeModal = () => setModal(null); - const deleteAllInstances = (deleteID: number) => { - const children = ( - - handleDelete(deleteID)} - style={{ - border: '1px solid #C6C6C6', - marginBottom: '2%', - marginTop: '5%' - }} - > - - - - - - - ); - setModal( - createModal({ - closeModal, - children, - message: - 'Deleting this element will delete all instances of this element within the application.\nDo you still wish to proceed?', - primBtnLabel: null, - primBtnAction: null, - secBtnAction: null, - secBtnLabel: null, - open: true - }) - ); - }; - - - const dispatch = useDispatch(); - - const handleClick = () => { - const childData = { - type: 'HTML Element', - typeId: id, - childId: null, - contextParam: { - allContext: [] - } - }; - - dispatch(addChild(childData)); - if (roomCode) { - // Emit 'addChildAction' event to the server - emitEvent('addChildAction', roomCode, childData); - } - }; - - // updated the id's to reflect the new element types input and label - - return ( - - {id <= 20 && ( -
{ - handleClick(); - }} - > - {typeof IconComponent !== 'undefined' && ( - - )} - {name} -
- )} - - {id > 20 && ( -
{ - handleClick(); - }} - > - {typeof CodeIcon !== 'undefined' && ( - - )} - {name} - -
- )} - {modal} -
- ); -}; - -export default HTMLItem; - +import React, { useState } from 'react'; +import Grid from '@mui/material/Grid'; +import { ItemTypes } from '../../constants/ItemTypes'; +import List from '@mui/material/List'; +import ListItem from '@mui/material/ListItem'; +import ListItemText from '@mui/material/ListItemText'; +import createModal from '../right/createModal'; +import makeStyles from '@mui/styles/makeStyles'; +import { useDrag } from 'react-dnd'; +import CodeIcon from '@mui/icons-material/Code'; +import * as Icons from '@mui/icons-material'; +import { useDispatch, useSelector } from 'react-redux'; +import { addChild } from '../../redux/reducers/slice/appStateSlice'; +import { emitEvent } from '../../helperFunctions/socket'; +import { RootState } from '../../redux/store'; + +const useStyles = makeStyles({ + HTMLPanelItem: { + height: 'auto', + width: 'auto', + fontSize: 'small', + alignItems: 'center', + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-evenly', + textAlign: 'center', + cursor: 'grab' + }, + lightThemeFontColor: { + color: '#8F8F8F' + }, + darkThemeFontColor: { + color: '#8F8F8F' + } +}); + +const HTMLItem: React.FC<{ + name: string; + id: number; + icon: any; + handleDelete: (id: number) => void; +}> = ({ name, id, icon, handleDelete }) => { + const IconComponent = Icons[icon]; + + const roomCode = useSelector((store: RootState) => store.roomSlice.roomCode); // current roomCode + + const classes = useStyles(); + const [modal, setModal] = useState(null); + const [{ isDragging }, drag] = useDrag({ + item: { + type: ItemTypes.INSTANCE, + newInstance: true, + instanceType: 'HTML Element', + name, + icon, + instanceTypeId: id + }, + collect: (monitor: any) => ({ + isDragging: !!monitor.isDragging() + }) + }); + + const closeModal = () => setModal(null); + const deleteAllInstances = (deleteID: number) => { + const children = ( + + handleDelete(deleteID)} + style={{ + border: '1px solid #C6C6C6', + marginBottom: '2%', + marginTop: '5%' + }} + > + + + + + + + ); + setModal( + createModal({ + closeModal, + children, + message: + 'Deleting this element will delete all instances of this element within the application.\nDo you still wish to proceed?', + primBtnLabel: null, + primBtnAction: null, + secBtnAction: null, + secBtnLabel: null, + open: true + }) + ); + }; + + const dispatch = useDispatch(); + + const handleClick = () => { + const childData = { + type: 'HTML Element', + typeId: id, + childId: null, + contextParam: { + allContext: [] + } + }; + + dispatch(addChild(childData)); + if (roomCode) { + // Emit 'addChildAction' event to the server + emitEvent('addChildAction', roomCode, childData); + } + }; + + // updated the id's to reflect the new element types input and label + + return ( + + {id <= 20 && ( +
{ + handleClick(); + }} + > + {typeof IconComponent !== 'undefined' && ( + + )} + {name} +
+ )} + + {id > 20 && ( +
{ + handleClick(); + }} + > + {typeof CodeIcon !== 'undefined' && ( + + )} + {name} + +
+ )} + {modal} +
+ ); +}; + +export default HTMLItem; diff --git a/app/src/components/left/MUIItem.tsx b/app/src/components/left/MUIItem.tsx new file mode 100644 index 00000000..b91a59f7 --- /dev/null +++ b/app/src/components/left/MUIItem.tsx @@ -0,0 +1,195 @@ +import React, { useState } from 'react'; +import Grid from '@mui/material/Grid'; +import List from '@mui/material/List'; +import ListItem from '@mui/material/ListItem'; +import ListItemText from '@mui/material/ListItemText'; +import makeStyles from '@mui/styles/makeStyles'; +import { useDrag } from 'react-dnd'; + +import { ItemTypes } from '../../constants/ItemTypes'; +import { RootState } from '../../redux/store'; +import * as Icons from '@mui/icons-material'; // Assuming a collection of MUI icons +import CodeIcon from '@mui/icons-material/Code'; // Default icon if specific icon not provided +import { useDispatch, useSelector } from 'react-redux'; +import { addChild } from '../../redux/reducers/slice/appStateSlice'; +import createModal from '../right/createModal'; // Modal creation utility +import { emitEvent } from '../../helperFunctions/socket'; // Event emission utility + +// Define component styles using MUI styling solutions +const useStyles = makeStyles({ + MUIPanelItem: { + height: 'auto', + width: 'auto', + fontSize: 'small', + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-evenly', + alignItems: 'center', + textAlign: 'center', + cursor: 'grab' + }, + lightThemeFontColor: { + color: '#8F8F8F' + }, + darkThemeFontColor: { + color: '#8F8F8F' + } +}); + +const MUIItem: React.FC<{ + name: string; + id: number; + icon: any; + handleDelete: (id: number) => void; +}> = ({ name, id, icon, handleDelete }) => { + const IconComponent = Icons[icon]; + + const roomCode = useSelector((store: RootState) => store.roomSlice.roomCode); // current roomCode + + // Use drag and drop functionality + const classes = useStyles(); + const [modal, setModal] = useState(null); + + const item = { + type: ItemTypes.INSTANCE, + newInstance: true, + instanceType: 'MUI Component', // MUI Element? - we should carefully consider what we call this + name, + icon, + instanceTypeId: id + }; + + // console.log('draggable item', item); + + const [{ isDragging }, drag] = useDrag({ + item, + collect: (monitor: any) => ({ + isDragging: !!monitor.isDragging() + }) + }); + + const closeModal = () => setModal(null); + const deleteAllInstances = (deleteID: number) => { + const children = ( + + handleDelete(deleteID)} + style={{ + border: '1px solid #C6C6C6', + marginBottom: '2%', + marginTop: '5%' + }} + > + + + + + + + ); + setModal( + createModal({ + closeModal, + children, + message: + 'Deleting this element will delete all instances of this element within the application.\nDo you still wish to proceed?', + primBtnLabel: null, + primBtnAction: null, + secBtnAction: null, + secBtnLabel: null, + open: true + }) + ); + }; + + const dispatch = useDispatch(); + + const handleClick = () => { + const childData = { + type: 'MUI Component', + typeId: id, + childId: null, + contextParam: { + allContext: [] + } + }; + + dispatch(addChild(childData)); + if (roomCode) { + // Emit 'addChildAction' event to the server + emitEvent('addChildAction', roomCode, childData); + } + }; + + // id over/under 20 logic + // html-g{name} - html grid name = item + return ( + + {id >= 20 && ( +
{ + handleClick(); + }} + > + {typeof IconComponent !== 'undefined' && ( + + )} + {name} +
+ )} + + {id < 20 && ( +
{ + handleClick(); + }} + > + {typeof CodeIcon !== 'undefined' && ( + + )} + {name} + +
+ )} + {modal} +
+ ); +}; + +export default MUIItem; diff --git a/app/src/components/main/Canvas.tsx b/app/src/components/main/Canvas.tsx index 1610cd08..3936c3d2 100644 --- a/app/src/components/main/Canvas.tsx +++ b/app/src/components/main/Canvas.tsx @@ -1,389 +1,390 @@ -import { Component, DragItem } from '../../interfaces/Interfaces'; -import { DropTargetMonitor, useDrop } from 'react-dnd'; -import React, { useEffect, useState, forwardRef } from 'react'; -import { - addChild, - changeFocus, - snapShotAction -} from '../../redux/reducers/slice/appStateSlice'; -import { useDispatch, useSelector } from 'react-redux'; -import { debounce, throttle } from 'lodash'; - -import Arrow from './Arrow'; -import { ItemTypes } from '../../constants/ItemTypes'; -import { RootState } from '../../redux/store'; -import { combineStyles } from '../../helperFunctions/combineStyles'; -import renderChildren from '../../helperFunctions/renderChildren'; -import { emitEvent, getSocket } from '../../helperFunctions/socket'; -import { FaMousePointer } from 'react-icons/fa'; -import { display } from 'html2canvas/dist/types/css/property-descriptors/display'; -import { ZoomIn, ZoomOut } from '@mui/icons-material'; -import { Button } from '@mui/material'; - -interface CanvasProps { - zoom: number; -} - -const Canvas = forwardRef(({ zoom }, ref) => { - const state = useSelector((store: RootState) => store.appState); - const contextParam = useSelector((store: RootState) => store.contextSlice); - const roomCode = useSelector((store: RootState) => store.roomSlice.roomCode); - const userName = useSelector((store: RootState) => store.roomSlice.userName); - const userList = useSelector((store: RootState) => store.roomSlice.userList); - - //remote cursor data - const [remoteCursors, setRemoteCursors] = useState([]); - - // Toggle switch for live cursor tracking - const [toggleSwitch, setToggleSwitch] = useState(true); - - // Toggle button text for live cursor tracking button - on/off - const [toggleText, setToggleText] = useState('off'); - const toggleButton = () => { - setToggleText(toggleText === 'on' ? 'off' : 'on'); - }; - - // Prevents lagging and provides smoother user experience got live cursor tracking (milliseconds can be adjusted but 500ms is most optimal) - const debounceSetPosition = debounce((newX, newY) => { - //emit socket event every 300ms when cursor moves - if (userList.length > 1) - emitEvent('cursorData', roomCode, { x: newX, y: newY, userName }); - }, 500); - - const handleMouseMove = (e) => { - debounceSetPosition(e.clientX, e.clientY); - }; - - const handleCursorDataFromServer = (remoteData) => { - setRemoteCursors((prevState) => { - //check if received cursor data is from an existing user in the room - const cursorIdx = prevState.findIndex( - (cursor) => cursor.remoteUserName === remoteData.userName - ); - //existing user - if (cursorIdx >= 0) { - //check if cursor position has changed - if ( - prevState[cursorIdx].x !== remoteData.x || - prevState[cursorIdx].y !== remoteData.y - ) { - //update existing user's cursor position - const updatedCursors = [...prevState]; - updatedCursors[cursorIdx] = { - ...prevState[cursorIdx], - x: remoteData.x, - y: remoteData.y - }; - return updatedCursors; - } else { - //return previous state if no change - return prevState; - } - } else { - //new user: add new user's cursor - return [ - ...prevState, - { - x: remoteData.x, - y: remoteData.y, - remoteUserName: remoteData.userName, - isVisible: true - } - ]; - } - }); - }; - - // Removes the mouse cursor of the user that leaves the collaboration room. - const handleCursorDeleteFromServer = () => { - setRemoteCursors((prevRemoteCursors) => - // filter cursors to include only those in the userList - prevRemoteCursors.filter((cursor) => - userList.includes(cursor.remoteUserName) - ) - ); - }; - - // Function that will turn on/off socket depending on toggle Switch. - const handleToggleSwitch = () => { - setToggleSwitch(!toggleSwitch); - //checks the state before it's updated so need to check the opposite condition - if (toggleSwitch) { - socket.off('remote cursor data from server'); - //make remote cursor invisible - setRemoteCursors((prevState) => { - const newState = prevState.map((cursor) => ({ - ...cursor, - isVisible: false - })); - return newState; - }); - } else { - socket.on('remote cursor data from server', (remoteData) => - handleCursorDataFromServer(remoteData) - ); - //make remote cursor visible - setRemoteCursors((prevState) => - prevState.map((cursor) => ({ - ...cursor, - isVisible: true - })) - ); - } - }; - - //Function to handle the multiple click events of the toggle button. - const multipleClicks = () => { - handleToggleSwitch(); - toggleButton(); - }; - - const socket = getSocket(); - //wrap the socket event listener in useEffect with dependency array as [socket], so the the effect will run only when: 1. After the initial rendering of the component 2. Every time the socket instance changes(connect, disconnect) - useEffect(() => { - if (socket) { - socket.on('remote cursor data from server', (remoteData) => - handleCursorDataFromServer(remoteData) - ); - } - - return () => { - if (socket) socket.off('remote cursor data from server'); - }; - }, [socket]); - - useEffect(() => { - handleCursorDeleteFromServer(); - }, [userList]); - - // find the current component based on the canvasFocus component ID in the state - const currentComponent: Component = state.components.find( - (elem: Component) => elem.id === state.canvasFocus.componentId - ); - - Arrow.deleteLines(); - - const dispatch = useDispatch(); - // changes focus of the canvas to a new component / child - const changeFocusFunction = ( - componentId?: number, - childId?: number | null - ) => { - dispatch(changeFocus({ componentId, childId })); - //if room exists, send focus dispatch to all users - if (roomCode) { - emitEvent('changeFocusAction', roomCode, { - componentId: componentId, - childId: childId - }); - } - }; - - // onClickHandler is responsible for changing the focused component and child component - function onClickHandler(event: React.MouseEvent) { - event.stopPropagation(); - changeFocusFunction(state.canvasFocus.componentId, null); - } - - // stores a snapshot of state into the past array for UNDO. snapShotFunc is also invoked for nestable elements in DirectChildHTMLNestable.tsx - const snapShotFunc = () => { - // make a deep clone of state - const deepCopiedState = JSON.parse(JSON.stringify(state)); - const focusIndex = state.canvasFocus.componentId - 1; - dispatch( - snapShotAction({ - focusIndex: focusIndex, - deepCopiedState: deepCopiedState - }) - ); - }; - - // This hook will allow the user to drag items from the left panel on to the canvas - const [{ isOver }, drop] = useDrop({ - accept: ItemTypes.INSTANCE, - drop: (item: DragItem, monitor: DropTargetMonitor) => { - const didDrop = monitor.didDrop(); - // takes a snapshot of state to be used in UNDO and REDO cases - snapShotFunc(); - // returns false for direct drop target - if (didDrop) { - return; - } - // if item dropped is going to be a new instance (i.e. it came from the left panel), then create a new child component - if (item.newInstance && item.instanceType !== 'Component') { - dispatch( - //update state - addChild({ - type: item.instanceType, - typeId: item.instanceTypeId, - childId: null, - contextParam: contextParam - }) - ); - - //emit the socket event - if (roomCode) { - emitEvent('addChildAction', roomCode, { - type: item.instanceType, - typeId: item.instanceTypeId, - childId: null, - contextParam: contextParam - }); - } - } else if (item.newInstance && item.instanceType === 'Component') { - let hasDiffParent = false; - const components = state.components; - let newChildName = ''; - // loop over components array - for (let i = 0; i < components.length; i++) { - const comp = components[i]; - //loop over each componenets child - for (let j = 0; j < comp.children.length; j++) { - const child = comp.children[j]; - if (child.name === 'separator') continue; - // check if the item.instanceTypeId matches and child ID - if (item.instanceTypeId === child.typeId) { - // check if the name of the parent matches the canvas focus name - // comp is the parent component - // currentComponent is the canvas.focus component - if (comp.name === currentComponent.name) { - i = components.length; - break; - } else { - // if false - // setCopiedComp(child); - hasDiffParent = true; - newChildName = child.name; - i = components.length; - break; - } - } - } - } - // if (!hasDiffParent) { - dispatch( - addChild({ - type: item.instanceType, - typeId: item.instanceTypeId, - childId: null, - contextParam: contextParam - }) - ); - if (roomCode) { - emitEvent('addChildAction', roomCode, { - type: item.instanceType, - typeId: item.instanceTypeId, - childId: null, - contextParam: contextParam - }); - } - } - }, - collect: (monitor) => ({ - isOver: !!monitor.isOver() - }) - }); - - // Styling for Canvas - const defaultCanvasStyle: React.CSSProperties = { - width: '100%', - minHeight: '100%', - aspectRatio: 'auto 774 / 1200', - boxSizing: 'border-box', - transform: `scale(${zoom})`, - transformOrigin: 'top center' - }; - - // Combine the default styles of the canvas with the custom styles set by the user for that component - // The renderChildren function renders all direct children of a given component - // Direct children are draggable/clickable - - const canvasStyle: React.CSSProperties = combineStyles( - defaultCanvasStyle, - currentComponent.style - ); - - // Array of colors that color code users as they join the room (In a set order) - const userColors = [ - '#0671e3', - '#2fd64d', - '#f0c000', - '#fb4c64', - '#be5be8', - '#fe9c06', - '#f6352b', - '#1667d1', - '#1667d1', - '#50ed6a' - ]; - - const buttonStyle: React.CSSProperties = { - textAlign: 'center', - color: '#ffffff', - backgroundColor: '#151515', - zIndex: 0, - border: '2px solid #0671e3', - margin: '8px 0 0 8px' - }; - - return ( -
- {renderChildren(currentComponent.children)} - {remoteCursors.map( - (cursor, idx) => - cursor.isVisible && ( -
- {} - {cursor.remoteUserName} -
- ) - )} - -
- ); -}); - -export default Canvas; +import { Component, DragItem } from '../../interfaces/Interfaces'; +import { DropTargetMonitor, useDrop } from 'react-dnd'; +import React, { useEffect, useState, forwardRef } from 'react'; +import { + addChild, + changeFocus, + snapShotAction +} from '../../redux/reducers/slice/appStateSlice'; +import { useDispatch, useSelector } from 'react-redux'; +import { debounce, throttle } from 'lodash'; + +import Arrow from './Arrow'; +import { ItemTypes } from '../../constants/ItemTypes'; +import { RootState } from '../../redux/store'; +import { combineStyles } from '../../helperFunctions/combineStyles'; +import renderChildren from '../../helperFunctions/renderChildren'; +import { emitEvent, getSocket } from '../../helperFunctions/socket'; +import { FaMousePointer } from 'react-icons/fa'; +import { display } from 'html2canvas/dist/types/css/property-descriptors/display'; +import { ZoomIn, ZoomOut } from '@mui/icons-material'; +import { Button } from '@mui/material'; + +interface CanvasProps { + zoom: number; +} + +const Canvas = forwardRef(({ zoom }, ref) => { + const state = useSelector((store: RootState) => store.appState); + const contextParam = useSelector((store: RootState) => store.contextSlice); + const roomCode = useSelector((store: RootState) => store.roomSlice.roomCode); + const userName = useSelector((store: RootState) => store.roomSlice.userName); + const userList = useSelector((store: RootState) => store.roomSlice.userList); + + //remote cursor data + const [remoteCursors, setRemoteCursors] = useState([]); + + // Toggle switch for live cursor tracking + const [toggleSwitch, setToggleSwitch] = useState(true); + + // Toggle button text for live cursor tracking button - on/off + const [toggleText, setToggleText] = useState('off'); + const toggleButton = () => { + setToggleText(toggleText === 'on' ? 'off' : 'on'); + }; + + // Prevents lagging and provides smoother user experience got live cursor tracking (milliseconds can be adjusted but 500ms is most optimal) + const debounceSetPosition = debounce((newX, newY) => { + //emit socket event every 300ms when cursor moves + if (userList.length > 1) + emitEvent('cursorData', roomCode, { x: newX, y: newY, userName }); + }, 500); + + const handleMouseMove = (e) => { + debounceSetPosition(e.clientX, e.clientY); + }; + + const handleCursorDataFromServer = (remoteData) => { + setRemoteCursors((prevState) => { + //check if received cursor data is from an existing user in the room + const cursorIdx = prevState.findIndex( + (cursor) => cursor.remoteUserName === remoteData.userName + ); + //existing user + if (cursorIdx >= 0) { + //check if cursor position has changed + if ( + prevState[cursorIdx].x !== remoteData.x || + prevState[cursorIdx].y !== remoteData.y + ) { + //update existing user's cursor position + const updatedCursors = [...prevState]; + updatedCursors[cursorIdx] = { + ...prevState[cursorIdx], + x: remoteData.x, + y: remoteData.y + }; + return updatedCursors; + } else { + //return previous state if no change + return prevState; + } + } else { + //new user: add new user's cursor + return [ + ...prevState, + { + x: remoteData.x, + y: remoteData.y, + remoteUserName: remoteData.userName, + isVisible: true + } + ]; + } + }); + }; + + // Removes the mouse cursor of the user that leaves the collaboration room. + const handleCursorDeleteFromServer = () => { + setRemoteCursors((prevRemoteCursors) => + // filter cursors to include only those in the userList + prevRemoteCursors.filter((cursor) => + userList.includes(cursor.remoteUserName) + ) + ); + }; + + // Function that will turn on/off socket depending on toggle Switch. + const handleToggleSwitch = () => { + setToggleSwitch(!toggleSwitch); + //checks the state before it's updated so need to check the opposite condition + if (toggleSwitch) { + socket.off('remote cursor data from server'); + //make remote cursor invisible + setRemoteCursors((prevState) => { + const newState = prevState.map((cursor) => ({ + ...cursor, + isVisible: false + })); + return newState; + }); + } else { + socket.on('remote cursor data from server', (remoteData) => + handleCursorDataFromServer(remoteData) + ); + //make remote cursor visible + setRemoteCursors((prevState) => + prevState.map((cursor) => ({ + ...cursor, + isVisible: true + })) + ); + } + }; + + //Function to handle the multiple click events of the toggle button. + const multipleClicks = () => { + handleToggleSwitch(); + toggleButton(); + }; + + const socket = getSocket(); + //wrap the socket event listener in useEffect with dependency array as [socket], so the the effect will run only when: 1. After the initial rendering of the component 2. Every time the socket instance changes(connect, disconnect) + useEffect(() => { + if (socket) { + socket.on('remote cursor data from server', (remoteData) => + handleCursorDataFromServer(remoteData) + ); + } + + return () => { + if (socket) socket.off('remote cursor data from server'); + }; + }, [socket]); + + useEffect(() => { + handleCursorDeleteFromServer(); + }, [userList]); + + // find the current component based on the canvasFocus component ID in the state + const currentComponent: Component = state.components.find( + (elem: Component) => elem.id === state.canvasFocus.componentId + ); + + Arrow.deleteLines(); + + const dispatch = useDispatch(); + // changes focus of the canvas to a new component / child + const changeFocusFunction = ( + componentId?: number, + childId?: number | null + ) => { + dispatch(changeFocus({ componentId, childId })); + //if room exists, send focus dispatch to all users + if (roomCode) { + emitEvent('changeFocusAction', roomCode, { + componentId: componentId, + childId: childId + }); + } + }; + + // onClickHandler is responsible for changing the focused component and child component + function onClickHandler(event: React.MouseEvent) { + event.stopPropagation(); + changeFocusFunction(state.canvasFocus.componentId, null); + } + + // stores a snapshot of state into the past array for UNDO. snapShotFunc is also invoked for nestable elements in DirectChildHTMLNestable.tsx + const snapShotFunc = () => { + // make a deep clone of state + const deepCopiedState = JSON.parse(JSON.stringify(state)); + const focusIndex = state.canvasFocus.componentId - 1; + dispatch( + snapShotAction({ + focusIndex: focusIndex, + deepCopiedState: deepCopiedState + }) + ); + }; + + // This hook will allow the user to drag items from the left panel on to the canvas + const [{ isOver }, drop] = useDrop({ + accept: ItemTypes.INSTANCE, + drop: (item: DragItem, monitor: DropTargetMonitor) => { + const didDrop = monitor.didDrop(); + // takes a snapshot of state to be used in UNDO and REDO cases + snapShotFunc(); + // returns false for direct drop target + if (didDrop) { + return; + } + // if item dropped is going to be a new instance (i.e. it came from the left panel), then create a new child component + if (item.newInstance && item.instanceType !== 'Component') { + console.log('inside not component check', item); + dispatch( + //update state + addChild({ + type: item.instanceType, + typeId: item.instanceTypeId, + childId: null, + contextParam: contextParam + }) + ); + + //emit the socket event + if (roomCode) { + emitEvent('addChildAction', roomCode, { + type: item.instanceType, + typeId: item.instanceTypeId, + childId: null, + contextParam: contextParam + }); + } + } else if (item.newInstance && item.instanceType === 'Component') { + let hasDiffParent = false; + const components = state.components; + let newChildName = ''; + // loop over components array + for (let i = 0; i < components.length; i++) { + const comp = components[i]; + //loop over each componenets child + for (let j = 0; j < comp.children.length; j++) { + const child = comp.children[j]; + if (child.name === 'separator') continue; + // check if the item.instanceTypeId matches and child ID + if (item.instanceTypeId === child.typeId) { + // check if the name of the parent matches the canvas focus name + // comp is the parent component + // currentComponent is the canvas.focus component + if (comp.name === currentComponent.name) { + i = components.length; + break; + } else { + // if false + // setCopiedComp(child); + hasDiffParent = true; + newChildName = child.name; + i = components.length; + break; + } + } + } + } + // if (!hasDiffParent) { + dispatch( + addChild({ + type: item.instanceType, + typeId: item.instanceTypeId, + childId: null, + contextParam: contextParam + }) + ); + if (roomCode) { + emitEvent('addChildAction', roomCode, { + type: item.instanceType, + typeId: item.instanceTypeId, + childId: null, + contextParam: contextParam + }); + } + } + }, + collect: (monitor) => ({ + isOver: !!monitor.isOver() + }) + }); + + // Styling for Canvas + const defaultCanvasStyle: React.CSSProperties = { + width: '100%', + minHeight: '100%', + aspectRatio: 'auto 774 / 1200', + boxSizing: 'border-box', + transform: `scale(${zoom})`, + transformOrigin: 'top center' + }; + + // Combine the default styles of the canvas with the custom styles set by the user for that component + // The renderChildren function renders all direct children of a given component + // Direct children are draggable/clickable + + const canvasStyle: React.CSSProperties = combineStyles( + defaultCanvasStyle, + currentComponent.style + ); + + // Array of colors that color code users as they join the room (In a set order) + const userColors = [ + '#0671e3', + '#2fd64d', + '#f0c000', + '#fb4c64', + '#be5be8', + '#fe9c06', + '#f6352b', + '#1667d1', + '#1667d1', + '#50ed6a' + ]; + + const buttonStyle: React.CSSProperties = { + textAlign: 'center', + color: '#ffffff', + backgroundColor: '#151515', + zIndex: 0, + border: '2px solid #0671e3', + margin: '8px 0 0 8px' + }; + + return ( +
+ {renderChildren(currentComponent.children)} + {remoteCursors.map( + (cursor, idx) => + cursor.isVisible && ( +
+ {} + {cursor.remoteUserName} +
+ ) + )} + +
+ ); +}); + +export default Canvas; diff --git a/app/src/components/main/DeleteButton.tsx b/app/src/components/main/DeleteButton.tsx index 5e3ae0e2..f9e5027a 100644 --- a/app/src/components/main/DeleteButton.tsx +++ b/app/src/components/main/DeleteButton.tsx @@ -1,47 +1,49 @@ -import React from 'react'; -import { DeleteButtons } from '../../interfaces/Interfaces'; -import { useDispatch, useSelector } from 'react-redux'; -import { deleteChild } from '../../redux/reducers/slice/appStateSlice'; -import { RootState } from '../../redux/store'; -import { emitEvent } from '../../helperFunctions/socket'; -import { Clear } from '@mui/icons-material'; - -function DeleteButton({ id, name, onClickHandler }: DeleteButtons) { - const contextParam = useSelector((store: RootState) => store.contextSlice); - - const roomCode = useSelector((store: RootState) => store.roomSlice.roomCode); - - const dispatch = useDispatch(); - - const deleteHTMLtype = (id: number) => { - dispatch(deleteChild({ id: id, contextParam: contextParam })); - if (roomCode) { - emitEvent('deleteChildAction', roomCode, { - id, - contextParam - }); - - // console.log( - // 'emit deleteChildAction event is triggered in DeleteButton.tsx' - // ); - } - }; - - return ( -
- -
- ); -} - -export default DeleteButton; +import React from 'react'; +import { DeleteButtons } from '../../interfaces/Interfaces'; +import { useDispatch, useSelector } from 'react-redux'; +import { deleteChild } from '../../redux/reducers/slice/appStateSlice'; +import { RootState } from '../../redux/store'; +import { emitEvent } from '../../helperFunctions/socket'; +import { Clear } from '@mui/icons-material'; + +function DeleteButton({ id, name, onClickHandler }: DeleteButtons) { + const contextParam = useSelector((store: RootState) => store.contextSlice); + + const roomCode = useSelector((store: RootState) => store.roomSlice.roomCode); + + const dispatch = useDispatch(); + + const deleteHTMLtype = (id: number) => { + dispatch(deleteChild({ id: id, contextParam: contextParam })); + if (roomCode) { + emitEvent('deleteChildAction', roomCode, { + id, + contextParam + }); + + // console.log( + // 'emit deleteChildAction event is triggered in DeleteButton.tsx' + // ); + } + }; + + return ( +
+ +
+ ); +} + +export default DeleteButton; diff --git a/app/src/components/main/DirectChildMUI.tsx b/app/src/components/main/DirectChildMUI.tsx new file mode 100644 index 00000000..934ef48a --- /dev/null +++ b/app/src/components/main/DirectChildMUI.tsx @@ -0,0 +1,90 @@ +import React from 'react'; +import { ChildElement, MUIType } from '../../interfaces/Interfaces'; +import { useDrag } from 'react-dnd'; +import { ItemTypes } from '../../constants/ItemTypes'; +import { combineStyles } from '../../helperFunctions/combineStyles'; +import globalDefaultStyle from '../../public/styles/globalDefaultStyles'; +import DeleteButton from './DeleteButton'; +import { useDispatch, useSelector } from 'react-redux'; +import { changeFocus } from '../../redux/reducers/slice/appStateSlice'; +import { RootState } from '../../redux/store'; +import { emitEvent } from '../../helperFunctions/socket'; + +function DirectChildMUI({ childId, name, type, typeId, style }: ChildElement) { + const state = useSelector((store: RootState) => store.appState); + + const roomCode = useSelector((store: RootState) => store.roomSlice.roomCode); + + const dispatch = useDispatch(); + + // find the MUI element corresponding with this instance of an MUI element + // find the current component to render on the canvas + const MUIType: MUIType = state.MUITypes.find( + (type: MUIType) => type.id === typeId + ); + // hook that allows component to be draggable + const [{ isDragging }, drag] = useDrag({ + // setting item attributes to be referenced when updating state with new instance of dragged item + item: { + type: ItemTypes.INSTANCE, + newInstance: false, + childId: childId, + instanceType: type, + instanceTypeId: typeId + }, + collect: (monitor: any) => ({ + isDragging: !!monitor.isDragging() + }) + }); + + const changeFocusFunction = (componentId: number, childId: number | null) => { + dispatch(changeFocus({ componentId, childId })); + if (roomCode) { + emitEvent('changeFocusAction', roomCode, { + componentId: componentId, + childId: childId + }); + // console.log('emit focus event from DirectChildMUI'); + } + }; + + // onClickHandler is responsible for changing the focused component and child component + function onClickHandler(event) { + event.stopPropagation(); + changeFocusFunction(state.canvasFocus.componentId, childId); + } + + // combine all styles so that higher priority style specifications overrule lower priority style specifications + // priority order is 1) style directly set for this child (style), 2) style of the referenced HTML element, and 3) default styling + const interactiveStyle = { + border: + state.canvasFocus.childId === childId + ? '2px solid #0671e3' + : '1px solid #31343A' + }; + + const combinedStyle = combineStyles( + combineStyles(combineStyles(globalDefaultStyle, MUIType.style), style), + interactiveStyle + ); + + return ( +
+ + {MUIType.placeHolderShort} + + +
+ ); +} + +export default DirectChildMUI; diff --git a/app/src/components/main/DirectChildMUINestable.tsx b/app/src/components/main/DirectChildMUINestable.tsx new file mode 100644 index 00000000..f30848cb --- /dev/null +++ b/app/src/components/main/DirectChildMUINestable.tsx @@ -0,0 +1,240 @@ +import React, { useRef } from 'react'; +import { ChildElement, MUIType } from '../../interfaces/Interfaces'; +import { useDrag, useDrop, DropTargetMonitor } from 'react-dnd'; +import { ItemTypes } from '../../constants/ItemTypes'; +import { combineStyles } from '../../helperFunctions/combineStyles'; +import globalDefaultStyle from '../../public/styles/globalDefaultStyles'; +import renderChildren from '../../helperFunctions/renderChildren'; +import DeleteButton from './DeleteButton'; +import validateNewParent from '../../helperFunctions/changePositionValidation'; +import componentNest from '../../helperFunctions/componentNestValidation'; +import AddRoute from './AddRoute'; +import AddLink from './AddLink'; +import { useDispatch, useSelector } from 'react-redux'; +import { RootState } from '../../redux/store'; +import { emitEvent } from '../../helperFunctions/socket'; + +import { + changeFocus, + changePosition, + addChild, + snapShotAction +} from '../../redux/reducers/slice/appStateSlice'; + +function DirectChildMUINestable({ + childId, + type, + typeId, + style, + children, + name, + attributes +}: ChildElement): React.JSX.Element { + const state = useSelector((store: RootState) => store.appState); + const contextParam = useSelector((store: RootState) => store.contextSlice); + + const dispatch = useDispatch(); + const roomCode = useSelector((store: RootState) => store.roomSlice.roomCode); + + const ref = useRef(null); + + // takes a snapshot of state to be used in UNDO and REDO cases. snapShotFunc is also invoked in Canvas.tsx + const snapShotFunc = () => { + //makes a deep clone of state + const deepCopiedState = JSON.parse(JSON.stringify(state)); + const focusIndex = state.canvasFocus.componentId - 1; + //pushes the last user action on the canvas into the past array of Component + dispatch( + snapShotAction({ + focusIndex: focusIndex, + deepCopiedState: deepCopiedState + }) + ); + }; + + // find the MUI element corresponding with this instance of an MUI element + // find the current component to render on the canvas + const MUIType: MUIType = state.MUITypes.find( + (type: MUIType) => type.id === typeId + ); + + // hook that allows component to be draggable + const [{ isDragging }, drag] = useDrag({ + // setting item attributes to be referenced when updating state with new instance of dragged item + item: { + type: ItemTypes.INSTANCE, + newInstance: false, + childId: childId, + instanceType: type, + instanceTypeId: typeId, + name: name + }, + canDrag: MUIType.id !== 1000, // dragging not permitted if element is separator + collect: (monitor: any) => { + return { + isDragging: !!monitor.isDragging() + }; + } + }); + + // both useDrop and useDrag used here to allow canvas components to be both a drop target and drag source + const [{ isOver }, drop] = useDrop({ + accept: ItemTypes.INSTANCE, + // triggered on drop + drop: (item: any, monitor: DropTargetMonitor) => { + const didDrop = monitor.didDrop(); + // takes a snapshot of state to be used in UNDO and REDO cases + snapShotFunc(); + if (didDrop) { + return; + } + // updates state with new instance + // if item dropped is going to be a new instance (i.e. it came from the left panel), then create a new child component + if (item.newInstance) { + if ( + (item.instanceType === 'Component' && + componentNest( + state.components[item.instanceTypeId - 1].children, + childId + )) || + item.instanceType !== 'Component' + ) { + dispatch( + addChild({ + type: item.instanceType, + typeId: item.instanceTypeId, + childId: childId, + contextParam: contextParam + }) + ); + if (roomCode) { + emitEvent('addChildAction', roomCode, { + type: item.instanceType, + typeId: item.instanceTypeId, + childId: childId, + contextParam: contextParam + }); + + // console.log( + // 'emit addChildAction event is triggered in DirectChildMUINestable' + // ); + } + } + } + // if item is not a new instance, change position of element dragged inside div so that the div is the new parent + else { + // check to see if the selected child is trying to nest within itself + if (validateNewParent(state, item.childId, childId) === true) { + dispatch( + changePosition({ + currentChildId: item.childId, + newParentChildId: childId, + contextParam: contextParam + }) + ); + if (roomCode) { + emitEvent('changePositionAction', roomCode, { + currentChildId: item.childId, + newParentChildId: childId, + contextParam: contextParam + }); + + // console.log( + // 'emit changePosition event is triggered in DirectChildMUINestable' + // ); + } + } + } + }, + + collect: (monitor: any) => { + return { + isOver: !!monitor.isOver({ shallow: true }) + }; + } + }); + + const changeFocusFunction = (componentId: number, childId: number | null) => { + dispatch(changeFocus({ componentId, childId })); + if (roomCode) { + emitEvent('changeFocusAction', roomCode, { + componentId: componentId, + childId: childId + }); + // console.log('emit focus event from DirectChildMUINestable'); + } + }; + + // onClickHandler is responsible for changing the focused component and child component + function onClickHandler(event) { + event.stopPropagation(); + changeFocusFunction(state.canvasFocus.componentId, childId); + } + + // combine all styles so that higher priority style specifications overrule lower priority style specifications + // priority order is 1) style directly set for this child (style), 2) style of the referenced MUI element, and 3) default styling + const defaultNestableStyle = { ...globalDefaultStyle }; + const interactiveStyle = { + border: + state.canvasFocus.childId === childId + ? '2px solid #0671e3' + : '1px solid #31343A' + }; + + // interactive style to change color when nested element is hovered over + if (isOver) defaultNestableStyle['#3c59ba']; + defaultNestableStyle['backgroundColor'] = isOver + ? '#3c59ba' + : defaultNestableStyle['backgroundColor']; + + const combinedStyle = combineStyles( + combineStyles(combineStyles(defaultNestableStyle, MUIType.style), style), + interactiveStyle + ); + + drag(drop(ref)); + + const routeButton = []; + if (typeId === 17) { + routeButton.push(); + } + if (typeId === 19) { + routeButton.push( + + ); + } + + return ( +
+ + {MUIType.placeHolderShort} + + {attributes && attributes.compLink ? ` ${attributes.compLink}` : ''} + + {routeButton} + + + {renderChildren(children)} +
+ ); +} + +export default DirectChildMUINestable; diff --git a/app/src/components/main/SeparatorChild.tsx b/app/src/components/main/SeparatorChild.tsx index 7caef1bd..f2910ebb 100644 --- a/app/src/components/main/SeparatorChild.tsx +++ b/app/src/components/main/SeparatorChild.tsx @@ -1,169 +1,177 @@ -import React, { useRef } from 'react'; -import { ChildElement, HTMLType, DragItem } from '../../interfaces/Interfaces'; -import { useDrag, useDrop, DropTargetMonitor } from 'react-dnd'; -import { ItemTypes } from '../../constants/ItemTypes'; -import { combineStyles } from '../../helperFunctions/combineStyles'; -import globalDefaultStyle from '../../public/styles/globalDefaultStyles'; -import renderChildren from '../../helperFunctions/renderChildren'; -import validateNewParent from '../../helperFunctions/changePositionValidation'; -import componentNest from '../../helperFunctions/componentNestValidation'; -import { useDispatch, useSelector } from 'react-redux'; -import { RootState } from '../../redux/store'; -import { - changeFocus, - changePosition, - addChild -} from '../../redux/reducers/slice/appStateSlice'; -import { emitEvent } from '../../helperFunctions/socket'; - -function SeparatorChild({ - childId, - type, - typeId, - style, - children -}: ChildElement): JSX.Element { - const state = useSelector((store: RootState) => store.appState); - const contextParam = useSelector((store: RootState) => store.contextSlice); - - const roomCode = useSelector((store: RootState) => store.roomSlice.roomCode); - - const dispatch = useDispatch(); - const ref = useRef(null); - - // find the HTML element corresponding with this instance of an HTML element - // find the current component to render on the canvas - const HTMLType: HTMLType = state.HTMLTypes.find( - (type: HTMLType) => type.id === typeId - ); - - // hook that allows component to be draggable - const [{ isDragging }, drag] = useDrag({ - // setting item attributes to be referenced when updating state with new instance of dragged item - item: { - type: ItemTypes.INSTANCE, - newInstance: false, - childId: childId, - instanceType: type, - instanceTypeId: typeId - }, - canDrag: HTMLType.id !== 1000, // dragging not permitted if element is separator - collect: (monitor: any) => { - return { - isDragging: !!monitor.isDragging() - }; - } - }); - - // both useDrop and useDrag used here to allow canvas components to be both a drop target and drag source - const [{ isOver }, drop] = useDrop({ - accept: ItemTypes.INSTANCE, - // triggered on drop - drop: (item: DragItem, monitor: DropTargetMonitor) => { - const didDrop = monitor.didDrop(); - if (didDrop) { - return; - } - // updates state with new instance - // if item dropped is going to be a new instance (i.e. it came from the left panel), then create a new child component - if (item.newInstance) { - if ( - (item.instanceType === 'Component' && - componentNest( - state.components[item.instanceTypeId - 1].children, - childId - )) || - item.instanceType !== 'Component' - ) { - dispatch( - addChild({ - type: item.instanceType, - typeId: item.instanceTypeId, - childId: childId, - contextParam: contextParam - }) - ); - if (roomCode) { - emitEvent('addChildAction', roomCode, { - type: item.instanceType, - typeId: item.instanceTypeId, - childId: childId, - contextParam: contextParam - }); - - // console.log( - // 'emit addChildAction event is triggered in SeparatorChild' - // ); - } - } - } - // if item is not a new instance, change position of element dragged inside separator so that separator is new parent (until replacement) - else { - // check to see if the selected child is trying to nest within itself - if (validateNewParent(state, item.childId, childId) === true) { - dispatch( - changePosition({ - currentChildId: item.childId, - newParentChildId: childId, - contextParam: contextParam - }) - ); - if (roomCode) { - emitEvent('changePositionAction', roomCode, { - currentChildId: item.childId, - newParentChildId: childId, - contextParam: contextParam - }); - - // console.log( - // 'emit changePosition event is triggered in SeparatorChild' - // ); - } - } - } - }, - - collect: (monitor: any) => { - return { - isOver: !!monitor.isOver({ shallow: true }) - }; - } - }); - - const changeFocusFunction = (componentId: number, childId: number | null) => { - dispatch(changeFocus({ componentId, childId })); - }; - - // onClickHandler is responsible for changing the focused component and child component - function onClickHandler(event) { - event.stopPropagation(); - changeFocusFunction(state.canvasFocus.componentId, childId); - } - - // combine all styles so that higher priority style specifications overrule lower priority style specifications - // priority order is 1) style directly set for this child (style), 2) style of the referenced HTML element, and 3) default styling - const defaultNestableStyle = { ...globalDefaultStyle }; - const separatorStyle = { - padding: isOver ? '40px 10px' : '2px 10px', - margin: '1px 10px', - transition: 'padding 1s ease-out' - }; - - defaultNestableStyle['backgroundColor'] = isOver - ? 'rgb(53, 78, 156)' - : 'rgba(0, 0, 255, 0.0)'; - - const combinedStyle = combineStyles( - combineStyles(combineStyles(defaultNestableStyle, HTMLType.style), style), - separatorStyle - ); - - drag(drop(ref)); - return ( -
- {renderChildren(children)} -
- ); -} - -export default SeparatorChild; +import React, { useRef } from 'react'; +import { + ChildElement, + HTMLType, + MUIType, + DragItem +} from '../../interfaces/Interfaces'; +import { useDrag, useDrop, DropTargetMonitor } from 'react-dnd'; +import { ItemTypes } from '../../constants/ItemTypes'; +import { combineStyles } from '../../helperFunctions/combineStyles'; +import globalDefaultStyle from '../../public/styles/globalDefaultStyles'; +import renderChildren from '../../helperFunctions/renderChildren'; +import validateNewParent from '../../helperFunctions/changePositionValidation'; +import componentNest from '../../helperFunctions/componentNestValidation'; +import { useDispatch, useSelector } from 'react-redux'; +import { RootState } from '../../redux/store'; +import { + changeFocus, + changePosition, + addChild +} from '../../redux/reducers/slice/appStateSlice'; +import { emitEvent } from '../../helperFunctions/socket'; + +function SeparatorChild({ + childId, + type, + typeId, + style, + children +}: ChildElement): JSX.Element { + const state = useSelector((store: RootState) => store.appState); + const contextParam = useSelector((store: RootState) => store.contextSlice); + + const roomCode = useSelector((store: RootState) => store.roomSlice.roomCode); + + const dispatch = useDispatch(); + const ref = useRef(null); + + // find the HTML element corresponding with this instance of an HTML element + // find the current component to render on the canvas + const HTMLType: HTMLType = state.HTMLTypes.find( + (type: HTMLType) => type.id === typeId + ); + + const MUIType: MUIType = state.MUITypes.find( + (type: MUIType) => type.id === typeId + ); + // hook that allows component to be draggable + const [{ isDragging }, drag] = useDrag({ + // setting item attributes to be referenced when updating state with new instance of dragged item + item: { + type: ItemTypes.INSTANCE, + newInstance: false, + childId: childId, + instanceType: type, + instanceTypeId: typeId + }, + canDrag: HTMLType.id !== 1000 || MUIType.id !== 1000, // dragging not permitted if element is separator + collect: (monitor: any) => { + return { + isDragging: !!monitor.isDragging() + }; + } + }); + + // both useDrop and useDrag used here to allow canvas components to be both a drop target and drag source + const [{ isOver }, drop] = useDrop({ + accept: ItemTypes.INSTANCE, + // triggered on drop + drop: (item: DragItem, monitor: DropTargetMonitor) => { + const didDrop = monitor.didDrop(); + if (didDrop) { + return; + } + // updates state with new instance + // if item dropped is going to be a new instance (i.e. it came from the left panel), then create a new child component + if (item.newInstance) { + if ( + (item.instanceType === 'Component' && + componentNest( + state.components[item.instanceTypeId - 1].children, + childId + )) || + item.instanceType !== 'Component' + ) { + dispatch( + addChild({ + type: item.instanceType, + typeId: item.instanceTypeId, + childId: childId, + contextParam: contextParam + }) + ); + if (roomCode) { + emitEvent('addChildAction', roomCode, { + type: item.instanceType, + typeId: item.instanceTypeId, + childId: childId, + contextParam: contextParam + }); + + // console.log( + // 'emit addChildAction event is triggered in SeparatorChild' + // ); + } + } + } + // if item is not a new instance, change position of element dragged inside separator so that separator is new parent (until replacement) + else { + // check to see if the selected child is trying to nest within itself + if (validateNewParent(state, item.childId, childId) === true) { + dispatch( + changePosition({ + currentChildId: item.childId, + newParentChildId: childId, + contextParam: contextParam + }) + ); + if (roomCode) { + emitEvent('changePositionAction', roomCode, { + currentChildId: item.childId, + newParentChildId: childId, + contextParam: contextParam + }); + + // console.log( + // 'emit changePosition event is triggered in SeparatorChild' + // ); + } + } + } + }, + + collect: (monitor: any) => { + return { + isOver: !!monitor.isOver({ shallow: true }) + }; + } + }); + + const changeFocusFunction = (componentId: number, childId: number | null) => { + dispatch(changeFocus({ componentId, childId })); + }; + + // onClickHandler is responsible for changing the focused component and child component + function onClickHandler(event) { + event.stopPropagation(); + changeFocusFunction(state.canvasFocus.componentId, childId); + } + + // combine all styles so that higher priority style specifications overrule lower priority style specifications + // priority order is 1) style directly set for this child (style), 2) style of the referenced HTML element, and 3) default styling + const defaultNestableStyle = { ...globalDefaultStyle }; + const separatorStyle = { + padding: isOver ? '40px 10px' : '2px 10px', + margin: '1px 10px', + transition: 'padding 1s ease-out' + }; + + defaultNestableStyle['backgroundColor'] = isOver + ? 'rgb(53, 78, 156)' + : 'rgba(0, 0, 255, 0.0)'; + + const combinedStyle = combineStyles( + combineStyles(combineStyles(defaultNestableStyle, HTMLType.style), style), + separatorStyle + ); + + drag(drop(ref)); + return ( +
+ {renderChildren(children)} +
+ ); +} + +export default SeparatorChild; diff --git a/app/src/components/right/ComponentPanelItem.tsx b/app/src/components/right/ComponentPanelItem.tsx index 861fd1c6..985a6713 100644 --- a/app/src/components/right/ComponentPanelItem.tsx +++ b/app/src/components/right/ComponentPanelItem.tsx @@ -1,102 +1,117 @@ -import React from 'react'; -import Grid from '@mui/material/Grid'; -import makeStyles from '@mui/styles/makeStyles'; -import { useDrag } from 'react-dnd'; -import { ItemTypes } from '../../constants/ItemTypes'; -import { useDispatch, useSelector } from 'react-redux'; -import { changeFocus } from '../../redux/reducers/slice/appStateSlice'; -import { RootState } from '../../redux/store'; -import { emitEvent } from '../../helperFunctions/socket'; -/* -DESCRIPTION: This component is each box beneath the 'HTML Elements' and - 'reusable components' (in classic React mode) headings. Drag-and-drop - functionality works only for reusable components. - - -root is a boolean reflecting whether a component is a root component (that is, a container) - -isFocus is boolean reflecting whether a component is the one currently displayed on the canvas -*/ -// ComponentPanelItem is a tile that represents a single component -const ComponentPanelItem: React.FC<{ - name: string; - id: number; - root: boolean; - isFocus: boolean; - isThemeLight: boolean; -}> = ({ name, id, root, isFocus, isThemeLight }) => { - const classes = useStyles(); - const state = useSelector((store: RootState) => store.appState); - const roomCode = useSelector((store: RootState) => store.roomSlice.roomCode); - const dispatch = useDispatch(); - - // useDrag hook allows components in left panel to be drag source - const [{ isDragging }, drag] = useDrag({ - item: { - type: ItemTypes.INSTANCE, - newInstance: true, - instanceType: 'Component', - instanceTypeId: id - }, - canDrag: !root && !isFocus, // dragging not permitted if component is root component or current component - collect: (monitor: any) => ({ - isDragging: !!monitor.isDragging() // !! converts an object to a boolean (i.e., if falsy, becomes false => !!0 === false) - }) - }); - - // when a component is clicked in the left panel, change canvas focus to that component - const handleClick = () => { - //LEGACY PD - dispatch(changeFocus({ componentId: id, childId: null })); - - if (roomCode) { - emitEvent('changeFocusAction', roomCode, { - componentId: id, - childId: null - }); - } - }; - - return ( - - {isFocus &&
} -
- {/* render element's name on the left panel*/} -

{name}

-
-
- ); -}; - -const useStyles = makeStyles({ - activeFocus: { - backgroundColor: 'rgba (0, 0, 0, 0.54)' //this doesnt do anything.... - }, - focusMark: { - backgroundColor: '#0671e3', - justifySelf: 'left', - width: '14px', - height: '14px', - borderRadius: '25px', - position: 'absolute' //so it doesn't cause the containing box to jiggle when selected due to change in size - }, - lightTheme: { - color: 'rgba (0, 0, 0, 0.54)' - }, - darkTheme: { - color: '#ffffff' - } -}); -export default ComponentPanelItem; +import React from 'react'; +import Grid from '@mui/material/Grid'; +import makeStyles from '@mui/styles/makeStyles'; +import { useDrag } from 'react-dnd'; +import { ItemTypes } from '../../constants/ItemTypes'; +import { useDispatch, useSelector } from 'react-redux'; +import { changeFocus } from '../../redux/reducers/slice/appStateSlice'; +import { RootState } from '../../redux/store'; +import { emitEvent } from '../../helperFunctions/socket'; +import * as Icons from '@mui/icons-material'; +/* +DESCRIPTION: This component is each box beneath the 'HTML Elements' and + 'reusable components' (in classic React mode) headings. Drag-and-drop + functionality works only for reusable components. + + -root is a boolean reflecting whether a component is a root component (that is, a container) + -isFocus is boolean reflecting whether a component is the one currently displayed on the canvas +*/ +// ComponentPanelItem is a tile that represents a single component +const ComponentPanelItem: React.FC<{ + name: string; + id: number; + root: boolean; + isFocus: boolean; + isThemeLight: boolean; +}> = ({ name, id, root, isFocus, isThemeLight }) => { + const classes = useStyles({}); + const state = useSelector((store: RootState) => store.appState); + const roomCode = useSelector((store: RootState) => store.roomSlice.roomCode); + const dispatch = useDispatch(); + + const IconComponent = Icons[name]; // Use the correct icon component based on the name + + // useDrag hook allows components in left panel to be drag source + const [{ isDragging }, drag] = useDrag({ + item: { + type: ItemTypes.INSTANCE, + newInstance: true, + instanceType: 'Component', + instanceTypeId: id + }, + canDrag: !root && !isFocus, // dragging not permitted if component is root component or current component + collect: (monitor: any) => ({ + isDragging: !!monitor.isDragging() // !! converts an object to a boolean (i.e., if falsy, becomes false => !!0 === false) + }) + }); + + // when a component is clicked in the left panel, change canvas focus to that component + const handleClick = () => { + //LEGACY PD + dispatch(changeFocus({ componentId: id, childId: null })); + + if (roomCode) { + emitEvent('changeFocusAction', roomCode, { + componentId: id, + childId: null + }); + } + }; + + return ( + + {isFocus &&
} +
+
+ {IconComponent && } +

+ {name} +

+
+
+
+ ); +}; + +const useStyles = makeStyles({ + nameContainer: { + display: 'flex', + alignItems: 'center' + }, + focusMark: { + border: '2px solid #0671e3', + borderRadius: '5%', + position: 'absolute', + top: '0', + left: '0', + right: '0', + bottom: '0' + }, + lightTheme: { + color: 'rgba (0, 0, 0, 0.54)' + }, + darkTheme: { + color: '#ffffff' + } +}); + +export default ComponentPanelItem; diff --git a/app/src/containers/CustomizationPanel.tsx b/app/src/containers/CustomizationPanel.tsx index db2883e1..49748638 100644 --- a/app/src/containers/CustomizationPanel.tsx +++ b/app/src/containers/CustomizationPanel.tsx @@ -1,1092 +1,1128 @@ -import { - Button, - Dialog, - DialogActions, - DialogContent, - DialogContentText, - DialogTitle, - FormControl, - List, - ListItem, - ListItemText, - TextField -} from '@mui/material'; -import { Redo, Undo } from '@mui/icons-material'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { - changeTailwind, - deleteChild, - deleteEventAction, - deletePage, - deleteReusableComponent, - redo, - undo, - updateAttributes, - updateCss, - updateEvents, - updateStateUsed, - updateUseContext -} from '../redux/reducers/slice/appStateSlice'; -import { useDispatch, useSelector } from 'react-redux'; - -import ClearIcon from '@mui/icons-material/Clear'; -import { DataGrid } from '@mui/x-data-grid'; -import ErrorMessages from '../constants/ErrorMessages'; -import FormSelector from '../components/form/Selector'; -import ProjectManager from '../components/right/ProjectManager'; -import UseStateModal from '../components/bottom/UseStateModal'; -import createModal from '../components/right/createModal'; -import makeStyles from '@mui/styles/makeStyles'; -import { ColumnTab } from '../interfaces/Interfaces'; -import { RootState } from '../redux/store'; -import { emitEvent } from '../helperFunctions/socket'; -import { Number } from 'mongoose'; - -// Previously named rightContainer, Renamed to Customizationpanel this now hangs on BottomTabs -// need to pass in props to use the useHistory feature of react router -const CustomizationPanel = ({ isThemeLight }): JSX.Element => { - const classes = useStyles(isThemeLight); - const dispatch = useDispatch(); - const state = useSelector((store: RootState) => store.appState); - const contextParam = useSelector((store: RootState) => store.contextSlice); - const roomCode = useSelector((store: RootState) => store.roomSlice.roomCode); - const style = useSelector((store: RootState) => store.styleSlice); - - const [displayMode, setDisplayMode] = useState(''); - const [flexDir, setFlexDir] = useState(''); - const [flexJustify, setFlexJustify] = useState(''); - const [flexAlign, setFlexAlign] = useState(''); - const [BGColor, setBGColor] = useState(''); - const [compText, setCompText] = useState(''); - const [compLink, setCompLink] = useState(''); - const [cssClasses, setCssClasses] = useState(''); - const [compWidth, setCompWidth] = useState(''); - const [compHeight, setCompHeight] = useState(''); - const [deleteLinkedPageError, setDeleteLinkedPageError] = useState(false); - const [deleteIndexError, setDeleteIndexError] = useState(false); - const [deleteComponentError, setDeleteComponentError] = useState(false); - const [modal, setModal] = useState(null); - const [useContextObj, setUseContextObj] = useState({}); - const [stateUsedObj, setStateUsedObj] = useState({}); - const [eventAll, setEventAll] = useState(['', '']); - const [eventRow, setEventRow] = useState([]); - - const currFocus = getFocus().child; - - useEffect(() => { - currFocus?.attributes?.compLink && - setCompLink(currFocus.attributes.compLink); - setEventAll(['', '']); - if (currFocus) { - const addedEvent: { id: string; funcName: any }[] = []; - for (const [event, funcName] of Object.entries(currFocus?.events)) { - addedEvent.push({ id: event, funcName }); - } - setEventRow(addedEvent); - } - }, [state]); - - //this function allows properties to persist and appear in nested divs - function deepIterate(arr) { - const output = []; - for (let i = 0; i < arr.length; i++) { - if (arr[i].typeId === 1000) continue; - output.push(arr[i]); - if (arr[i].children.length) { - output.push(...deepIterate(arr[i].children)); - } - } - return output; - } - - const resetFields = () => { - const childrenArray = deepIterate(configTarget.children); - for (const element of childrenArray) { - if (configTarget.child && element.childId === configTarget.child.id) { - const attributes = element.attributes; - const style = element.style; - setCompText(attributes.compText ? attributes.compText : ''); - setCompLink(attributes.compLink ? attributes.compLink : ''); - setCssClasses(attributes.cssClasses ? attributes.cssClasses : ''); - } - } - const style = configTarget.child - ? configTarget.child.style - : configTarget.style; - setDisplayMode(style.display ? style.display : ''); - setFlexDir(style.flexDirection ? style.flexDirection : ''); - setFlexJustify(style.justifyContent ? style.justifyContent : ''); - setFlexAlign(style.alignItems ? style.alignItems : ''); - setCompWidth(style.width ? style.width : ''); - setCompHeight(style.height ? style.height : ''); - setBGColor(style.backgroundColor ? style.backgroundColor : ''); - setEventAll(['', '']); - }; - let configTarget; - // after component renders, reset the input fields with the current styles of the selected child - useEffect(() => { - resetFields(); - }, [state.canvasFocus.componentId, state.canvasFocus.childId]); - // handles all input field changes, with specific updates called based on input's name - const handleChange = (e: React.ChangeEvent<{ value: any }>) => { - const inputVal = e.target.value; - switch (e.target.name) { - case 'display': - setDisplayMode(inputVal); - break; - case 'flexdir': - setFlexDir(inputVal); - break; - case 'flexjust': - setFlexJustify(inputVal); - break; - case 'flexalign': - setFlexAlign(inputVal); - break; - case 'width': - setCompWidth(inputVal); - break; - case 'height': - setCompHeight(inputVal); - break; - case 'bgcolor': - setBGColor(inputVal); - break; - case 'compText': - setCompText(inputVal); - break; - case 'compLink': - setCompLink(inputVal); - break; - case 'cssClasses': - setCssClasses(inputVal); - break; - case 'event': - setEventAll( - inputVal ? [inputVal, `handle${inputVal.slice(2)}`] : ['', ''] - ); - break; - case 'funcName': - setEventAll([eventAll[0], inputVal]); - break; - default: - break; - } - }; - // returns the current component referenced in canvasFocus - // along with its child instance, if it exists - function getFocus() { - // find and store component's name based on canvasFocus.componentId - // note: deep clone here to make sure we don't end up altering state - const focusTarget = JSON.parse( - JSON.stringify( - state.components.find( - (comp) => comp.id === state.canvasFocus.componentId - ) - ) - ); - delete focusTarget.child; - // checks if canvasFocus references a childId - const childInstanceId = state.canvasFocus.childId; - let focusChild; - // if so, breadth-first search through focusTarget's descendants to find matching child - if (childInstanceId) { - focusTarget.child = {}; - focusTarget.child.id = childInstanceId; - focusChild = {}; // child instance being referenced in canvasFocus - const searchArray = [...focusTarget.children]; - while (searchArray.length > 0) { - const currentChild = searchArray.shift(); - // if a match is found, set focusChild to the matched child and break out of the loop - if (currentChild.childId === childInstanceId) { - focusChild = currentChild; - focusTarget.child.style = focusChild.style; - focusTarget.child.events = focusChild.events; - focusTarget.child.attributes = focusChild.attributes; - break; - } - if (currentChild.name !== 'input' && currentChild.name !== 'img') - currentChild.children.forEach((child) => searchArray.push(child)); - } - - // if type is Component, use child's typeId to search through state components and find matching component's name - if (focusChild.type === 'Component') { - focusTarget.child.type = 'component'; - focusTarget.child.name = state.components.find( - (comp) => comp.id === focusChild.typeId - ).name; - // if type is HTML Element, search through HTML types to find matching element's name - } else if (focusChild.type === 'HTML Element') { - focusTarget.child.type = 'HTML element'; - focusTarget.child.name = state.HTMLTypes.find( - (elem) => elem.id === focusChild.typeId - ).name; - } - } - return focusTarget; - } - // since determining the details of the focused component/child is an expensive operation, only perform this operation if the child/component have changed - configTarget = useMemo( - () => getFocus(), - [state.canvasFocus.childId, state.canvasFocus.componentId] - ); - const isPage = (configTarget): boolean => { - const { components, rootComponents } = state; - return components - .filter((component) => rootComponents.includes(component.id)) - .some((el) => el.id === configTarget.id); - }; - const isIndex = (): boolean => configTarget.id === 1; - const isLinkedTo = (): boolean => { - const { id } = configTarget; - const pageName = state.components[id - 1].name; - let isLinked = false; - const searchNestedChildren = (comps) => { - if (comps.length === 0) return; - comps.forEach((comp, i) => { - if (comp.type === 'Route Link' && comp.name === pageName) { - isLinked = true; - } - if (comp.children.length > 0) searchNestedChildren(comp.children); - }); - }; - searchNestedChildren(state.components); - return isLinked; - }; - - const updateAttributeWithState = ( - attributeName: string, - componentProviderId, - statePropsId, - statePropsRow, - stateKey = '' - ) => { - const newInput = statePropsRow.value; - // get the stateProps of the componentProvider - const currentComponent = - state.components[state.canvasFocus.componentId - 1]; - let newContextObj = { ...currentComponent.useContext }; - if (!newContextObj) { - newContextObj = {}; - } - if (!newContextObj[componentProviderId]) { - newContextObj[componentProviderId] = { statesFromProvider: new Set() }; - } - newContextObj[componentProviderId].statesFromProvider.add(statePropsId); - if (attributeName === 'compText') { - newContextObj[componentProviderId].compText = statePropsId; - setStateUsedObj({ - ...stateUsedObj, - compText: stateKey, - compTextProviderId: componentProviderId, - compTextPropsId: statePropsId - }); - setCompText(newInput); - setUseContextObj(newContextObj); - } - - if (attributeName === 'compLink') { - newContextObj[componentProviderId].compLink = statePropsId; - setStateUsedObj({ - ...stateUsedObj, - compLink: stateKey, - compLinkProviderId: componentProviderId, - compLinkPropsId: statePropsId - }); - setCompLink(newInput); - setUseContextObj(newContextObj); - } - }; - - const eventColumnTabs: ColumnTab[] = [ - { - field: 'id', - headerName: 'Event', - width: '40%', - editable: false, - flex: 1, - disableColumnMenu: true - }, - { - field: 'funcName', - headerName: 'Function Name', - width: '50%', - editable: false, - flex: 1, - disableColumnMenu: true - }, - { - field: 'delete', - headerName: 'Delete', - width: '10%', - editable: false, - flex: 1, - sortable: false, - disableColumnMenu: true, - renderCell: function renderCell(params: any) { - return ( - - ); - } - } - ]; - - const deleteEvent = (selectedEvent) => { - dispatch( - deleteEventAction({ event: selectedEvent, contextParam: contextParam }) - ); - }; - - const handleSave = (): Object => { - dispatch(changeTailwind(false)); - dispatch( - updateStateUsed({ - stateUsedObj: stateUsedObj, - contextParam: contextParam - }) - ); - - dispatch( - updateUseContext({ - useContextObj: useContextObj, - contextParam: contextParam - }) - ); - - const styleObj: any = {}; - if (displayMode !== '') styleObj.display = displayMode; - if (flexDir !== '') styleObj.flexDirection = flexDir; - if (flexJustify !== '') styleObj.justifyContent = flexJustify; - if (flexAlign !== '') styleObj.alignItems = flexAlign; - if (compWidth !== '') styleObj.width = compWidth; - if (compHeight !== '') styleObj.height = compHeight; - if (BGColor !== '') styleObj.backgroundColor = BGColor; - dispatch(updateCss({ style: styleObj, contextParam: contextParam })); - - const attributesObj: any = {}; - if (compText !== '') attributesObj.compText = compText; - if (compLink !== '') attributesObj.compLink = compLink; - if (cssClasses !== '') attributesObj.cssClasses = cssClasses; - dispatch( - updateAttributes({ - attributes: attributesObj, - contextParam: contextParam - }) - ); - - const eventsObj: any = {}; - if (eventAll[0] !== '') eventsObj[eventAll[0]] = eventAll[1]; - dispatch(updateEvents({ events: eventsObj, contextParam: contextParam })); - - if (roomCode) { - emitEvent('updateChildAction', roomCode, { - stateUsedObj: stateUsedObj, - contextParam: contextParam, - useContextObj: useContextObj, - attributes: attributesObj, - style: styleObj, - events: eventsObj - }); - // console.log( - // 'emit updateChildAction event is triggered in CustomizationPanel.tsx' - // ); - } - - return styleObj; - }; - const handleTailwind = (): void => { - dispatch(changeTailwind(true)); - handleSave(); - }; - - // const clickToUpdate = () => { - // handleSave(); - // saveEmitter(); - // } - - // UNDO/REDO functionality--onClick these functions will be invoked. - const handleUndo = () => { - dispatch(undo({ contextParam })); - }; - const handleRedo = () => { - dispatch(redo({ contextParam })); - }; - // placeholder for handling deleting instance - const handleDelete = () => { - dispatch(deleteChild({ id: {}, contextParam: contextParam })); - if (roomCode) { - emitEvent('deleteChildAction', roomCode, { - id: {}, - contextParam: contextParam - }); - // console.log( - // 'emit deleteChildAction event is triggered in CustomizationPanel.tsx' - // ); - } - }; - - const handlePageDelete = (id) => () => { - // TODO: return modal - if (isLinkedTo()) return setDeleteLinkedPageError(true); - isIndex() ? handleDialogError('index') : dispatch(deletePage({ id })); - }; - const handleDialogError = (err) => { - if (err === 'index') setDeleteIndexError(true); - else setDeleteComponentError(true); - }; - const handleCloseDialogError = () => { - setDeleteIndexError(false); - setDeleteComponentError(false); - setDeleteLinkedPageError(false); - }; - // closes out the open modal - const closeModal = (): void => setModal(''); - // creates modal that asks if user wants to clear all components - // if user clears their components, then their components are removed from state and the modal is closed - const clearComps = (): void => { - // Reset state for project to initial state - const handleDeleteReusableComponent = (): void => { - closeModal(); - dispatch(deleteReusableComponent({ contextParam: contextParam })); - }; - // set modal options - const children = ( - - - - - - - - - ); - - // create modal - setModal( - createModal({ - closeModal, - children, - message: - 'Deleting this component will delete all instances of this component within the application. Do you still wish to proceed?', - primBtnLabel: null, - primBtnAction: null, - secBtnAction: null, - secBtnLabel: null, - open: true - }) - ); - }; - const keyBindedFunc = useCallback((e) => { - // the || is for either Mac or Windows OS - // Undo - (e.key === 'z' && e.metaKey && !e.shiftKey) || - (e.key === 'z' && e.ctrlKey && !e.shiftKey) - ? handleUndo() - : // Redo - (e.shiftKey && e.metaKey && e.key === 'z') || - (e.shiftKey && e.ctrlKey && e.key === 'z') - ? handleRedo() - : // Delete HTML tag off canvas - e.key === 'Backspace' && - e.target.tagName !== 'TEXTAREA' && - e.target.tagName !== 'INPUT' - ? handleDelete() - : // Save - (e.key === 's' && e.ctrlKey && e.shiftKey) || - (e.key === 's' && e.metaKey && e.shiftKey) - ? handleSave() - : ''; - }, []); - - useEffect(() => { - document.addEventListener('keydown', keyBindedFunc); - return () => { - document.removeEventListener('keydown', keyBindedFunc); - }; - }, []); - - if (state.canvasFocus.childId === null) { - return ( -
- -
-
-
-

- Parent Component: -
-
- - {configTarget.name} - -

- Drag or click an html element to the canvas to see what - happens! -

-

-
-
-
- -
- ); - } - return ( -
- - {/* -----------------------------MOVED PROJECT MANAGER-------------------------------------- */} -
-
-
-

- Instance of - {configTarget.child.type === 'component' - ? ' component:' - : ' element:'}{' '} - {configTarget.child.name} -

-
-
-
- - {displayMode === 'flex' && ( -
- - - -
- )} - - -
-
-

Background Color:

-
-
- - - -
-
-
-
-
-
-

Text:

-
-
- - - -
-
- -
-
-
-
-

Link:

-
-
- - - -
-
- -
-
-
-
-

CSS Classes:

-
-
- - - -
-
-
- -
- {eventAll[0] && ( -
-
-

Function Name:

-
- - - -
- )} - {currFocus && Object.keys(currFocus.events).length !== 0 && ( -
- -
- )} -
-
-
-
- -
- {/*
- -
- -
- -
*/} - {configTarget.child ? ( -
- -
- ) : isPage(configTarget) ? ( -
- -
- ) : ( -
- -
- )} -
- - -
-
-
-
- - - {deleteIndexError ? ErrorMessages.deleteIndexTitle : ''} - {deleteComponentError ? ErrorMessages.deleteComponentTitle : ''} - {deleteLinkedPageError ? ErrorMessages.deleteLinkedPageTitle : ''} - - - - {deleteIndexError ? ErrorMessages.deleteIndexMessage : ''} - {deleteComponentError ? ErrorMessages.deleteComponentMessage : ''} - {deleteLinkedPageError ? ErrorMessages.deleteLinkedPageMessage : ''} - - - - - - - {modal} -
- ); -}; -const useStyles = makeStyles({ - select: { - fontSize: '1em', - borderRadius: '10px', - '> .MuiSelect-icon': { - color: '#C6C6C6' - } - }, - selectInput: { - paddingTop: '15px', - paddingLeft: '15px' - }, - formControl: { - minWidth: '125px' - }, - configRow: { - display: 'flex', - paddingLeft: '25px', - paddingRight: '25px', - marginTop: '20px' - }, - configType: { - minWidth: '185px', - fontSize: '85%' - }, - configValue: { - marginLeft: '20px' - }, - buttonRow: (isThemeLight) => ({ - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - textAlign: 'center', - '& > .MuiButton-textSecondary': { - color: isThemeLight ? '#808080' : '#ECECEA', // color for delete page - border: isThemeLight ? '1px solid #808080' : '1px solid #ECECEA' - } - }), - button: { - fontSize: '1rem', - padding: '9px 35px', - margin: '10px 15px 0 0', - borderRadius: '8px' - }, - saveButtonLight: { - border: '1px solid #0671e3', - backgroundColor: 'rgba(0, 0, 0, 0.2)' - }, - saveButtonDark: { - border: '1px solid #3c59ba' - }, - compName: { - fontSize: '1rem' - }, - rootCompName: { - fontSize: '1.75rem' - }, - // 'Parent Component' font size - configHeader: { - '& > h4': { - fontSize: '1rem', - letterSpacing: '0.5px', - marginBottom: '10px', - marginTop: '10px' - } - }, - rootConfigHeader: { - height: '70px', - '& > h4': { - fontSize: '1.75rem', - letterSpacing: '0.5px', - marginBottom: '0', - marginTop: '30px' - } - }, - lightThemeFontColor: { - color: 'white' - }, - darkThemeFontColor: { - color: '#fff' - } -}); - -export default CustomizationPanel; +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, + FormControl, + List, + ListItem, + ListItemText, + TextField +} from '@mui/material'; +import { Redo, Undo } from '@mui/icons-material'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { + changeTailwind, + deleteChild, + deleteEventAction, + deletePage, + deleteReusableComponent, + redo, + undo, + updateAttributes, + updateCss, + updateEvents, + updateStateUsed, + updateUseContext +} from '../redux/reducers/slice/appStateSlice'; +import { useDispatch, useSelector } from 'react-redux'; + +import ClearIcon from '@mui/icons-material/Clear'; +import { DataGrid } from '@mui/x-data-grid'; +import ErrorMessages from '../constants/ErrorMessages'; +import FormSelector from '../components/form/Selector'; +import ProjectManager from '../components/right/ProjectManager'; +import UseStateModal from '../components/bottom/UseStateModal'; +import createModal from '../components/right/createModal'; +import makeStyles from '@mui/styles/makeStyles'; +import { ColumnTab } from '../interfaces/Interfaces'; +import { RootState } from '../redux/store'; +import { emitEvent } from '../helperFunctions/socket'; +import { Number } from 'mongoose'; + +// Previously named rightContainer, Renamed to Customizationpanel this now hangs on BottomTabs +// need to pass in props to use the useHistory feature of react router +const CustomizationPanel = ({ isThemeLight }): JSX.Element => { + const classes = useStyles(isThemeLight); + const dispatch = useDispatch(); + const state = useSelector((store: RootState) => store.appState); + const contextParam = useSelector((store: RootState) => store.contextSlice); + const roomCode = useSelector((store: RootState) => store.roomSlice.roomCode); + const style = useSelector((store: RootState) => store.styleSlice); + + const [displayMode, setDisplayMode] = useState(''); + const [flexDir, setFlexDir] = useState(''); + const [flexJustify, setFlexJustify] = useState(''); + const [flexAlign, setFlexAlign] = useState(''); + const [flexOptionsVisible, setFlexOptionsVisible] = useState(false); + const [BGColor, setBGColor] = useState(''); + const [compText, setCompText] = useState(''); + const [compLink, setCompLink] = useState(''); + const [cssClasses, setCssClasses] = useState(''); + const [compWidth, setCompWidth] = useState(''); + const [compHeight, setCompHeight] = useState(''); + const [deleteLinkedPageError, setDeleteLinkedPageError] = useState(false); + const [deleteIndexError, setDeleteIndexError] = useState(false); + const [deleteComponentError, setDeleteComponentError] = useState(false); + const [modal, setModal] = useState(null); + const [useContextObj, setUseContextObj] = useState({}); + const [stateUsedObj, setStateUsedObj] = useState({}); + const [eventAll, setEventAll] = useState(['', '']); + const [eventOptionsVisible, setEventOptionsVisible] = useState(false); + const [eventRow, setEventRow] = useState([]); + const [eventRowsVisible, setEventRowsVisible] = useState(false); + + const currFocus = getFocus().child; + + useEffect(() => { + currFocus?.attributes?.compLink && + setCompLink(currFocus.attributes.compLink); + setEventAll(['', '']); + if (currFocus) { + const addedEvent: { id: string; funcName: any }[] = []; + for (const [event, funcName] of Object.entries(currFocus?.events)) { + addedEvent.push({ id: event, funcName }); + } + setEventRow(addedEvent); + } + }, [state]); + + useEffect(() => { + if (displayMode === 'flex') { + return setFlexOptionsVisible(true); + } + return setFlexOptionsVisible(false); + }, [displayMode]); + + useEffect(() => { + if (eventAll[0] !== '') { + return setEventOptionsVisible(true); + } + return setEventOptionsVisible(false); + }, [eventAll]); + + useEffect(() => { + if (eventRow.length) { + return setEventRowsVisible(true); + } + return setEventRowsVisible(false); + }, [eventRow]); + + const marginTopAmount = () => { + let totalMargin = 0; + if (eventOptionsVisible) totalMargin += 90; + if (flexOptionsVisible) totalMargin = Math.max(totalMargin, 210); + if (eventRowsVisible) totalMargin = Math.max(totalMargin, 335); + return `${totalMargin}px`; + }; + + //this function allows properties to persist and appear in nested divs + function deepIterate(arr) { + const output = []; + for (let i = 0; i < arr.length; i++) { + if (arr[i].typeId === 1000) continue; + output.push(arr[i]); + if (arr[i].children.length) { + output.push(...deepIterate(arr[i].children)); + } + } + return output; + } + + const resetFields = () => { + const childrenArray = deepIterate(configTarget.children); + for (const element of childrenArray) { + if (configTarget.child && element.childId === configTarget.child.id) { + const attributes = element.attributes; + const style = element.style; + setCompText(attributes.compText ? attributes.compText : ''); + setCompLink(attributes.compLink ? attributes.compLink : ''); + setCssClasses(attributes.cssClasses ? attributes.cssClasses : ''); + } + } + const style = configTarget.child + ? configTarget.child.style + : configTarget.style; + setDisplayMode(style.display ? style.display : ''); + setFlexDir(style.flexDirection ? style.flexDirection : ''); + setFlexJustify(style.justifyContent ? style.justifyContent : ''); + setFlexAlign(style.alignItems ? style.alignItems : ''); + setCompWidth(style.width ? style.width : ''); + setCompHeight(style.height ? style.height : ''); + setBGColor(style.backgroundColor ? style.backgroundColor : ''); + setEventAll(['', '']); + }; + let configTarget; + // after component renders, reset the input fields with the current styles of the selected child + useEffect(() => { + resetFields(); + }, [state.canvasFocus.componentId, state.canvasFocus.childId]); + // handles all input field changes, with specific updates called based on input's name + const handleChange = (e: React.ChangeEvent) => { + const inputVal = e.target.value; + switch (e.target.name) { + case 'display': + setDisplayMode(inputVal); + break; + case 'flexdir': + setFlexDir(inputVal); + break; + case 'flexjust': + setFlexJustify(inputVal); + break; + case 'flexalign': + setFlexAlign(inputVal); + break; + case 'width': + setCompWidth(inputVal); + break; + case 'height': + setCompHeight(inputVal); + break; + case 'bgcolor': + setBGColor(inputVal); + break; + case 'compText': + setCompText(inputVal); + break; + case 'compLink': + setCompLink(inputVal); + break; + case 'cssClasses': + setCssClasses(inputVal); + break; + case 'event': + setEventAll( + inputVal ? [inputVal, `handle${inputVal.slice(2)}`] : ['', ''] + ); + break; + case 'funcName': + setEventAll([eventAll[0], inputVal]); + break; + default: + break; + } + }; + // returns the current component referenced in canvasFocus + // along with its child instance, if it exists + function getFocus() { + // find and store component's name based on canvasFocus.componentId + // note: deep clone here to make sure we don't end up altering state + const focusTarget = JSON.parse( + JSON.stringify( + state.components.find( + (comp) => comp.id === state.canvasFocus.componentId + ) + ) + ); + delete focusTarget.child; + // checks if canvasFocus references a childId + const childInstanceId = state.canvasFocus.childId; + let focusChild; + // if so, breadth-first search through focusTarget's descendants to find matching child + if (childInstanceId) { + focusTarget.child = {}; + focusTarget.child.id = childInstanceId; + focusChild = {}; // child instance being referenced in canvasFocus + const searchArray = [...focusTarget.children]; + while (searchArray.length > 0) { + const currentChild = searchArray.shift(); + // if a match is found, set focusChild to the matched child and break out of the loop + if (currentChild.childId === childInstanceId) { + focusChild = currentChild; + focusTarget.child.style = focusChild.style; + focusTarget.child.events = focusChild.events; + focusTarget.child.attributes = focusChild.attributes; + break; + } + if (currentChild.name !== 'input' && currentChild.name !== 'img') + currentChild.children.forEach((child) => searchArray.push(child)); + } + + // if type is Component, use child's typeId to search through state components and find matching component's name + if (focusChild.type === 'Component') { + focusTarget.child.type = 'component'; + focusTarget.child.name = state.components.find( + (comp) => comp.id === focusChild.typeId + ).name; + // if type is HTML Element, search through HTML types to find matching element's name + } else if (focusChild.type === 'HTML Element') { + focusTarget.child.type = 'HTML element'; + focusTarget.child.name = state.HTMLTypes.find( + (elem) => elem.id === focusChild.typeId + ).name; + } + } + return focusTarget; + } + // since determining the details of the focused component/child is an expensive operation, only perform this operation if the child/component have changed + configTarget = useMemo( + () => getFocus(), + [state.canvasFocus.childId, state.canvasFocus.componentId] + ); + const isPage = (configTarget): boolean => { + const { components, rootComponents } = state; + return components + .filter((component) => rootComponents.includes(component.id)) + .some((el) => el.id === configTarget.id); + }; + const isIndex = (): boolean => configTarget.id === 1; + const isLinkedTo = (): boolean => { + const { id } = configTarget; + const pageName = state.components[id - 1].name; + let isLinked = false; + const searchNestedChildren = (comps) => { + if (comps.length === 0) return; + comps.forEach((comp, i) => { + if (comp.type === 'Route Link' && comp.name === pageName) { + isLinked = true; + } + if (comp.children.length > 0) searchNestedChildren(comp.children); + }); + }; + searchNestedChildren(state.components); + return isLinked; + }; + + const updateAttributeWithState = ( + attributeName: string, + componentProviderId, + statePropsId, + statePropsRow, + stateKey = '' + ) => { + const newInput = statePropsRow.value; + // get the stateProps of the componentProvider + const currentComponent = + state.components[state.canvasFocus.componentId - 1]; + let newContextObj = { ...currentComponent.useContext }; + if (!newContextObj) { + newContextObj = {}; + } + if (!newContextObj[componentProviderId]) { + newContextObj[componentProviderId] = { statesFromProvider: new Set() }; + } + newContextObj[componentProviderId].statesFromProvider.add(statePropsId); + if (attributeName === 'compText') { + newContextObj[componentProviderId].compText = statePropsId; + setStateUsedObj({ + ...stateUsedObj, + compText: stateKey, + compTextProviderId: componentProviderId, + compTextPropsId: statePropsId + }); + setCompText(newInput); + setUseContextObj(newContextObj); + } + + if (attributeName === 'compLink') { + newContextObj[componentProviderId].compLink = statePropsId; + setStateUsedObj({ + ...stateUsedObj, + compLink: stateKey, + compLinkProviderId: componentProviderId, + compLinkPropsId: statePropsId + }); + setCompLink(newInput); + setUseContextObj(newContextObj); + } + }; + + const eventColumnTabs: ColumnTab[] = [ + { + field: 'id', + headerName: 'Event', + width: '40%', + editable: false, + flex: 1, + disableColumnMenu: true + }, + { + field: 'funcName', + headerName: 'Function Name', + width: '50%', + editable: false, + flex: 1, + disableColumnMenu: true + }, + { + field: 'delete', + headerName: 'Delete', + width: '10%', + editable: false, + flex: 1, + sortable: false, + disableColumnMenu: true, + renderCell: function renderCell(params: any) { + return ( + + ); + } + } + ]; + + const deleteEvent = (selectedEvent) => { + dispatch( + deleteEventAction({ event: selectedEvent, contextParam: contextParam }) + ); + }; + + const handleSave = (): Object => { + dispatch(changeTailwind(false)); + dispatch( + updateStateUsed({ + stateUsedObj: stateUsedObj, + contextParam: contextParam + }) + ); + + dispatch( + updateUseContext({ + useContextObj: useContextObj, + contextParam: contextParam + }) + ); + + const styleObj: any = {}; + if (displayMode !== '') styleObj.display = displayMode; + if (flexDir !== '') styleObj.flexDirection = flexDir; + if (flexJustify !== '') styleObj.justifyContent = flexJustify; + if (flexAlign !== '') styleObj.alignItems = flexAlign; + if (compWidth !== '') styleObj.width = compWidth; + if (compHeight !== '') styleObj.height = compHeight; + if (BGColor !== '') styleObj.backgroundColor = BGColor; + dispatch(updateCss({ style: styleObj, contextParam: contextParam })); + + const attributesObj: any = {}; + if (compText !== '') attributesObj.compText = compText; + if (compLink !== '') attributesObj.compLink = compLink; + if (cssClasses !== '') attributesObj.cssClasses = cssClasses; + dispatch( + updateAttributes({ + attributes: attributesObj, + contextParam: contextParam + }) + ); + + const eventsObj: any = {}; + if (eventAll[0] !== '') eventsObj[eventAll[0]] = eventAll[1]; + dispatch(updateEvents({ events: eventsObj, contextParam: contextParam })); + + if (roomCode) { + emitEvent('updateChildAction', roomCode, { + stateUsedObj: stateUsedObj, + contextParam: contextParam, + useContextObj: useContextObj, + attributes: attributesObj, + style: styleObj, + events: eventsObj + }); + // console.log( + // 'emit updateChildAction event is triggered in CustomizationPanel.tsx' + // ); + } + + return styleObj; + }; + const handleTailwind = (): void => { + dispatch(changeTailwind(true)); + handleSave(); + }; + + // const clickToUpdate = () => { + // handleSave(); + // saveEmitter(); + // } + + // UNDO/REDO functionality--onClick these functions will be invoked. + const handleUndo = () => { + dispatch(undo({ contextParam })); + }; + const handleRedo = () => { + dispatch(redo({ contextParam })); + }; + // placeholder for handling deleting instance + const handleDelete = () => { + dispatch(deleteChild({ id: {}, contextParam: contextParam })); + if (roomCode) { + emitEvent('deleteChildAction', roomCode, { + id: {}, + contextParam: contextParam + }); + // console.log( + // 'emit deleteChildAction event is triggered in CustomizationPanel.tsx' + // ); + } + }; + + const handlePageDelete = (id) => () => { + // TODO: return modal + if (isLinkedTo()) return setDeleteLinkedPageError(true); + isIndex() ? handleDialogError('index') : dispatch(deletePage({ id })); + }; + const handleDialogError = (err) => { + if (err === 'index') setDeleteIndexError(true); + else setDeleteComponentError(true); + }; + const handleCloseDialogError = () => { + setDeleteIndexError(false); + setDeleteComponentError(false); + setDeleteLinkedPageError(false); + }; + // closes out the open modal + const closeModal = (): void => setModal(''); + // creates modal that asks if user wants to clear all components + // if user clears their components, then their components are removed from state and the modal is closed + const clearComps = (): void => { + // Reset state for project to initial state + const handleDeleteReusableComponent = (): void => { + closeModal(); + dispatch(deleteReusableComponent({ contextParam: contextParam })); + }; + // set modal options + const children = ( + + + + + + + + + ); + + // create modal + setModal( + createModal({ + closeModal, + children, + message: + 'Deleting this component will delete all instances of this component within the application. Do you still wish to proceed?', + primBtnLabel: null, + primBtnAction: null, + secBtnAction: null, + secBtnLabel: null, + open: true + }) + ); + }; + const keyBindedFunc = useCallback((e) => { + // the || is for either Mac or Windows OS + // Undo + (e.key === 'z' && e.metaKey && !e.shiftKey) || + (e.key === 'z' && e.ctrlKey && !e.shiftKey) + ? handleUndo() + : // Redo + (e.shiftKey && e.metaKey && e.key === 'z') || + (e.shiftKey && e.ctrlKey && e.key === 'z') + ? handleRedo() + : // Delete HTML tag off canvas + e.key === 'Backspace' && + e.target.tagName !== 'TEXTAREA' && + e.target.tagName !== 'INPUT' + ? handleDelete() + : // Save + (e.key === 's' && e.ctrlKey && e.shiftKey) || + (e.key === 's' && e.metaKey && e.shiftKey) + ? handleSave() + : ''; + }, []); + + useEffect(() => { + document.addEventListener('keydown', keyBindedFunc); + return () => { + document.removeEventListener('keydown', keyBindedFunc); + }; + }, []); + + if (state.canvasFocus.childId === null) { + return ( +
+ +
+
+
+

+ Parent Component: +
+
+ + {configTarget.name} + +

+ Drag or click an html element to the canvas to see what + happens! +

+

+
+
+
+ +
+ ); + } + return ( +
+ + {/* -----------------------------MOVED PROJECT MANAGER-------------------------------------- */} +
+
+
+

+ Instance of + {configTarget.child.type === 'component' + ? ' component:' + : ' element:'}{' '} + {configTarget.child.name} +

+
+
+
+ + {displayMode === 'flex' && ( +
+ + + +
+ )} + + +
+
+

Background Color:

+
+
+ + + +
+
+
+
+
+
+

Text:

+
+
+ + + +
+
+ +
+
+
+
+

Link:

+
+
+ + + +
+
+ +
+
+
+
+

CSS Classes:

+
+
+ + + +
+
+
+ +
+ {eventAll[0] && ( +
+
+

Function Name:

+
+ + + +
+ )} + {currFocus && Object.keys(currFocus.events).length !== 0 && ( +
+ +
+ )} +
+
+
+
+ +
+ {/*
+ +
+ +
+ +
*/} + {configTarget.child ? ( +
+ +
+ ) : isPage(configTarget) ? ( +
+ +
+ ) : ( +
+ +
+ )} +
+ + +
+
+
+
+ + + {deleteIndexError ? ErrorMessages.deleteIndexTitle : ''} + {deleteComponentError ? ErrorMessages.deleteComponentTitle : ''} + {deleteLinkedPageError ? ErrorMessages.deleteLinkedPageTitle : ''} + + + + {deleteIndexError ? ErrorMessages.deleteIndexMessage : ''} + {deleteComponentError ? ErrorMessages.deleteComponentMessage : ''} + {deleteLinkedPageError ? ErrorMessages.deleteLinkedPageMessage : ''} + + + + + + + {modal} +
+ ); +}; +const useStyles = makeStyles({ + select: { + fontSize: '1em', + borderRadius: '10px', + '> .MuiSelect-icon': { + color: '#C6C6C6' + } + }, + selectInput: { + paddingTop: '15px', + paddingLeft: '15px' + }, + formControl: { + minWidth: '125px' + }, + configRow: { + display: 'flex', + paddingLeft: '25px', + paddingRight: '25px', + marginTop: '20px' + }, + configType: { + minWidth: '185px', + fontSize: '85%' + }, + configValue: { + marginLeft: '20px' + }, + buttonRow: (isThemeLight) => ({ + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + textAlign: 'center', + marginLeft: '15px', + '& > .MuiButton-textSecondary': { + color: isThemeLight ? '#808080' : '#ECECEA', // color for delete page + border: isThemeLight ? '1px solid #808080' : '1px solid #ECECEA' + } + }), + button: { + fontSize: '1rem', + padding: '9px 35px', + margin: '10px 15px 0 0', + borderRadius: '8px' + }, + saveButtonLight: { + border: '1px solid #0671e3', + backgroundColor: 'rgba(0, 0, 0, 0.2)' + }, + saveButtonDark: { + border: '1px solid #3c59ba' + }, + compName: { + fontSize: '1rem' + }, + rootCompName: { + fontSize: '1.75rem' + }, + // 'Parent Component' font size + configHeader: { + '& > h4': { + fontSize: '1rem', + letterSpacing: '0.5px', + marginBottom: '10px', + marginTop: '10px' + } + }, + rootConfigHeader: { + height: '70px', + '& > h4': { + fontSize: '1.75rem', + letterSpacing: '0.5px', + marginBottom: '0', + marginTop: '30px' + } + }, + lightThemeFontColor: { + color: 'white' + }, + darkThemeFontColor: { + color: '#fff' + } +}); + +export default CustomizationPanel; diff --git a/app/src/helperFunctions/generateCode.ts b/app/src/helperFunctions/generateCode.ts index 4b4a2150..35bbf93e 100644 --- a/app/src/helperFunctions/generateCode.ts +++ b/app/src/helperFunctions/generateCode.ts @@ -1,602 +1,639 @@ -import { - Component, - ChildElement, - HTMLType, - ChildStyle, - StateProp -} from '../interfaces/Interfaces'; -declare global { - interface Window { - api: any; - } -} -// generate code based on component hierarchy and then return the rendered code -const generateCode = ( - components: Component[], - componentId: number, - rootComponents: number[], - projectType: string, - HTMLTypes: HTMLType[], - tailwind: boolean, - contextParam: any -) => { - const code = generateUnformattedCode( - components, - componentId, - rootComponents, - projectType, - HTMLTypes, - tailwind, - contextParam - ); - return formatCode(code); -}; - -// generate code based on the component hierarchy -const generateUnformattedCode = ( - comps: Component[], - componentId: number, - rootComponents: number[], - projectType: string, - HTMLTypes: HTMLType[], - tailwind: boolean, - contextParam: any -) => { - const components = [...comps]; - // find the component that we're going to generate code for - const currComponent = components.find((elem) => elem.id === componentId); - // find the unique components that we need to import into this component file - let imports: any = []; - let providers: string = ''; - let context: string = ''; - let links: boolean = false; - let images: boolean = false; - const isRoot = rootComponents.includes(componentId); - let importReactRouter = false; - // returns an array of objects which may include components, html elements, and/or route links - const getEnrichedChildren = (currentComponent: Component | ChildElement) => { - // declare an array of enriched children - const enrichedChildren = currentComponent.children?.map((elem: any) => { - //enrichedChildren is iterating through the children array - const child: ChildElement = { ...elem }; - // check if child is a component - if (child.type === 'Component') { - // verify that the child is in the components array in state - const referencedComponent = components.find( - (elem) => elem.id === child.typeId - ); - // check if imports array include the referenced component, if not, add its name to the imports array (e.g. the name/tag of the component/element) - if (!imports.includes(referencedComponent.name)) - imports.push(referencedComponent.name); - child['name'] = referencedComponent.name; - return child; - } else if (child.type === 'HTML Element') { - const referencedHTML = HTMLTypes.find( - (elem) => elem.id === child.typeId - ); - child['tag'] = referencedHTML.tag; - if ( - referencedHTML.tag === 'h1' || - referencedHTML.tag === 'h2' || - referencedHTML.tag === 'a' || - referencedHTML.tag === 'p' || - referencedHTML.tag === 'button' || - referencedHTML.tag === 'span' || - referencedHTML.tag === 'div' || - referencedHTML.tag === 'separator' || - referencedHTML.tag === 'form' || - referencedHTML.tag === 'ul' || - referencedHTML.tag === 'ol' || - referencedHTML.tag === 'menu' || - referencedHTML.tag === 'li' || - referencedHTML.tag === 'Link' || - referencedHTML.tag === 'Switch' || - referencedHTML.tag === 'Route' - ) { - child.children = getEnrichedChildren(child); - } - - // when we see a Switch or LinkTo, import React Router - if ( - (referencedHTML.tag === 'Switch' || referencedHTML.tag === 'Route') && - projectType === 'Classic React' - ) - importReactRouter = true; - else if (referencedHTML.tag === 'Link') links = true; - if (referencedHTML.tag === 'Image') images = true; - return child; - } else if (child.type === 'Route Link') { - links = true; - child.name = components.find( - (comp: Component) => comp.id === child.typeId - ).name; - return child; - } - }); - return enrichedChildren; - }; - // Raised formatStyles so that it is declared before it is referenced. It was backwards. - // format styles stored in object to match React inline style format - const formatStyles = (styleObj: ChildStyle) => { - if (Object.keys(styleObj).length === 0) return ``; - const formattedStyles: String[] = []; - let styleString: String; - for (let i in styleObj) { - if (i === 'style') { - styleString = i + '=' + '{' + JSON.stringify(styleObj[i]) + '}'; - formattedStyles.push(styleString); - } - } - return formattedStyles; - }; - // function to dynamically add classes, ids, and styles to an element if it exists. - //LEGACY PD: CAN ADD PROPS HERE AS JSX ATTRIBUTE - const elementTagDetails = (childElement: ChildElement) => { - let customizationDetails = ''; - let passedInPropsString = ''; - if (childElement.type === 'Component') { - let childComponent: Component; - for (let i = 0; i < components.length; i++) { - if (childElement.name === components[i].name) { - childComponent = components[i]; - } - } - childComponent.passedInProps.forEach((prop) => { - passedInPropsString += `${prop.key} = {${prop.key}} `; - }); - } - - if (childElement.childId && childElement.tag !== 'Route') - customizationDetails += - ' ' + `id="${+childElement.childId}" ` + `${passedInPropsString}`; - - if ( - childElement.attributes && - childElement.attributes.cssClasses && - !tailwind - ) { - customizationDetails += - ' ' + `className="${childElement.attributes.cssClasses}"`; - } - if ( - childElement.style && - Object.keys(childElement.style).length > 0 && - tailwind === false - ) - customizationDetails += ' ' + formatStyles(childElement.style); - if ( - childElement.style && - Object.keys(childElement.style).length > 0 && - tailwind === true - ) { - let { - height, - alignItems, - backgroundColor, - display, - flexDirection, - width, - justifyContent - } = childElement.style; - let w: String, - h: String, - items: String, - bg: String, - d: String, - flexDir: String, - justCon: String, - cssClasses: String; - if (childElement.style.alignItems) { - if (alignItems === 'center') items = 'items-center '; - else if (alignItems === 'flex-start') items = 'items-start '; - else if (alignItems === 'flex-end') items = 'items-end '; - else if (alignItems === 'stretch') items = 'items-stretch '; - } - if (childElement.style.backgroundColor) { - bg = `bg-[${backgroundColor}] `; - } - if (childElement.style.display) { - if (display === 'flex') d = 'flex '; - else if (display === 'inline-block') d = 'inline-block '; - else if (display === 'block') d = 'block '; - else if (display === 'none') d = 'hidden '; - } - if (childElement.style.flexDirection) { - if (flexDirection === 'column') flexDir = 'flex-col '; - } - if (childElement.style.height) { - if (height === '100%') h = 'h-full '; - else if (height === '50%') h = 'h-1/2 '; - else if (height === '25%') h = 'h-1/4 '; - else if (height === 'auto') h = 'auto '; - } - if (childElement.style.justifyContent) { - if (justifyContent === 'center') justCon = 'justify-center '; - else if (justifyContent === 'flex-start') justCon = 'justify-start '; - else if (justifyContent === 'space-between') - justCon = 'justify-between '; - else if (justifyContent === 'space-around') justCon = 'justify-around '; - else if (justifyContent === 'flex-end') justCon = 'justify-end '; - else if (justifyContent === 'space-evenly') justCon = 'justify-evenly '; - } - if (childElement.style.width) { - if (width === '100%') w = 'w-full '; - else if (width === '50%') w = 'w-1/2 '; - else if (width === '25%') w = 'w-1/4 '; - else if (width === 'auto') w = 'w-auto '; - } - if (childElement.attributes && childElement.attributes.cssClasses) { - cssClasses = `${childElement.attributes.cssClasses} `; - } - customizationDetails += - ' ' + - `className = "${cssClasses ? cssClasses : ''} ${w ? w : ''}${ - h ? h : '' - }${justCon ? justCon : ''}${flexDir ? flexDir : ''}${d ? d : ''}${ - bg ? bg : '' - }${items ? items : ''}"`; - } - - if (childElement.events && Object.keys(childElement.events).length > 0) { - // SPACE BETWEEN ATTRIBUTE EXPRESSIONS - for (const [event, funcName] of Object.entries(childElement.events)) { - customizationDetails += ' ' + `${event}={(event) => ${funcName}()}`; - } - } - - return customizationDetails; - }; - // function to fix the spacing of the ace editor for new lines of added content. This was breaking on nested components, leaving everything right justified. - const tabSpacer = (level: number) => { - let tabs = ' '; - for (let i = 0; i < level; i++) tabs += ' '; - return tabs; - }; - // function to dynamically generate the appropriate levels for the code preview - const levelSpacer = (level: number, spaces: number) => { - if (level === 2) return `\n${tabSpacer(spaces)}`; - else return ''; - }; - // function to dynamically generate a complete html (& also other library type) elements - const elementGenerator = (childElement: ChildElement, level: number = 2) => { - let innerText = ''; - let activeLink = '""'; - if (childElement.attributes && childElement.attributes.compText) { - if (childElement.stateUsed && childElement.stateUsed.compText) { - innerText = '{' + childElement.stateUsed.compText + '}'; - } else { - innerText = childElement.attributes.compText; - } - } - if (childElement.attributes && childElement.attributes.compLink) { - if (childElement.stateUsed && childElement.stateUsed.compLink) { - activeLink = '{' + childElement.stateUsed.compLink + '}'; - } else { - activeLink = '"' + childElement.attributes.compLink + '"'; - } - } - - const nestable = - childElement.tag === 'h1' || - childElement.tag === 'h2' || - childElement.tag === 'a' || - childElement.tag === 'span' || - childElement.tag === 'button' || - childElement.tag === 'p' || - childElement.tag === 'div' || - childElement.tag === 'form' || - childElement.tag === 'ol' || - childElement.tag === 'ul' || - childElement.tag === 'menu' || - childElement.tag === 'li' || - childElement.tag === 'Switch' || - childElement.tag === 'Route'; - - if (childElement.tag === 'img') { - return `${levelSpacer(level, 5)}<${ - childElement.tag - } src=${activeLink} ${elementTagDetails(childElement)}/>${levelSpacer( - 2, - 3 + level - )}`; - } else if (childElement.tag === 'a') { - return `${levelSpacer(level, 5)}<${ - childElement.tag - } href=${activeLink} ${elementTagDetails(childElement)}>${innerText}${levelSpacer(2, 3 + level)}`; - } else if (childElement.tag === 'input') { - return `${levelSpacer(level, 5)}<${childElement.tag}${elementTagDetails( - childElement - )}>${levelSpacer(2, 3 + level)}`; - } else if (childElement.tag === 'Link' && projectType === 'Classic React') { - return `${levelSpacer(level, 5)} - ${tabSpacer(level)}${writeNestedElements( - childElement.children, - level + 1 - )}${innerText} - ${tabSpacer(level - 1)}${levelSpacer(2, 3 + level)}`; - } else if (childElement.tag === 'Link' && projectType === 'Next.js') { - return `${levelSpacer( - level, - 5 - )} - ${tabSpacer(level)}${innerText}${writeNestedElements( - childElement.children, - level + 1 - )} - ${tabSpacer(level - 1)}${levelSpacer(2, 3 + level)}`; - } else if (childElement.tag === 'Image') { - return `${levelSpacer(level, 5)}<${ - childElement.tag - } src=${activeLink} ${elementTagDetails(childElement)}/>`; - } else if (nestable) { - if ( - (childElement.tag === 'Route' || childElement.tag === 'Switch') && - projectType === 'Next.js' - ) { - return `${writeNestedElements(childElement.children, level)}`; - } - const routePath = - childElement.tag === 'Route' ? ' ' + 'exact path=' + activeLink : ''; - return `${levelSpacer(level, 5)}<${childElement.tag}${elementTagDetails( - childElement - )}${routePath}> - ${tabSpacer(level)}${innerText} - ${tabSpacer(level)}${writeNestedElements( - childElement.children, - level + 1 - )} - ${tabSpacer(level - 1)}${levelSpacer( - 2, - 3 + level - )}`; - } else if (childElement.tag !== 'separator') { - return `${levelSpacer(level, 5)}<${childElement.tag}${elementTagDetails( - childElement - )}>${innerText}${levelSpacer(2, 3 + level)}`; - } - }; - // write all code that will be under the "return" of the component - const writeNestedElements = (enrichedChildren: any, level: number = 2) => { - return `${enrichedChildren - .map((child: ChildElement) => { - if (child.type === 'Component') { - return `<${child.name} ${elementTagDetails(child)}/>`; - } else if (child.type === 'HTML Element') { - return elementGenerator(child, level); - } - // route links are for gatsby.js and next.js feature. if the user creates a route link and then switches projects, generate code for a normal link instead - else if (child.type === 'Route Link') { - if (projectType === 'Next.js') { - // if route link points to index, to go endpoint / rather than /index - if (child.name === 'index') - return ``; - else - return ``; - } else if (projectType === 'Gatsby.js') { - if (child.name === 'index') - return `
${child.name}
`; - else - return `
${child.name}
`; - } else return ``; - } - }) - .filter((element) => !!element) - .join('')}`; - }; - // function to properly incorporate the user created state that is stored in the application state - const writeStateProps = (stateArray: String[]) => { - let stateToRender: String = ''; - for (const element of stateArray) { - stateToRender += levelSpacer(2, 2) + element + ';'; - } - return stateToRender; - }; - const enrichedChildren: ChildElement[] = getEnrichedChildren(currComponent); - // import statements differ between root (pages) and regular components (components) - const importsMapped = - projectType === 'Next.js' || projectType === 'Gatsby.js' - ? imports - .map((comp: string) => { - return isRoot - ? `import ${comp} from '../components/${comp}'` - : `import ${comp} from './${comp}'`; - }) - .join('\n') - : imports - .map((comp: string) => { - return `import ${comp} from './${comp}'`; - }) - .join('\n'); - const createState = (stateProps: StateProp[]) => { - let state = '{'; - stateProps.forEach((ele) => { - state += ele.key + ':' + JSON.stringify(ele.value) + ', '; - }); - state = state.substring(0, state.length - 2) + '}'; - return state; - }; - - // create final component code. component code differs between classic react, next.js, gatsby.js - // classic react code - if (projectType === 'Classic React') { - //string to store all imports string for context - let contextImports = ''; - const { allContext } = contextParam; - - for (const context of allContext) { - contextImports += `import ${context.name}Provider from '../contexts/${context.name}.js'\n`; - } - - //build an object with keys representing all components, their values are arrays storing all contexts that those components are consuming - const componentContext = allContext.reduce((acc, curr) => { - for (const component of curr.components) { - if (acc[component] === undefined) acc[component] = []; - acc[component].push(curr.name); - } - return acc; - }, {}); - - //return a string with all contexts provider in component's body - const createRender = () => { - let result = `${writeNestedElements(enrichedChildren)}`; - if (importReactRouter) result = `\n ${result}\n `; - if (allContext.length < 1) return result; - - if (currComponent.name === 'App') { - allContext.reverse().forEach((el, i) => { - let tabs = `\t\t`; - if (i === allContext.length - 1) { - tabs = `\t\t\t`; - } - result = `${tabs.repeat(allContext.length - i)}<${ - el.name - }Provider>\n ${result}\n ${tabs.repeat(allContext.length - i)}`; - }); - } - return result; - }; - - //decide which imports statements to use for which components - const createContextImport = () => { - if (!(currComponent.name in componentContext)) return ''; - - let importStr = ''; - componentContext[currComponent.name].forEach((context) => { - importStr += `import { ${context} } from '../contexts/${context}.js'\n`; - }); - - return importStr; - }; - - //call use context hooks for components that are consuming contexts - //LEGACY PD: - const createUseContextHook = () => { - if (!(currComponent.name in componentContext)) return ''; - - let importStr = ''; - componentContext[currComponent.name].forEach((context) => { - importStr += ` const [${context}Val] = useContext(${context})\n`; - }); - - return importStr; - }; - - const createEventHandler = (children: ChildElement[]) => { - let importStr = ''; - children.map((child) => { - if (child.type === 'HTML Element') { - if (child.events) { - for (const [event, funcName] of Object.entries(child.events)) { - importStr += `\tconst ${funcName} = () => {};\n`; - } - } - if (child.children.length !== 0) - importStr += createEventHandler(child.children); - } - }); - - return importStr; - }; - - let generatedCode = - "import React, { useState, useEffect, useContext} from 'react';\n\n"; - generatedCode += currComponent.name === 'App' ? contextImports : ''; - generatedCode += importReactRouter - ? `import { BrowserRouter as Router, Route, Switch, Link } from 'react-router-dom';\n` - : ``; - generatedCode += createContextImport() ? `${createContextImport()}\n` : ''; - generatedCode += importsMapped ? `${importsMapped}\n` : ''; - // below is the return statement of the codepreview - generatedCode += `const ${currComponent.name} = (props) => {\n`; - generatedCode += writeStateProps(currComponent.useStateCodes) - ? `\t${writeStateProps(currComponent.useStateCodes)}\n` - : ''; - generatedCode += createEventHandler(enrichedChildren) - ? `${createEventHandler(enrichedChildren)}\n` - : ''; - generatedCode += ` - return( - <> - ${createRender()} - - );`; - generatedCode += `\n}`; - return generatedCode; - } else if (projectType === 'Next.js') { - return ` - import React, { useState } from 'react'; - ${importsMapped} - import Head from 'next/head' - ${links ? `import Link from 'next/link'` : ``} - ${images ? `import Image from 'next/image'` : ``} - - const ${ - currComponent.name[0].toUpperCase() + currComponent.name.slice(1) - } = (props): JSX.Element => { - return ( - <> - ${ - isRoot - ? ` - - ${currComponent.name} - ` - : `` - } - ${writeNestedElements(enrichedChildren)} - - ); - } - export default ${ - currComponent.name[0].toUpperCase() + currComponent.name.slice(1) - }; - `; - } else { - // gatsby component code - return ` - import React, { useState } from 'react'; - ${importsMapped} - import { StaticQuery, graphql } from 'gatsby'; - ${links ? `import { Link } from 'gatsby'` : ``} - const ${currComponent.name} = (props: any): JSX.Element => { - return ( - <> - ${ - isRoot - ? ` - ${currComponent.name} - ` - : `` - } -
- ${writeNestedElements(enrichedChildren)} -
- - ); - } - export default ${currComponent.name}; - `; - } -}; -// formats code with prettier linter -const formatCode = (code: string) => { - if (import.meta.env.NODE_ENV === 'test') { - const { format } = require('prettier'); - return format(code, { - singleQuote: true, - trailingComma: 'es5', - bracketSpacing: true, - jsxBracketSameLine: true, - parser: 'babel' - }); - } else { - return code; - } -}; -export default generateCode; +import { + Component, + ChildElement, + HTMLType, + MUIType, + ChildStyle, + StateProp +} from '../interfaces/Interfaces'; +declare global { + interface Window { + api: any; + } +} +// generate code based on component hierarchy and then return the rendered code +const generateCode = ( + components: Component[], + componentId: number, + rootComponents: number[], + projectType: string, + HTMLTypes: HTMLType[], + MUITypes: MUIType[], + tailwind: boolean, + contextParam: any +) => { + const code = generateUnformattedCode( + components, + componentId, + rootComponents, + projectType, + HTMLTypes, + MUITypes, + tailwind, + contextParam + ); + return formatCode(code); +}; + +// generate code based on the component hierarchy +const generateUnformattedCode = ( + comps: Component[], + componentId: number, + rootComponents: number[], + projectType: string, + HTMLTypes: HTMLType[], + MUITypes: MUIType[], + tailwind: boolean, + contextParam: any +) => { + const components = [...comps]; + // find the component that we're going to generate code for + const currComponent = components.find((elem) => elem.id === componentId); + // find the unique components that we need to import into this component file + let imports: any = []; + let providers: string = ''; + let context: string = ''; + let links: boolean = false; + let images: boolean = false; + const isRoot = rootComponents.includes(componentId); + let importReactRouter = false; + // returns an array of objects which may include components, html elements, and/or route links + const getEnrichedChildren = (currentComponent: Component | ChildElement) => { + // declare an array of enriched children + const enrichedChildren = currentComponent.children?.map((elem: any) => { + //enrichedChildren is iterating through the children array + const child: ChildElement = { ...elem }; + // check if child is a component + if (child.type === 'Component') { + // verify that the child is in the components array in state + const referencedComponent = components.find( + (elem) => elem.id === child.typeId + ); + // check if imports array include the referenced component, if not, add its name to the imports array (e.g. the name/tag of the component/element) + if (!imports.includes(referencedComponent.name)) + imports.push(referencedComponent.name); + child['name'] = referencedComponent.name; + return child; + } else if (child.type === 'HTML Element') { + const referencedHTML = HTMLTypes.find( + (elem) => elem.id === child.typeId + ); + child['tag'] = referencedHTML.tag; + if ( + referencedHTML.tag === 'h1' || + referencedHTML.tag === 'h2' || + referencedHTML.tag === 'a' || + referencedHTML.tag === 'p' || + referencedHTML.tag === 'button' || + referencedHTML.tag === 'span' || + referencedHTML.tag === 'div' || + referencedHTML.tag === 'separator' || + referencedHTML.tag === 'form' || + referencedHTML.tag === 'ul' || + referencedHTML.tag === 'ol' || + referencedHTML.tag === 'menu' || + referencedHTML.tag === 'li' || + referencedHTML.tag === 'Link' || + referencedHTML.tag === 'Switch' || + referencedHTML.tag === 'Route' + ) { + child.children = getEnrichedChildren(child); + } + + // when we see a Switch or LinkTo, import React Router + if ( + (referencedHTML.tag === 'Switch' || referencedHTML.tag === 'Route') && + projectType === 'Classic React' + ) + importReactRouter = true; + else if (referencedHTML.tag === 'Link') links = true; + if (referencedHTML.tag === 'Image') images = true; + return child; + } else if (child.type === 'MUI Component') { + const referencedMUI = MUITypes.find((elem) => elem.id === child.typeId); + child['tag'] = referencedMUI.tag; + if ( + referencedMUI.tag === 'button' || + referencedMUI.tag === 'card' || + referencedMUI.tag === 'typography' || + referencedMUI.tag === 'input' || + referencedMUI.tag === 'Link' || + referencedMUI.tag === 'Switch' || + referencedMUI.tag === 'Route' + ) { + child.children = getEnrichedChildren(child); + } + + // when we see a Switch or LinkTo, import React Router + if ( + (referencedMUI.tag === 'Switch' || referencedMUI.tag === 'Route') && + projectType === 'Classic React' + ) + importReactRouter = true; + else if (referencedMUI.tag === 'Link') links = true; + if (referencedMUI.tag === 'Image') images = true; + return child; + } else if (child.type === 'Route Link') { + links = true; + child.name = components.find( + (comp: Component) => comp.id === child.typeId + ).name; + return child; + } + }); + return enrichedChildren; + }; + // Raised formatStyles so that it is declared before it is referenced. It was backwards. + // format styles stored in object to match React inline style format + const formatStyles = (styleObj: ChildStyle) => { + if (Object.keys(styleObj).length === 0) return ``; + const formattedStyles: String[] = []; + let styleString: String; + for (let i in styleObj) { + if (i === 'style') { + styleString = i + '=' + '{' + JSON.stringify(styleObj[i]) + '}'; + formattedStyles.push(styleString); + } + } + return formattedStyles; + }; + // function to dynamically add classes, ids, and styles to an element if it exists. + //LEGACY PD: CAN ADD PROPS HERE AS JSX ATTRIBUTE + const elementTagDetails = (childElement: ChildElement) => { + let customizationDetails = ''; + let passedInPropsString = ''; + if (childElement.type === 'Component') { + let childComponent: Component; + for (let i = 0; i < components.length; i++) { + if (childElement.name === components[i].name) { + childComponent = components[i]; + } + } + childComponent.passedInProps.forEach((prop) => { + passedInPropsString += `${prop.key} = {${prop.key}} `; + }); + } + + if (childElement.childId && childElement.tag !== 'Route') + customizationDetails += + ' ' + `id="${+childElement.childId}" ` + `${passedInPropsString}`; + + if ( + childElement.attributes && + childElement.attributes.cssClasses && + !tailwind + ) { + customizationDetails += + ' ' + `className="${childElement.attributes.cssClasses}"`; + } + if ( + childElement.style && + Object.keys(childElement.style).length > 0 && + tailwind === false + ) + customizationDetails += ' ' + formatStyles(childElement.style); + if ( + childElement.style && + Object.keys(childElement.style).length > 0 && + tailwind === true + ) { + let { + height, + alignItems, + backgroundColor, + display, + flexDirection, + width, + justifyContent + } = childElement.style; + let w: String, + h: String, + items: String, + bg: String, + d: String, + flexDir: String, + justCon: String, + cssClasses: String; + if (childElement.style.alignItems) { + if (alignItems === 'center') items = 'items-center '; + else if (alignItems === 'flex-start') items = 'items-start '; + else if (alignItems === 'flex-end') items = 'items-end '; + else if (alignItems === 'stretch') items = 'items-stretch '; + } + if (childElement.style.backgroundColor) { + bg = `bg-[${backgroundColor}] `; + } + if (childElement.style.display) { + if (display === 'flex') d = 'flex '; + else if (display === 'inline-block') d = 'inline-block '; + else if (display === 'block') d = 'block '; + else if (display === 'none') d = 'hidden '; + } + if (childElement.style.flexDirection) { + if (flexDirection === 'column') flexDir = 'flex-col '; + } + if (childElement.style.height) { + if (height === '100%') h = 'h-full '; + else if (height === '50%') h = 'h-1/2 '; + else if (height === '25%') h = 'h-1/4 '; + else if (height === 'auto') h = 'auto '; + } + if (childElement.style.justifyContent) { + if (justifyContent === 'center') justCon = 'justify-center '; + else if (justifyContent === 'flex-start') justCon = 'justify-start '; + else if (justifyContent === 'space-between') + justCon = 'justify-between '; + else if (justifyContent === 'space-around') justCon = 'justify-around '; + else if (justifyContent === 'flex-end') justCon = 'justify-end '; + else if (justifyContent === 'space-evenly') justCon = 'justify-evenly '; + } + if (childElement.style.width) { + if (width === '100%') w = 'w-full '; + else if (width === '50%') w = 'w-1/2 '; + else if (width === '25%') w = 'w-1/4 '; + else if (width === 'auto') w = 'w-auto '; + } + if (childElement.attributes && childElement.attributes.cssClasses) { + cssClasses = `${childElement.attributes.cssClasses} `; + } + customizationDetails += + ' ' + + `className = "${cssClasses ? cssClasses : ''} ${w ? w : ''}${ + h ? h : '' + }${justCon ? justCon : ''}${flexDir ? flexDir : ''}${d ? d : ''}${ + bg ? bg : '' + }${items ? items : ''}"`; + } + + if (childElement.events && Object.keys(childElement.events).length > 0) { + // SPACE BETWEEN ATTRIBUTE EXPRESSIONS + for (const [event, funcName] of Object.entries(childElement.events)) { + customizationDetails += ' ' + `${event}={(event) => ${funcName}()}`; + } + } + + return customizationDetails; + }; + // function to fix the spacing of the ace editor for new lines of added content. This was breaking on nested components, leaving everything right justified. + const tabSpacer = (level: number) => { + let tabs = ' '; + for (let i = 0; i < level; i++) tabs += ' '; + return tabs; + }; + // function to dynamically generate the appropriate levels for the code preview + const levelSpacer = (level: number, spaces: number) => { + if (level === 2) return `\n${tabSpacer(spaces)}`; + else return ''; + }; + // function to dynamically generate a complete html (& also other library type) elements + const elementGenerator = (childElement: ChildElement, level: number = 2) => { + let innerText = ''; + let activeLink = '""'; + if (childElement.attributes && childElement.attributes.compText) { + if (childElement.stateUsed && childElement.stateUsed.compText) { + innerText = '{' + childElement.stateUsed.compText + '}'; + } else { + innerText = childElement.attributes.compText; + } + } + if (childElement.attributes && childElement.attributes.compLink) { + if (childElement.stateUsed && childElement.stateUsed.compLink) { + activeLink = '{' + childElement.stateUsed.compLink + '}'; + } else { + activeLink = '"' + childElement.attributes.compLink + '"'; + } + } + + const nestable = + childElement.tag === 'h1' || + childElement.tag === 'h2' || + childElement.tag === 'a' || + childElement.tag === 'span' || + childElement.tag === 'button' || + childElement.tag === 'p' || + childElement.tag === 'div' || + childElement.tag === 'form' || + childElement.tag === 'ol' || + childElement.tag === 'ul' || + childElement.tag === 'menu' || + childElement.tag === 'li' || + childElement.tag === 'Switch' || + childElement.tag === 'Route'; + + if (childElement.tag === 'img') { + return `${levelSpacer(level, 5)}<${ + childElement.tag + } src=${activeLink} ${elementTagDetails(childElement)}/>${levelSpacer( + 2, + 3 + level + )}`; + } else if (childElement.tag === 'a') { + return `${levelSpacer(level, 5)}<${ + childElement.tag + } href=${activeLink} ${elementTagDetails(childElement)}>${innerText}${levelSpacer(2, 3 + level)}`; + } else if (childElement.tag === 'input') { + return `${levelSpacer(level, 5)}<${childElement.tag}${elementTagDetails( + childElement + )}>${levelSpacer(2, 3 + level)}`; + } else if (childElement.tag === 'Link' && projectType === 'Classic React') { + return `${levelSpacer(level, 5)} + ${tabSpacer(level)}${writeNestedElements( + childElement.children, + level + 1 + )}${innerText} + ${tabSpacer(level - 1)}${levelSpacer(2, 3 + level)}`; + } else if (childElement.tag === 'Link' && projectType === 'Next.js') { + return `${levelSpacer( + level, + 5 + )} + ${tabSpacer(level)}${innerText}${writeNestedElements( + childElement.children, + level + 1 + )} + ${tabSpacer(level - 1)}${levelSpacer(2, 3 + level)}`; + } else if (childElement.tag === 'Image') { + return `${levelSpacer(level, 5)}<${ + childElement.tag + } src=${activeLink} ${elementTagDetails(childElement)}/>`; + } else if (nestable) { + if ( + (childElement.tag === 'Route' || childElement.tag === 'Switch') && + projectType === 'Next.js' + ) { + return `${writeNestedElements(childElement.children, level)}`; + } + const routePath = + childElement.tag === 'Route' ? ' ' + 'exact path=' + activeLink : ''; + return `${levelSpacer(level, 5)}<${childElement.tag}${elementTagDetails( + childElement + )}${routePath}> + ${tabSpacer(level)}${innerText} + ${tabSpacer(level)}${writeNestedElements( + childElement.children, + level + 1 + )} + ${tabSpacer(level - 1)}${levelSpacer( + 2, + 3 + level + )}`; + } else if (childElement.tag !== 'separator') { + return `${levelSpacer(level, 5)}<${childElement.tag}${elementTagDetails( + childElement + )}>${innerText}${levelSpacer(2, 3 + level)}`; + } + }; + // write all code that will be under the "return" of the component + const writeNestedElements = (enrichedChildren: any, level: number = 2) => { + return `${enrichedChildren + .map((child: ChildElement) => { + if (child.type === 'Component') { + return `<${child.name} ${elementTagDetails(child)}/>`; + } else if (child.type === 'HTML Element') { + return elementGenerator(child, level); + } + // route links are for gatsby.js and next.js feature. if the user creates a route link and then switches projects, generate code for a normal link instead + else if (child.type === 'Route Link') { + if (projectType === 'Next.js') { + // if route link points to index, to go endpoint / rather than /index + if (child.name === 'index') + return ``; + else + return ``; + } else if (projectType === 'Gatsby.js') { + if (child.name === 'index') + return `
${child.name}
`; + else + return `
${child.name}
`; + } else return ``; + } + }) + .filter((element) => !!element) + .join('')}`; + }; + // function to properly incorporate the user created state that is stored in the application state + const writeStateProps = (stateArray: String[]) => { + let stateToRender: String = ''; + for (const element of stateArray) { + stateToRender += levelSpacer(2, 2) + element + ';'; + } + return stateToRender; + }; + const enrichedChildren: ChildElement[] = getEnrichedChildren(currComponent); + // import statements differ between root (pages) and regular components (components) + const importsMapped = + projectType === 'Next.js' || projectType === 'Gatsby.js' + ? imports + .map((comp: string) => { + return isRoot + ? `import ${comp} from '../components/${comp}'` + : `import ${comp} from './${comp}'`; + }) + .join('\n') + : imports + .map((comp: string) => { + return `import ${comp} from './${comp}'`; + }) + .join('\n'); + const createState = (stateProps: StateProp[]) => { + let state = '{'; + stateProps.forEach((ele) => { + state += ele.key + ':' + JSON.stringify(ele.value) + ', '; + }); + state = state.substring(0, state.length - 2) + '}'; + return state; + }; + + // create final component code. component code differs between classic react, next.js, gatsby.js + // classic react code + if (projectType === 'Classic React') { + //string to store all imports string for context + let contextImports = ''; + const { allContext } = contextParam; + + for (const context of allContext) { + contextImports += `import ${context.name}Provider from '../contexts/${context.name}.js'\n`; + } + + //build an object with keys representing all components, their values are arrays storing all contexts that those components are consuming + const componentContext = allContext.reduce((acc, curr) => { + for (const component of curr.components) { + if (acc[component] === undefined) acc[component] = []; + acc[component].push(curr.name); + } + return acc; + }, {}); + + //return a string with all contexts provider in component's body + const createRender = () => { + let result = `${writeNestedElements(enrichedChildren)}`; + if (importReactRouter) result = `\n ${result}\n `; + if (allContext.length < 1) return result; + + if (currComponent.name === 'App') { + allContext.reverse().forEach((el, i) => { + let tabs = `\t\t`; + if (i === allContext.length - 1) { + tabs = `\t\t\t`; + } + result = `${tabs.repeat(allContext.length - i)}<${ + el.name + }Provider>\n ${result}\n ${tabs.repeat(allContext.length - i)}`; + }); + } + return result; + }; + + //decide which imports statements to use for which components + const createContextImport = () => { + if (!(currComponent.name in componentContext)) return ''; + + let importStr = ''; + componentContext[currComponent.name].forEach((context) => { + importStr += `import { ${context} } from '../contexts/${context}.js'\n`; + }); + + return importStr; + }; + + //call use context hooks for components that are consuming contexts + //LEGACY PD: + const createUseContextHook = () => { + if (!(currComponent.name in componentContext)) return ''; + + let importStr = ''; + componentContext[currComponent.name].forEach((context) => { + importStr += ` const [${context}Val] = useContext(${context})\n`; + }); + + return importStr; + }; + + const createEventHandler = (children: ChildElement[]) => { + let importStr = ''; + children.map((child) => { + if (child.type === 'HTML Element') { + if (child.events) { + for (const [event, funcName] of Object.entries(child.events)) { + importStr += `\tconst ${funcName} = () => {};\n`; + } + } + if (child.children.length !== 0) + importStr += createEventHandler(child.children); + } + if (child.type === 'MUI Component') { + if (child.events) { + for (const [event, funcName] of Object.entries(child.events)) { + importStr += `\tconst ${funcName} = () => {};\n`; + } + } + if (child.children.length !== 0) + importStr += createEventHandler(child.children); + } + }); + + return importStr; + }; + + let generatedCode = + "import React, { useState, useEffect, useContext} from 'react';\n\n"; + generatedCode += currComponent.name === 'App' ? contextImports : ''; + generatedCode += importReactRouter + ? `import { BrowserRouter as Router, Route, Switch, Link } from 'react-router-dom';\n` + : ``; + generatedCode += createContextImport() ? `${createContextImport()}\n` : ''; + generatedCode += importsMapped ? `${importsMapped}\n` : ''; + // below is the return statement of the codepreview + generatedCode += `const ${currComponent.name} = (props) => {\n`; + generatedCode += writeStateProps(currComponent.useStateCodes) + ? `\t${writeStateProps(currComponent.useStateCodes)}\n` + : ''; + generatedCode += createEventHandler(enrichedChildren) + ? `${createEventHandler(enrichedChildren)}\n` + : ''; + generatedCode += ` + return( + <> + ${createRender()} + + );`; + generatedCode += `\n}`; + return generatedCode; + } else if (projectType === 'Next.js') { + return ` + import React, { useState } from 'react'; + ${importsMapped} + import Head from 'next/head' + ${links ? `import Link from 'next/link'` : ``} + ${images ? `import Image from 'next/image'` : ``} + + const ${ + currComponent.name[0].toUpperCase() + currComponent.name.slice(1) + } = (props): JSX.Element => { + return ( + <> + ${ + isRoot + ? ` + + ${currComponent.name} + ` + : `` + } + ${writeNestedElements(enrichedChildren)} + + ); + } + export default ${ + currComponent.name[0].toUpperCase() + currComponent.name.slice(1) + }; + `; + } else { + // gatsby component code + return ` + import React, { useState } from 'react'; + ${importsMapped} + import { StaticQuery, graphql } from 'gatsby'; + ${links ? `import { Link } from 'gatsby'` : ``} + const ${currComponent.name} = (props: any): JSX.Element => { + return ( + <> + ${ + isRoot + ? ` + ${currComponent.name} + ` + : `` + } +
+ ${writeNestedElements(enrichedChildren)} +
+ + ); + } + export default ${currComponent.name}; + `; + } +}; +// formats code with prettier linter +const formatCode = (code: string) => { + if (import.meta.env.NODE_ENV === 'test') { + const { format } = require('prettier'); + return format(code, { + singleQuote: true, + trailingComma: 'es5', + bracketSpacing: true, + jsxBracketSameLine: true, + parser: 'babel' + }); + } else { + return code; + } +}; +export default generateCode; diff --git a/app/src/helperFunctions/renderChildren.tsx b/app/src/helperFunctions/renderChildren.tsx index 33844c64..9a19901a 100644 --- a/app/src/helperFunctions/renderChildren.tsx +++ b/app/src/helperFunctions/renderChildren.tsx @@ -1,166 +1,231 @@ -import React from 'react'; -import { ChildElement } from '../interfaces/Interfaces'; -import DirectChildComponent from '../components/main/DirectChildComponent'; -import DirectChildHTML from '../components/main/DirectChildHTML'; -import DirectChildHTMLNestable from '../components/main/DirectChildHTMLNestable'; -import SeparatorChild from '../components/main/SeparatorChild'; -import RouteLink from '../components/main/RouteLink'; -import { useSelector } from 'react-redux'; -import { RootState } from '../redux/store'; - -// helper method to render all direct children of a component -// direct children are clickable and draggable -// direct children may also have their own indirect children (grandchildren, great-grandchildren, etc) which are not draggable and clickable -// there are four types of direct children that can be rendered on the screen -const renderChildren = (children: ChildElement[]) => { - const state = useSelector((store: RootState) => store.appState); - - return children.map((child: ChildElement, i: number) => { - const { type, style, childId, children, attributes, name } = child; - let { typeId } = child; - if (name === '') child.name = state.components[typeId - 1].name; - // A DirectChildComponent is an instance of a top level component - // This component will render IndirectChild components (div/components rendered inside a child component) - // Removed style from prop drills so that styling isn't applied to canvas items. - // Also added keys & removed an unnecessary div around DirChildNestables that was causing errors. - if (type === 'Component') { - return ( - - ); - } - // child is a non-nestable type of HTML element (aka NOT divs, forms, OrderedLists, UnorderedLists, menus) - else if ( - type === 'HTML Element' && - typeId !== 11 && - typeId !== 1000 && - typeId !== 1 && - typeId !== 2 && - typeId !== 3 && - typeId !== 4 && - typeId !== 5 && - typeId !== 6 && - typeId !== 8 && - typeId !== 9 && - typeId !== 14 && - typeId !== 15 && - typeId !== 16 && - typeId !== 17 && - typeId !== 18 && - typeId !== -1 && - typeId !== 19 - ) { - return ( - - ); - } - // child is a nestable type of HTML element (divs, forms, OrderedLists, UnorderedLists, menus) - else if ( - type === 'HTML Element' && - (typeId === 11 || - typeId === 1 || - typeId === 2 || - typeId === 3 || - typeId === 4 || - typeId === 5 || - typeId === 6 || - typeId === 8 || - typeId === 9 || - typeId === 14 || - typeId === 15 || - typeId === 16 || - typeId === 17 || - typeId === 18 || - typeId === -1 || - typeId === 19) - ) { - if ( - (typeId === 18 || typeId === 19) && - state.projectType === 'Classic React' - ) - typeId = 18; - if ((typeId === 17 || typeId === -1) && state.projectType === 'Next.js') - return renderChildren(children); - if ((typeId === 18 || typeId === 19) && state.projectType === 'Next.js') - typeId = 19; - return ( - - ); - } else if (type === 'HTML Element' && typeId === 1000) { - return ( - - ); - } - // A route link is a next.js or gatsby.js navigation link - // The route link component includes a clickable link that, when clicked, will change the user focus to the referenced component - else if (type === 'Route Link') { - return ( - - ); - } - }); -}; -// removed style from prop drilling -export default renderChildren; +import React from 'react'; +import { ChildElement } from '../interfaces/Interfaces'; +import DirectChildComponent from '../components/main/DirectChildComponent'; +import DirectChildHTML from '../components/main/DirectChildHTML'; +import DirectChildHTMLNestable from '../components/main/DirectChildHTMLNestable'; +import DirectChildMUI from '../components/main/DirectChildMUI'; +import DirectChildMUINestable from '../components/main/DirectChildMUINestable'; +import SeparatorChild from '../components/main/SeparatorChild'; +import RouteLink from '../components/main/RouteLink'; +import { useSelector } from 'react-redux'; +import { RootState } from '../redux/store'; + +// helper method to render all direct children of a component +// direct children are clickable and draggable +// direct children may also have their own indirect children (grandchildren, great-grandchildren, etc) which are not draggable and clickable +// there are four types of direct children that can be rendered on the screen +const renderChildren = (children: ChildElement[]) => { + const state = useSelector((store: RootState) => store.appState); + + return children.map((child: ChildElement, i: number) => { + const { type, style, childId, children, attributes, name } = child; + // console.log('state components', state.components); + // console.log('state MUI type', state.MUITypes); + let { typeId } = child; + // console.log('typeID', typeId); + // console.log('name', name); + // console.log('child.name', state.components[typeId - 1].name); + if (name === '') child.name = state.components[typeId - 1].name; + // A DirectChildComponent is an instance of a top level component + // This component will render IndirectChild components (div/components rendered inside a child component) + // Removed style from prop drills so that styling isn't applied to canvas items. + // Also added keys & removed an unnecessary div around DirChildNestables that was causing errors. + if (type === 'Component') { + return ( + + ); + } + // child is a non-nestable type of HTML element + // nestable = false -> input(10), img(12), image(20) + else if ( + type === 'HTML Element' && + typeId !== 11 && + typeId !== 1000 && + typeId !== 1 && + typeId !== 2 && + typeId !== 3 && + typeId !== 4 && + typeId !== 5 && + typeId !== 6 && + typeId !== 8 && + typeId !== 9 && + typeId !== 14 && + typeId !== 15 && + typeId !== 16 && + typeId !== 17 && + typeId !== 18 && + typeId !== -1 && + typeId !== 19 + ) { + return ( + + ); + } + // child is a nestable type of HTML element (divs, forms, OrderedLists, UnorderedLists, menus) + else if ( + type === 'HTML Element' && + (typeId === 11 || + typeId === 1 || + typeId === 2 || + typeId === 3 || + typeId === 4 || + typeId === 5 || + typeId === 6 || + typeId === 8 || + typeId === 9 || + typeId === 14 || + typeId === 15 || + typeId === 16 || + typeId === 17 || + typeId === 18 || + typeId === -1 || + typeId === 19) + ) { + if ( + (typeId === 18 || typeId === 19) && + state.projectType === 'Classic React' + ) + typeId = 18; + if ((typeId === 17 || typeId === -1) && state.projectType === 'Next.js') + return renderChildren(children); + if ((typeId === 18 || typeId === 19) && state.projectType === 'Next.js') + typeId = 19; + return ( + + ); + } else if (type === 'MUI Component' && typeId === 31) { + return ( + + ); + } + // child is a nestable type of HTML element (divs, forms, OrderedLists, UnorderedLists, menus) + else if ( + type === 'MUI Component' && + (typeId === 21 || typeId === 41 || typeId === 51) + ) { + return ( + + ); + } else if (type === 'MUI Component' && typeId === 1000) { + return ( + + ); + } else if (type === 'HTML Element' && typeId === 1000) { + return ( + + ); + } + // A route link is a next.js or gatsby.js navigation link + // The route link component includes a clickable link that, when clicked, will change the user focus to the referenced component + else if (type === 'Route Link') { + return ( + + ); + } + }); +}; +// removed style from prop drilling +export default renderChildren; diff --git a/app/src/interfaces/Interfaces.ts b/app/src/interfaces/Interfaces.ts index 5e49e324..1c38b7f9 100644 --- a/app/src/interfaces/Interfaces.ts +++ b/app/src/interfaces/Interfaces.ts @@ -17,6 +17,7 @@ export interface State { nextBottomSeparatorId?: number; nextChildId: number; HTMLTypes: HTMLType[]; + MUITypes: MUIType[]; tailwind: boolean; stylesheet: string; codePreview: boolean; @@ -114,6 +115,7 @@ export interface MUIType { framework: string; nestable: boolean; imports: any[]; + props: any[]; } export interface DragItem extends DragObjectWithType { newInstance: boolean; diff --git a/app/src/public/styles/style.css b/app/src/public/styles/style.css index 8ae9bfa2..b349080e 100644 --- a/app/src/public/styles/style.css +++ b/app/src/public/styles/style.css @@ -1,911 +1,937 @@ -@import url('https://fonts.googleapis.com/css?family=Open+Sans&display=swap'); -@import url('https://fonts.googleapis.com/css2?family=Raleway&display=swap'); -@import url('https://fonts.googleapis.com/css2?family=Roboto&display=swap'); -@import url('https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@48,400,0,0'); -/* @import url('https: //fonts.googleapis.com/css2?family=Fira+Code:wght@300;400;500;600;700&display=swap'); */ - -*, -*::before, -*::after { - box-sizing: inherit; -} - -html { - box-sizing: border-box; - overflow: auto; -} - -body { - margin: 0; - padding: 0; - font-family: 'Roboto', 'Raleway', sans-serif; - font-size: 0.9 em; - font-weight: normal; - overflow: hidden; - height: 100vh; - width: 100vw; - background-color: #191919; -} - -h4 { - color: #155084; - text-align: center; -} - -.creation-panel { - color: black; - display: flex; - flex-direction: row; - height: 100%; - width: 100%; -} - -.customization-section { - color: black; - display: flex; - flex-direction: row; - height: 100%; -} - -.state-panel { - margin-top: -15px; - flex-grow: 2; - display: flex; - flex-direction: row; - justify-content: space-evenly; -} - -.state-prop-grid { - height: 300px; - width: 410px; - flex-grow: 1; - padding-left: 10px; - padding-right: 10px; -} - -.HTMLItemCreate { - flex-grow: 1; - justify-content: center; -} - -:disabled { - cursor: not-allowed; -} - -header { - display: none; -} - -.app-container { - display: flex; - width: 100%; - height: calc(100vh - 64px); -} - -.mode-change-button { - display: flex; - align-content: center; - padding-top: 50px; -} - -.progress-bar { - flex-grow: 1; -} - -.column { - background-color: #1e2024; - margin-right: 1.5px; -} - -/* -///////////////////////////////////////////////// -LEFT COLUMN -///////////////////////////////////////////////// -*/ - -.app-container { - position: relative; -} - -.left-container { - display: flex; - z-index: 1; -} - -span.material-symbols-outlined { - padding: 1px; - font-size: 18px; -} - -.left { - padding: 0px; - display: flex; - height: 100%; - width: 280px; - min-width: 225px; - flex-direction: column; - align-content: center; - position: relative; - overflow-x: hidden; -} - -.column.left::-webkit-scrollbar { - width: 0.5rem; - height: 0.5rem; -} - -.column.left::-webkit-scrollbar-thumb { - transition: 0.3s ease all; - border-color: transparent; - background-color: #1e2024; - border-radius: 8px; - z-index: 40; -} - -.hide-show { - position: absolute; - height: 100%; - left: -252px; - transition: all 0.5s ease-in-out; -} - -.hide-show:hover { - left: 0px; - z-index: 289; -} - -.HTMLItems { - display: flex; - flex-direction: column; - justify-content: space-around; - flex-wrap: wrap; - width: 100%; - flex-grow: 1; -} - -.HTMLItemsDark { - display: flex; - flex-direction: column; - justify-content: space-around; - flex-wrap: wrap; - flex-grow: 1; - border: 3px solid white; -} - -#Half { - display: flex; - flex-direction: column; - justify-content: top; - height: 60vh; - text-align: center; - overflow-x: hidden; - overflow-y: auto; -} - -#HTMLItemsTopHalf::-webkit-scrollbar { - width: 0.8rem; - height: 0.5rem; -} - -#HTMLItemsTopHalf::-webkit-scrollbar-thumb { - transition: 0.3s ease all; - border-color: transparent; - background-color: #1e2024; - border-radius: 8px; - z-index: 40; -} - -#CompBottomHalf { - flex-grow: 1; - text-align: center; - overflow-x: hidden; - overflow-y: auto; -} - -.HTMLElements { - display: flex; - flex-direction: column; - align-items: flex-start; - flex-wrap: wrap; -} - -#HTMLgrid { - margin: 5px; -} - -#HTMLItemsGrid { - display: flex; -} - -#HTMLItem { - transition: 0.3s; - border: 1px solid transparent; - color: #f2fbf8; - padding: 10%; - border-radius: 10px; - box-shadow: #191919; -} - -#HTMLItem:hover { - background-color: #474545a8; - color: #8ca8ff; -} - -#submitButton { - margin-left: 5px; -} - -#submitButton:hover { - background-color: #0671e3; - color: white; - border: none; - cursor: pointer; -} - -.lineDiv { - width: 150%; - margin-left: 0px; -} - -.customForm { - display: flex; - flex-direction: column; - align-items: start; - color: white; - font-size: 95%; -} - -.customForm h4 { - align-self: center; -} - -#newElement { - border: none; - background: none; - display: flex; - align-items: baseline; - justify-content: flex-start; - width: 0.5em; -} - -/* span container in HTMLItem.tsx */ -#customHTMLElement { - display: flex; - flex-direction: row; - justify-content: center; - width: 125px; -} - -.component-input { - bottom: 2.8%; -} - -.clear-workspace { - top: 96%; - position: sticky !important; - z-index: 1; -} - -.hidden { - display: none; -} - -/* -///////////////////////////////////////////////// -MAIN COLUMN -//////////////////////////////////////////////// -*/ - -h1 { - color: rgb(75, 75, 75); -} - -.main-container { - display: flex; - flex-direction: column; - flex: 1; - min-width: 700px; -} - -.main-header { - background-color: #212121; - box-shadow: 0 5px 7px -2px rgba(0, 0, 0, 0.1); - z-index: 10; -} - -.main-header-buttons { - display: flex; - font-size: 12px; -} - -.main { - background: #fff; - flex: 1; - width: 100%; - overflow: auto; - display: flex; - background-color: #e4e4e4; -} - -.draggable { - position: relative; -} - -.image { - top: 0; - left: 0; - width: 100%; - position: relative; -} - -.image-container { - position: relative; - display: flex; - top: 0; - left: 0; -} - -.export-preference div span { - transition: 0.5s; -} - -#export-modal { - transition: 0.5s; -} - -#export-modal:hover { - background-color: #66c4eb; - cursor: pointer; -} -/* ///CHANGED COLOR COLOR */ -#export-modal:hover span { - color: white; -} - -.info { - width: 100%; - height: 100%; - display: flex; - justify-content: center; - align-items: center; - text-align: center; -} - -.ace_scrollbar { - display: none; -} - -.ace_print-margin { - display: none; -} - -/* -////////////////////////////////////////////////// -RIGHT COLUMN -///////////////////////////////////////////////// -// RIGHT NOW, I'M NOT SURE WHAT RIGHT COL IS REFERING TO -*/ -#rightContainer { - display: flex; - flex-direction: row; - height: 100%; - width: 99.9%; -} - -.right { - padding-top: 0px; - width: 100%; - height: 100%; -} - -.rightContainer { - margin-top: 65px; - color: black; -} - -.rightPanelWrapper { - display: flex; - flex-direction: row; - justify-content: center; - height: 100%; - width: 100%; -} - -#addComponentButton { - height: 49px; - width: 130px; - background-color: #0671e3; - border: 2px solid #0671e3; - border-radius: 10px; - font-size: 16.5px; -} - -#addComponentButton:hover { - background-color: #25366d; - border: 2px solid #354e9c; - color: white; - cursor: pointer; -} - -.export { - border-top: 1px solid #ccc; - display: flex; - justify-content: center; - align-items: center; - flex-direction: row; -} - -.makeStyles-panelWrapperList-4::-webkit-scrollbar-track { - background-color: rgba(25, 25, 25, 0.8); -} - -.compPanelItem { - height: 100%; - display: flex; - justify-content: center; - align-content: center; - border-color: #0671e3; -} - -.compPanelItem h3 { - font-size: 1em; - letter-spacing: 0.75px; - padding: 5px 10px; -} - -.compPanelItem:hover { - cursor: pointer; -} - -/* -///////////////////////////////////////////////////// -BOTTOM PANEL -///////////////////////////////////////////////////// -*/ - -.tab-content { - height: calc(100% - 76px); - width: 100%; - overflow-y: scroll; - overflow-x: auto; - position: relative; - z-index: 0; - background-color: #1e2024; -} - -.tab-content::-webkit-scrollbar { - width: 0.8rem; - height: 0.5rem; -} - -.tab-content::-webkit-scrollbar-thumb { - transition: 0.3s ease all; - border-color: transparent; - background-color: gray; - border-radius: 8px; - z-index: 40; -} - -.tab-content::-webkit-scrollbar { - width: 0.8rem; - height: 0.5rem; -} - -.tab-content::-webkit-scrollbar-thumb { - transition: 0.3s ease all; - border-color: transparent; - background-color: gray; - border-radius: 8px; - z-index: 40; -} - -.MuiDataGrid-menuList, -.MuiDataGrid-columnsPanel, -.MuiDataGrid-filterForm { - background-color: #333333; -} -.MuiDataGrid-panelHeader, -.MuiDataGrid-panelFooter { - background-color: #252525; -} - -#resize-drag { - cursor: row-resize; - height: 23px; - width: 75px; - background-color: #1e2024; - text-align: center; - border-radius: 7px 7px 0 0; - align-self: center; - margin-top: -25px; - color: silver; - user-select: none; - z-index: 1; -} - -.bottom-panel { - transition: width 250ms ease-in-out; - background-color: #0671e3; - width: 100%; - height: 100%; - display: flex; - flex-direction: column; -} - -.htmlattr { - height: 80%; - overflow-y: scroll; - margin-left: 50px; -} - -.props-container { - margin-left: 10px; - height: 33%; - width: 100%; -} - -.props-input { - margin-bottom: 15px; -} - -.chips { - height: 55%; - overflow: auto; -} - -.flex-container { - display: flex; - flex-direction: row; - padding-top: 5px; -} - -.flex1 { - padding: 10px 20px 10px 5px; -} - -.flex2 { - padding-right: 10px; - padding-left: 10px; - border: rgb(68, 68, 68) solid 1px; - border-radius: 5px; - background-color: #0671e3; -} - -.event-table { - height: 300px; - width: 410px; - flex-grow: 1; - padding-left: 25px; - padding-right: 25px; - margin-top: 20px; -} - -#message-container::-webkit-scrollbar { - width: 0.1rem; - height: 0.1rem; -} - -#message-container::-webkit-scrollbar-thumb { - transition: 0.3s ease all; - border-color: transparent; - background-color: #1e2024; - background-image: linear-gradient(180deg, #1e2024 15%, grey 15%, grey 85%, #1e2024 85%); - border-radius: 10px; - z-index: 40; - overflow-y: hidden; -} - -#message-input { - outline: none; -} - -.video-scrollable::-webkit-scrollbar { - width: 0.1rem; - height: 0.1rem; -} - -.video-scrollable::-webkit-scrollbar-thumb { - transition: 0.3s ease all; - border-color: transparent; - background-color: #1e2024; - background-image: linear-gradient(180deg, #1e2024 15%, #31343A 15%, #31343A 85%, #1e2024 85%); - border-radius: 10px; - z-index: 40; - overflow-y: hidden; -} - -/* -////////////////////////////////////////// -NAVBAR -////////////////////////////////////////// -*/ - -.main-navbar { - display: flex; - justify-content: space-between; - align-items: center; - z-index: 9999; -} - -.main-logo { - display: flex; - align-items: center; - gap: 10px; - margin-left: 15px; -} - -.main-navbar svg { - width: 30px; - height: 30px; - color: gray; - margin-right: 2vw; - cursor: pointer; -} - -.navDropDown { - position: absolute; - width: 230px; - border-radius: 8px; - right: 1vw; - z-index: 10; - background-color: #31343a; - top: 45px; - padding: 10px 0; - display: hidden; -} - -.hideNavDropDown { - display: none; -} - -.navDropDown button { - width: 100%; - background-color: #31343a; - color: #d9dada; - padding: 9px 25px; - display: flex; - align-items: center; - border: none; -} - -.navDropDown span { - margin-left: -25px; -} - -.navDropDown p { - margin: 3px 5%; - color: #d9dada; -} - -.navDropDown button:hover { - cursor: pointer; - background-color: #393c42; -} - -.navDropDown button svg { - width: 20px; - height: 20px; - color: gray; -} - -#navbarButton { - color: white; - margin: 0px 5px 0px 5px; -} - -#customized-menu { - width: 16%; -} - -#canvasContainer::-webkit-scrollbar { - width: 0.5rem; - height: 0.7rem; -} - -#canvasContainer::-webkit-scrollbar-thumb { - transition: 0.3s ease all; - border-color: transparent; - background-color: gray; - border-radius: 8px; - z-index: 40; -} - -/* -///////////////////////////////////////////////// -CANVAS WINDOW -///////////////////////////////////////////////// -*/ - -.delete-button-empty { - float: right; - background-color: rgba(0, 0, 0, 0); - border-radius: 50%; - border: none; - font-size: 15px; - color: white; -} - -.deleteIcon { - font-size: 18px; - color: gray; - cursor: pointer; -} - -.deleteIcon:hover { - color: white; -} - -.componentContainer { - display: flex; - flex-direction: column; - align-items: center; - font-family: 'Roboto', 'Raleway', sans-serif; - height: 100%; -} - -/* Material-UI */ -/* Sortable tree sorting */ -.sortable-tree { - height: 100%; - background-color: rgb(37, 37, 38); -} - -div.rst__rowContents { - width: 45px; - border: none; - color: #eee; - background-color: #333333; -} - -.rst__rowContentsDragDisabled { - background-color: #333333; - background-color: rgba(37, 37, 38, 0.4); -} - -.rst__moveHandle, -.rst__loadingHandle { - width: 40px; -} - -.rst_tree { - overflow: auto; -} - -.rst__rowLabel { - padding-right: 5px; -} - -.closeIcon:hover { - cursor: pointer; -} - -/* Additional styling for ReacType X */ -.componentDefault { - width: auto; - border: 2px solid gray; - min-height: 150px; - padding: 10px; - background-color: rgb(213, 80, 164); - opacity: 0.75; -} - -.inputName { - margin-bottom: 35px; - text-align: center; -} - -.oauth-bttn:hover { - background-color: #4e76c7; -} - -.text-editor .bttn { - position: absolute; - bottom: 90px; - right: 30px; - z-index: 999; -} - -.cssSaveBtn { - position: absolute; - bottom: 30px; - right: 30px; - z-index: 999; - background-color: #0671e3; - box-shadow: none; - padding: 32px; -} - -.useState-btn { - color: rgb(241, 240, 240); - background-color: #2d313a; - border: 1px solid #2d313a; - border-radius: 10px; - box-shadow: '2px 2px 2px #1a1a1a'; - font-family: Arial, Helvetica, sans-serif; - font-size: 15px; - padding: 12px; - margin-left: 20px; -} - -.useState-position { - display: flex; - flex-direction: column; - position: fixed; - align-items: center; - top: 30%; - left: 30%; -} - -.useState-header { - font-size: 35px; - background-color: #0671e3; - color: rgb(241, 240, 240); - width: 600px; - border: 3px; - border-style: solid; - border-color: #0671e3; - font-family: 'Open Sans', sans-serif; - border-radius: 15px 15px 0px 0px; -} - -.useState-dropdown { - align-self: flex-start; -} - -.useState-window { - width: 600px; - height: 400px; - resize: none; - white-space: pre-line; - font-size: 18px; - border: 2px; - border-style: solid; - border-color: black; - border-radius: 0px 0px 15px 15px; - font-family: Arial, Helvetica, sans-serif; - background-color: rgb(241, 240, 240); - display: flex; - flex-direction: column; - align-items: center; -} - -li { - color: black; -} - -.bi { - margin-right: 0.5rem; - text-transform: none; -} - -.blue-accent-text { - color: #2997ff; -} - -a.nav_link { - display: flex; - justify-content: center; - align-items: center; - color: rgba(0, 0, 0, 0.87); - cursor: pointer; - text-decoration: none; - margin-top: 1rem; -} - -a.nav_link:hover { - color: #3ec1ac; - text-decoration: underline; -} - -.other-options { - margin-bottom: 1r; - text-transform: none; - size: 1rem; - color: white; - transition: 300ms; -} - -.other-options:hover { - color: #aaaaaa; - cursor: pointer; -} +@import url('https://fonts.googleapis.com/css?family=Open+Sans&display=swap'); +@import url('https://fonts.googleapis.com/css2?family=Raleway&display=swap'); +@import url('https://fonts.googleapis.com/css2?family=Roboto&display=swap'); +@import url('https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@48,400,0,0'); +/* @import url('https: //fonts.googleapis.com/css2?family=Fira+Code:wght@300;400;500;600;700&display=swap'); */ + +*, +*::before, +*::after { + box-sizing: inherit; +} + +html { + box-sizing: border-box; + overflow: auto; +} + +body { + margin: 0; + padding: 0; + font-family: 'Roboto', 'Raleway', sans-serif; + font-size: 0.9 em; + font-weight: normal; + overflow: hidden; + height: 100vh; + width: 100vw; + background-color: #191919; +} + +h4 { + color: #155084; + text-align: center; +} + +.creation-panel { + color: black; + display: flex; + flex-direction: row; + height: 100%; + width: 100%; +} + +.customization-section { + color: black; + display: flex; + flex-direction: row; + height: 100%; +} + +.state-panel { + margin-top: -15px; + flex-grow: 2; + display: flex; + flex-direction: row; + justify-content: space-evenly; +} + +.state-prop-grid { + height: 300px; + width: 410px; + flex-grow: 1; + padding-left: 10px; + padding-right: 10px; +} + +.HTMLItemCreate { + flex-grow: 1; + justify-content: center; +} + +:disabled { + cursor: not-allowed; +} + +header { + display: none; +} + +.app-container { + display: flex; + width: 100%; + height: calc(100vh - 64px); +} + +.mode-change-button { + display: flex; + align-content: center; + padding-top: 50px; +} + +.progress-bar { + flex-grow: 1; +} + +.column { + background-color: #1e2024; + margin-right: 1.5px; +} + +/* +///////////////////////////////////////////////// +LEFT COLUMN +///////////////////////////////////////////////// +*/ + +.app-container { + position: relative; +} + +.left-container { + display: flex; + z-index: 1; +} + +span.material-symbols-outlined { + padding: 1px; + font-size: 18px; +} + +.left { + padding: 0px; + display: flex; + height: 100%; + width: 280px; + min-width: 225px; + flex-direction: column; + align-content: center; + position: relative; + overflow-x: hidden; +} + +.column.left::-webkit-scrollbar { + width: 0.5rem; + height: 0.5rem; +} + +.column.left::-webkit-scrollbar-thumb { + transition: 0.3s ease all; + border-color: transparent; + background-color: #1e2024; + border-radius: 8px; + z-index: 40; +} + +.hide-show { + position: absolute; + height: 100%; + left: -252px; + transition: all 0.5s ease-in-out; +} + +.hide-show:hover { + left: 0px; + z-index: 289; +} + +.HTMLItems { + display: flex; + flex-direction: column; + justify-content: space-around; + flex-wrap: wrap; + width: 100%; + flex-grow: 1; +} + +.HTMLItemsDark { + display: flex; + flex-direction: column; + justify-content: space-around; + flex-wrap: wrap; + flex-grow: 1; + border: 3px solid white; +} + +#Half { + display: flex; + flex-direction: column; + justify-content: top; + height: 60vh; + text-align: center; + overflow-x: hidden; + overflow-y: auto; +} + +#HTMLItemsTopHalf::-webkit-scrollbar { + width: 0.8rem; + height: 0.5rem; +} + +#HTMLItemsTopHalf::-webkit-scrollbar-thumb { + transition: 0.3s ease all; + border-color: transparent; + background-color: #1e2024; + border-radius: 8px; + z-index: 40; +} + +#CompBottomHalf { + flex-grow: 1; + text-align: center; + overflow-x: hidden; + overflow-y: auto; +} + +.HTMLElements { + display: flex; + flex-direction: column; + align-items: flex-start; + flex-wrap: wrap; +} + +#HTMLgrid { + margin: 5px; +} + +#HTMLItemsGrid { + display: flex; +} + +#HTMLItem { + transition: 0.3s; + border: 1px solid transparent; + color: #f2fbf8; + padding: 10%; + border-radius: 10px; + box-shadow: #191919; +} + +#HTMLItem:hover { + background-color: #474545a8; + color: #8ca8ff; +} + +#MUIItem { + transition: 0.3s; + border: 1px solid transparent; + color: #f2fbf8; + padding: 10%; + border-radius: 10px; + box-shadow: #191919; +} + +#MUIItem:hover { + background-color: #474545a8; + color: #8ca8ff; +} + +#submitButton { + margin-left: 5px; +} + +#submitButton:hover { + background-color: #0671e3; + color: white; + border: none; + cursor: pointer; +} + +.lineDiv { + width: 150%; + margin-left: 0px; +} + +.customForm { + display: flex; + flex-direction: column; + align-items: start; + color: white; + font-size: 95%; +} + +.customForm h4 { + align-self: center; +} + +#newElement { + border: none; + background: none; + display: flex; + align-items: baseline; + justify-content: flex-start; + width: 0.5em; +} + +/* span container in HTMLItem.tsx */ +#customHTMLElement { + display: flex; + flex-direction: row; + justify-content: center; + width: 125px; +} + +.component-input { + bottom: 2.8%; +} + +.clear-workspace { + top: 96%; + position: sticky !important; + z-index: 1; +} + +.hidden { + display: none; +} + +/* +///////////////////////////////////////////////// +MAIN COLUMN +//////////////////////////////////////////////// +*/ + +h1 { + color: rgb(75, 75, 75); +} + +.main-container { + display: flex; + flex-direction: column; + flex: 1; + min-width: 700px; +} + +.main-header { + background-color: #212121; + box-shadow: 0 5px 7px -2px rgba(0, 0, 0, 0.1); + z-index: 10; +} + +.main-header-buttons { + display: flex; + font-size: 12px; +} + +.main { + background: #fff; + flex: 1; + width: 100%; + overflow: auto; + display: flex; + background-color: #e4e4e4; +} + +.draggable { + position: relative; +} + +.image { + top: 0; + left: 0; + width: 100%; + position: relative; +} + +.image-container { + position: relative; + display: flex; + top: 0; + left: 0; +} + +.export-preference div span { + transition: 0.5s; +} + +#export-modal { + transition: 0.5s; +} + +#export-modal:hover { + background-color: #66c4eb; + cursor: pointer; +} +/* ///CHANGED COLOR COLOR */ +#export-modal:hover span { + color: white; +} + +.info { + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + text-align: center; +} + +.ace_scrollbar { + display: none; +} + +.ace_print-margin { + display: none; +} + +/* +////////////////////////////////////////////////// +RIGHT COLUMN +///////////////////////////////////////////////// +// RIGHT NOW, I'M NOT SURE WHAT RIGHT COL IS REFERING TO +*/ +#rightContainer { + display: flex; + flex-direction: row; + height: 100%; + width: 99.9%; +} + +.right { + padding-top: 0px; + width: 100%; + height: 100%; +} + +.rightContainer { + margin-top: 65px; + color: black; +} + +.rightPanelWrapper { + display: flex; + flex-direction: row; + justify-content: center; + height: 100%; + width: 100%; +} + +#addComponentButton { + height: 49px; + width: 130px; + background-color: #0671e3; + border: 2px solid #0671e3; + border-radius: 10px; + font-size: 16.5px; +} + +#addComponentButton:hover { + background-color: #25366d; + border: 2px solid #354e9c; + color: white; + cursor: pointer; +} + +.export { + border-top: 1px solid #ccc; + display: flex; + justify-content: center; + align-items: center; + flex-direction: row; +} + +.makeStyles-panelWrapperList-4::-webkit-scrollbar-track { + background-color: rgba(25, 25, 25, 0.8); +} + +.compPanelItem { + height: 100%; + display: flex; + justify-content: center; + align-content: center; + border-color: #0671e3; +} + +.compPanelItem h3 { + font-size: 1em; + letter-spacing: 0.75px; + padding: 5px 10px; +} + +.compPanelItem:hover { + cursor: pointer; +} + +/* +///////////////////////////////////////////////////// +BOTTOM PANEL +///////////////////////////////////////////////////// +*/ + +.tab-content { + height: calc(100% - 76px); + width: 100%; + overflow-y: scroll; + overflow-x: auto; + position: relative; + z-index: 0; + background-color: #1e2024; +} + +.tab-content::-webkit-scrollbar { + width: 0.8rem; + height: 0.5rem; +} + +.tab-content::-webkit-scrollbar-thumb { + transition: 0.3s ease all; + border-color: transparent; + background-color: gray; + border-radius: 8px; + z-index: 40; +} + +.tab-content::-webkit-scrollbar { + width: 0.8rem; + height: 0.5rem; +} + +.tab-content::-webkit-scrollbar-thumb { + transition: 0.3s ease all; + border-color: transparent; + background-color: gray; + border-radius: 8px; + z-index: 40; +} + +.MuiDataGrid-menuList, +.MuiDataGrid-columnsPanel, +.MuiDataGrid-filterForm { + background-color: #333333; +} +.MuiDataGrid-panelHeader, +.MuiDataGrid-panelFooter { + background-color: #252525; +} + +#resize-drag { + cursor: row-resize; + height: 23px; + width: 75px; + background-color: #1e2024; + text-align: center; + border-radius: 7px 7px 0 0; + align-self: center; + margin-top: -25px; + color: silver; + user-select: none; + z-index: 1; +} + +.bottom-panel { + transition: width 250ms ease-in-out; + background-color: #0671e3; + width: 100%; + height: 100%; + display: flex; + flex-direction: column; +} + +.htmlattr { + height: 80%; + overflow-y: scroll; + margin-left: 50px; +} + +.props-container { + margin-left: 10px; + height: 33%; + width: 100%; +} + +.props-input { + margin-bottom: 15px; +} + +.chips { + height: 55%; + overflow: auto; +} + +.flex-container { + display: flex; + flex-direction: row; + padding-top: 5px; +} + +.flex1 { + padding: 10px 20px 10px 5px; +} + +.flex2 { + padding-right: 10px; + padding-left: 10px; + border: rgb(68, 68, 68) solid 1px; + border-radius: 5px; + background-color: #0671e3; +} + +.event-table { + height: 300px; + width: 410px; + flex-grow: 1; + padding-left: 25px; + padding-right: 25px; + margin-top: 20px; +} + +#message-container::-webkit-scrollbar { + width: 0.1rem; + height: 0.1rem; +} + +#message-container::-webkit-scrollbar-thumb { + transition: 0.3s ease all; + border-color: transparent; + background-color: #1e2024; + background-image: linear-gradient( + 180deg, + #1e2024 15%, + grey 15%, + grey 85%, + #1e2024 85% + ); + border-radius: 10px; + z-index: 40; + overflow-y: hidden; +} + +#message-input { + outline: none; +} + +.video-scrollable::-webkit-scrollbar { + width: 0.1rem; + height: 0.1rem; +} + +.video-scrollable::-webkit-scrollbar-thumb { + transition: 0.3s ease all; + border-color: transparent; + background-color: #1e2024; + background-image: linear-gradient( + 180deg, + #1e2024 15%, + #31343a 15%, + #31343a 85%, + #1e2024 85% + ); + border-radius: 10px; + z-index: 40; + overflow-y: hidden; +} + +/* +////////////////////////////////////////// +NAVBAR +////////////////////////////////////////// +*/ + +.main-navbar { + display: flex; + justify-content: space-between; + align-items: center; + z-index: 9999; +} + +.main-logo { + display: flex; + align-items: center; + gap: 10px; + margin-left: 15px; +} + +.main-navbar svg { + width: 30px; + height: 30px; + color: gray; + margin-right: 2vw; + cursor: pointer; +} + +.navDropDown { + position: absolute; + width: 230px; + border-radius: 8px; + right: 1vw; + z-index: 10; + background-color: #31343a; + top: 45px; + padding: 10px 0; + display: hidden; +} + +.hideNavDropDown { + display: none; +} + +.navDropDown button { + width: 100%; + background-color: #31343a; + color: #d9dada; + padding: 9px 25px; + display: flex; + align-items: center; + border: none; +} + +.navDropDown span { + margin-left: -25px; +} + +.navDropDown p { + margin: 3px 5%; + color: #d9dada; +} + +.navDropDown button:hover { + cursor: pointer; + background-color: #393c42; +} + +.navDropDown button svg { + width: 20px; + height: 20px; + color: gray; +} + +#navbarButton { + color: white; + margin: 0px 5px 0px 5px; +} + +#customized-menu { + width: 16%; +} + +#canvasContainer::-webkit-scrollbar { + width: 0.5rem; + height: 0.7rem; +} + +#canvasContainer::-webkit-scrollbar-thumb { + transition: 0.3s ease all; + border-color: transparent; + background-color: gray; + border-radius: 8px; + z-index: 40; +} + +/* +///////////////////////////////////////////////// +CANVAS WINDOW +///////////////////////////////////////////////// +*/ + +.delete-button-empty { + float: right; + background-color: rgba(0, 0, 0, 0); + border-radius: 50%; + border: none; + font-size: 15px; + color: white; +} + +.deleteIcon { + font-size: 18px; + color: gray; + cursor: pointer; +} + +.deleteIcon:hover { + color: white; +} + +.componentContainer { + display: flex; + flex-direction: column; + align-items: center; + font-family: 'Roboto', 'Raleway', sans-serif; + height: 100%; +} + +/* Material-UI */ +/* Sortable tree sorting */ +.sortable-tree { + height: 100%; + background-color: rgb(37, 37, 38); +} + +div.rst__rowContents { + width: 45px; + border: none; + color: #eee; + background-color: #333333; +} + +.rst__rowContentsDragDisabled { + background-color: #333333; + background-color: rgba(37, 37, 38, 0.4); +} + +.rst__moveHandle, +.rst__loadingHandle { + width: 40px; +} + +.rst_tree { + overflow: auto; +} + +.rst__rowLabel { + padding-right: 5px; +} + +.closeIcon:hover { + cursor: pointer; +} + +/* Additional styling for ReacType X */ +.componentDefault { + width: auto; + border: 2px solid gray; + min-height: 150px; + padding: 10px; + background-color: rgb(213, 80, 164); + opacity: 0.75; +} + +.inputName { + margin-bottom: 35px; + text-align: center; +} + +.oauth-bttn:hover { + background-color: #4e76c7; +} + +.text-editor .bttn { + position: absolute; + bottom: 90px; + right: 30px; + z-index: 999; +} + +.cssSaveBtn { + position: absolute; + bottom: 30px; + right: 30px; + z-index: 999; + background-color: #0671e3; + box-shadow: none; + padding: 32px; +} + +.useState-btn { + color: rgb(241, 240, 240); + background-color: #2d313a; + border: 1px solid #2d313a; + border-radius: 10px; + box-shadow: '2px 2px 2px #1a1a1a'; + font-family: Arial, Helvetica, sans-serif; + font-size: 15px; + padding: 12px; + margin-left: 20px; +} + +.useState-position { + display: flex; + flex-direction: column; + position: fixed; + align-items: center; + top: 30%; + left: 30%; +} + +.useState-header { + font-size: 35px; + background-color: #0671e3; + color: rgb(241, 240, 240); + width: 600px; + border: 3px; + border-style: solid; + border-color: #0671e3; + font-family: 'Open Sans', sans-serif; + border-radius: 15px 15px 0px 0px; +} + +.useState-dropdown { + align-self: flex-start; +} + +.useState-window { + width: 600px; + height: 400px; + resize: none; + white-space: pre-line; + font-size: 18px; + border: 2px; + border-style: solid; + border-color: black; + border-radius: 0px 0px 15px 15px; + font-family: Arial, Helvetica, sans-serif; + background-color: rgb(241, 240, 240); + display: flex; + flex-direction: column; + align-items: center; +} + +li { + color: black; +} + +.bi { + margin-right: 0.5rem; + text-transform: none; +} + +.blue-accent-text { + color: #2997ff; +} + +a.nav_link { + display: flex; + justify-content: center; + align-items: center; + color: rgba(0, 0, 0, 0.87); + cursor: pointer; + text-decoration: none; + margin-top: 1rem; +} + +a.nav_link:hover { + color: #3ec1ac; + text-decoration: underline; +} + +.other-options { + margin-bottom: 1r; + text-transform: none; + size: 1rem; + color: white; + transition: 300ms; +} + +.other-options:hover { + color: #aaaaaa; + cursor: pointer; +} diff --git a/app/src/redux/MUITypes.ts b/app/src/redux/MUITypes.ts new file mode 100644 index 00000000..6d31525f --- /dev/null +++ b/app/src/redux/MUITypes.ts @@ -0,0 +1,147 @@ +import { MUIType } from '../interfaces/Interfaces'; +import { TroubleshootSharp } from '@mui/icons-material'; + +const MUITypes: MUIType[] = [ + { + id: 21, + tag: 'mui button', + name: 'Button', + style: {}, + placeHolderShort: 'mui button', + placeHolderLong: 'Material UI Button Component', + icon: 'EditAttributes', + framework: 'reactClassic', + nestable: true, + imports: ["import Button from '@mui/material/Button'"], + props: [ + 'children', + 'classes', + 'color', + 'component', + 'disabled', + 'disableElevation', + 'disableFocusRipple', + 'disableRipple', + 'endIcon', + 'fullWidth', + 'href', + 'size', + 'startIcon', + 'sx', + 'variant' + ] + }, + // do not move this separator element out of index 1 in this array + // in componentReducer.ts, separator is referenced as 'initialState.HTMLTypes[1]' + { + id: 1000, + tag: 'separator', + name: 'separator', + style: { border: 'none' }, + placeHolderShort: '', + placeHolderLong: '', + icon: null, + framework: '', + nestable: true, + imports: [], + props: [] + }, + { + id: 31, + tag: 'textfield', + name: 'TextField', + style: {}, + placeHolderShort: 'textfield', + placeHolderLong: 'Material UI TextField Component', + icon: 'Input', + framework: 'reactClassic', + nestable: false, + imports: [ + "import Box from '@mui/material/Box'", + "import TextField from '@mui/material/TextField'" + ], + props: [ + 'autoComplete', + 'autoFocus', + 'classes', + 'color', + 'defaultValue', + 'disabled', + 'error', + 'FormHelperTextProps', + 'fullWidth', + 'helperText', + 'id', + 'InputLabelProps', + 'inputProps', + 'InputProps', + 'inputRef', + 'label', + 'margin', + 'maxRows', + 'minRows', + 'multiline', + 'name', + 'onChange', + 'placeholder', + 'required', + 'rows', + 'select', + 'SelectProps', + 'size', + 'sx', + 'type', + 'value', + 'variant' + ] + }, + { + id: 41, + tag: 'card', + name: 'Card', + style: {}, + placeHolderShort: 'card', + placeHolderLong: 'Material UI Card Component', + icon: 'CardTravel', + framework: 'reactClassic', + nestable: true, + imports: [ + "import Box from '@mui/material/Box'", + "import Card from '@mui/material/Card'", + "import CardActions from '@mui/material/CardActions'", + "import CardContent from '@mui/material/CardContent'", + "import Button from '@mui/material/Button'", + "import Typography from '@mui/material/Typography'" + ], + props: ['children', 'classes', 'raised', 'sx'] + }, + { + id: 51, + tag: 'typography', + name: 'Typography', + style: {}, + placeHolderShort: 'typography', + placeHolderLong: 'Material UI Typography Component', + icon: 'TextFields', + framework: 'reactClassic', + nestable: true, + imports: [ + "import Box from '@mui/material/Box'", + "import Typography from '@mui/material/Typography'" + ], + props: [ + 'align', + 'children', + 'classes', + 'component', + 'gutterBottom', + 'noWrap', + 'paragraph', + 'sx', + 'variant', + 'variantMapping' + ] + } +]; + +export default MUITypes; diff --git a/app/src/redux/reducers/slice/appStateSlice.ts b/app/src/redux/reducers/slice/appStateSlice.ts index 7333d280..18bf5f3c 100644 --- a/app/src/redux/reducers/slice/appStateSlice.ts +++ b/app/src/redux/reducers/slice/appStateSlice.ts @@ -1,1368 +1,1396 @@ -// Main slice for all the component state./// -import { createSlice } from '@reduxjs/toolkit'; -// Import Interfaces for State, and HTML Types -import { - State, - Component, - ChildElement, - HTMLType -} from '../../../interfaces/Interfaces'; -import HTMLTypes from '../../HTMLTypes'; -import generateCode from '../../../helperFunctions/generateCode'; -import manageSeparators from '../../../helperFunctions/manageSeparators'; - -export const initialState: State = { - name: '', - _id: '', - forked: false, - published: false, - isLoggedIn: false, - components: [ - { - id: 1, - name: 'App', - style: {}, - code: '
Drag in a component or HTML element into the canvas!
', - children: [], - isPage: true, - past: [], - future: [], - stateProps: [], - useStateCodes: [], // array of strings for each useState codes - events: {}, // Add the missing 'events' property - passedInProps: [] // Add the missing 'passedInProps' property - } - ], - projectType: 'Classic React', - rootComponents: [1], - canvasFocus: { componentId: 1, childId: null }, - nextComponentId: 2, - nextChildId: 1, - nextTopSeparatorId: 1000, - HTMLTypes: HTMLTypes, // left as is for now - tailwind: false, - stylesheet: '', - codePreview: false, - screenshotTrigger: false, - customElementId: 10001 -}; - -let separator = initialState.HTMLTypes[1]; - -const findComponent = (components: Component[], componentId: number) => { - return components.find((elem) => elem.id === componentId); -}; - -// Finds a parent -// returns object with parent object and index value of child -const findParent = (component: Component, childId: number) => { - // using a breadth first search to search through instance tree - // We're going to keep track of the nodes we need to search through with an Array - // Initialize this array with the top level node - const nodeArr: (Component | ChildElement)[] = [component]; - // iterate through each node in the array as long as there are elements in the array - while (nodeArr.length > 0) { - // shift off the first value and assign to an element - const currentNode = nodeArr.shift(); - // try to find id of childNode in children - if (currentNode?.children) { - if (currentNode.name !== 'input' && currentNode.name !== 'img') { - for (let i = 0; i <= currentNode.children.length - 1; i++) { - // if match is found return object with both the parent and the index value of the child - if (currentNode.children[i].childId === childId) { - return { directParent: currentNode, childIndexValue: i }; - } - } - // if child node isn't found add each of the current node's children to the search array - currentNode.children.forEach((node: ChildElement) => - nodeArr.push(node) - ); - } - } - } - // if no search is found return -1 - return { directParent: null, childIndexValue: null }; -}; - -// determine if there's a child of a given type in a component -const childTypeExists = ( - type: string, - typeId: number, - component: Component -) => { - const nodeArr = [...component.children]; - // breadth-first search through component tree to see if a child exists - while (nodeArr.length > 0) { - // shift off the first value and assign to an element - const currentNode = nodeArr.shift(); - if (currentNode?.children) { - if (currentNode.type === type && currentNode.typeId === typeId) - return true; - // if child node isn't found add each of the current node's children to the search array - currentNode.children.forEach((node) => nodeArr.push(node)); - } - } - // if no match is found return false - return false; -}; -// find child in component and return child object -const findChild = (component: Component, childId: number) => { - if (childId === null) return component; - const nodeArr = [...component.children]; - // breadth first search through component tree to see if a child exists - while (nodeArr.length > 0) { - // shift off the first value and assign to an element - const currentNode = nodeArr.shift(); - if (currentNode?.children) { - if (currentNode.childId === childId) return currentNode; - // if child node isn't found add each of the current node's children to the search array - if (currentNode.name !== 'input' && currentNode.name !== 'img') - currentNode.children.forEach((node) => nodeArr.push(node)); - } - } - // if no match is found return false - return; -}; -// update all ids and typeIds to match one another -const updateAllIds = (comp: Component[] | ChildElement[]) => { - // put components' names and ids into an obj - const obj = { spr: 1000, others: 1 }; - // for each of the components, if it has children, iterate through that children array - comp.forEach((el: Component | ChildElement) => { - if (el.children.length > 0) { - for (let i = 0; i < el.children.length; i++) { - // update each child's childId - if (el.children[i].name === 'separator') { - el.children[i].childId = obj['spr']++; - } else { - el.children[i].childId = obj['others']++; - } - // // if the child's name and id exists in the object - // recursively call the updateAllIds function on the child's children array if - // the child's children array is greater than 0 - if (el.children[i].children.length > 0) { - updateAllIds(el.children[i].children); - } - } - } - }); -}; -const updateIds = (components: Component[]) => { - // component IDs should be array index + 1 - components.forEach((comp, i) => (comp.id = i + 1)); - updateAllIds(components); - // create key-value pairs of component names and corresponding IDs - const componentIds = {}; - components.forEach((component) => { - if (!component.isPage) componentIds[component.name] = component.id; - }); - // assign correct ID to components that are children inside of remaining pages - components.forEach((page) => { - if (page.isPage) { - page.children.forEach((child) => { - if (child.type === 'Component') child.typeId = componentIds[child.name]; - }); - } - }); - return components; -}; -// updated compoment updateRoots with TS number type implemented -const updateRoots = (components: Component[]): number[] => { - const roots: number[] = []; - components.forEach((comp) => { - if (comp.isPage) roots.push(comp.id); - }); - return roots; -}; -// updated state property to state from object -const deleteById = (id: number, name: string, state: State): Component[] => { - // name of the component we want to delete - const checkChildren = (child: Component[] | ChildElement[]) => { - // for each of the components in the passed in components array, if the child - // component has a children array, iterate through the array of children - child.forEach((el: Component | ChildElement) => { - if (el.children.length) { - const arr: ChildElement[] = []; - for (let i = 0; i < el.children.length; i++) { - // check to see if the name variable doesn't match the name of the child - if (el.children[i].name !== name) { - // if so, push into the new array the child component - arr.push(el.children[i]); - } - } - // set the children array to be the new array - el.children = arr; - // recursively call the checkChildren function with the updated children array - checkChildren(el.children); - } - }); - }; - // creating a copy of the components array - const copyComp = [...state.components]; - if (copyComp.length) { - checkChildren(copyComp); - } - const filteredArr = [...copyComp].filter((comp) => comp.id != id); - return updateIds(filteredArr); -}; - -const updateUseStateCodes = (currentComponent: Component | ChildElement) => { - // array of snippets of state prop codes - const localStateCode: string[] = []; // avoid never by assigning it to string - currentComponent.stateProps - .filter((n, i) => i % 2 === 0) - .forEach((stateProp) => { - const useStateCode = `const [${stateProp.key}, set${ - stateProp.key.charAt(0).toUpperCase() + stateProp.key.slice(1) - }] = useState<${stateProp.type} | undefined>(${JSON.stringify( - stateProp.value - )})`; - localStateCode.push(useStateCode); - }); - if (currentComponent.name !== 'App' && currentComponent.name !== 'Index') { - currentComponent.passedInProps.forEach((passedInProp) => { - const prop = `const ${passedInProp.key} = props.${passedInProp.key}`; - localStateCode.push(prop); - }); - } - // store localStateCodes in global state context - return localStateCode; -}; - -// Creates new slice for components with applicable reducers -const appStateSlice = createSlice({ - name: 'appState', - initialState, - reducers: { - addComponent: (state, action) => { - if ( - typeof action.payload.componentName !== 'string' || - action.payload.componentName === '' - ) { - return; - } - - const newComponent = { - id: state.components.length + 1, - name: action.payload.componentName, - nextChildId: 1, - style: {}, - attributes: {}, - events: {}, - code: '', - children: [], - isPage: action.payload.root, - past: [], - future: [], - stateProps: [], - useStateCodes: [], - passedInProps: [] - }; - state.components.push(newComponent); - // functionality if the new component will become the root component - if (action.payload.root) state.rootComponents.push(newComponent.id); - // updates the focus to the new component, which redirects to the new blank canvas of said new component - - // change canvas focus to just created component - - const nextComponentId = state.nextComponentId + 1; - newComponent.code = generateCode( - state.components, - newComponent.id, - [...state.rootComponents], - state.projectType, - state.HTMLTypes, - state.tailwind, - action.payload.contextParam - ); - - state.nextComponentId = nextComponentId; - }, - - addChild: (state, action) => { - let parentComponentId: number; - const { - type, - typeId, - childId - }: { type: string; typeId: number; childId: any } = action.payload; - - // determine the parent component id - if (action.payload.copyId) { - parentComponentId = action.payload.copyId; - } else { - parentComponentId = state.canvasFocus.componentId; - } - - // make a copy of the components from state - const components = [...state.components]; - - // find the parent component - const parentComponent = findComponent(components, parentComponentId); - - // if type is 'Component', loop through components to find componentName and componentChildren - let componentName: string = ''; - let componentChildren: ChildElement[] = []; - if (type === 'Component') { - components.forEach((comp) => { - if (comp.id === typeId) { - componentName = comp.name; - componentChildren = comp.children; - } - }); - } - if (type === 'Component') { - const originalComponent = findComponent(state.components, typeId); - if (originalComponent) { - if ( - childTypeExists('Component', parentComponentId, originalComponent) - ) - return state; - } - } - - // create a new name based on the type of element - let newName = state.HTMLTypes.reduce((name, el) => { - if (typeId === el.id) { - name = type === 'Component' ? componentName : el.tag; - } - return name; - }, ''); - - if (type === 'Route Link') { - components.find((comp) => { - if (comp.id === typeId) { - newName = comp.name; - return; - } - }); - } - - const newChild: ChildElement = { - type, - typeId, - name: newName, - childId: state.nextChildId, - style: {}, - attributes: {}, - events: {}, - children: componentChildren, // work in progress possible solution // children: componentChildren as ChildElement[], - stateProps: [], //legacy pd: added stateprops and passedinprops - passedInProps: [] - }; - - // added missing properties - const topSeparator: ChildElement = { - type: 'HTML Element', - typeId: separator.id, - name: 'separator', - childId: state.nextTopSeparatorId, - style: separator.style, - attributes: {}, - events: {}, // Added - children: [], - stateProps: [], // Added - passedInProps: [] // Added - }; - // if the childId is null, this signifies that we are adding a child to the top-level component rather than another child element - // we also add a separator before any new child - // if the newChild Element is an input or img type, delete the children key/value pair - // if (newChild.name === 'input' && newChild.name === 'img') - // delete newChild.children; - let directParent: HTMLElement | any; - if (childId === null) { - if (parentComponent) { - parentComponent.children.push(topSeparator); - parentComponent.children.push(newChild); - } - } - // if there is a childId (childId here references the direct parent of the new child) find that child and add new child to its children array - else { - if (parentComponent) { - directParent = findChild(parentComponent, childId); - //disable nesting a component inside a HTML element - if (directParent.type === 'HTML Element' && type === 'HTML Element') { - directParent.children.push(topSeparator); - directParent.children.push(newChild); - } else { - return { ...state }; - } - } - } - // update canvasFocus to the new child - const canvasFocus = { - ...state.canvasFocus, - componentId: state.canvasFocus.componentId, - childId: newChild.childId - }; - - const nextChildId = state.nextChildId + 1; - let nextTopSeparatorId = state.nextTopSeparatorId + 1; - let addChildArray = components[canvasFocus.componentId - 1].children; - // merge separator needs interface and type - addChildArray = manageSeparators.mergeSeparator(addChildArray, 1); - if (directParent && directParent.name === 'separator') - nextTopSeparatorId = manageSeparators.handleSeparators( - // handle separator needs interface and type - addChildArray, - 'add' - ); - - components[canvasFocus.componentId - 1].children = addChildArray; - - // generate code - if (parentComponent) { - parentComponent.code = generateCode( - components, - parentComponentId, - [...state.rootComponents], - state.projectType, - state.HTMLTypes, - state.tailwind, - action.payload.contextParam - ); - } - - // update the state with the new values - state.components = components; - state.nextChildId = nextChildId; - state.canvasFocus = canvasFocus; - state.nextTopSeparatorId = nextTopSeparatorId; - }, - - changeTailwind: (state, action) => { - // return { ...state, tailwind: action.payload } - state.tailwind = action.payload; - }, - changeFocus: (state, action) => { - const { componentId, childId } = action.payload; - // makes separators not selectable - if (childId < 1000) { - //update componentId and childId in the state - state.canvasFocus = { ...state.canvasFocus, componentId, childId }; - //makes it so the code preview will update when clicking on a new component - state.components = state.components.map((element) => { - return Object.assign({}, element); - }); - } - }, - - updateStateUsed: (state, action) => { - const { stateUsedObj } = action.payload; - const components = [...state.components]; - const component = findComponent( - components, - state.canvasFocus.componentId - ); - if (component) { - if (state.canvasFocus.childId !== null) { - const targetChild = findChild(component, state.canvasFocus.childId); - if (targetChild) { - targetChild.stateUsed = stateUsedObj; - component.code = generateCode( - components, - state.canvasFocus.componentId, - [...state.rootComponents], - state.projectType, - state.HTMLTypes, - state.tailwind, - action.payload.contextParam - ); - state.components = components; - } - } - } - }, - - updateUseContext: (state, action) => { - const { useContextObj } = action.payload; - const components = [...state.components]; - const component = findComponent( - components, - state.canvasFocus.componentId - ); - if (component) { - component.useContext = useContextObj; - component.code = generateCode( - components, - state.canvasFocus.componentId, - [...state.rootComponents], - state.projectType, - state.HTMLTypes, - state.tailwind, - action.payload.contextParam - ); - state.components = components; - } - }, - - resetAllState: (state) => { - if (state.isLoggedIn) { - Object.assign(state, initialState); - state.isLoggedIn = true; - return; - } - Object.assign(state, initialState); - }, - changePosition: (state, action) => { - const { currentChildId, newParentChildId } = action.payload; - // if the currentChild Id is the same as the newParentId (i.e. a component is trying to drop itself into itself), don't update state - if (currentChildId === newParentChildId) return state; - // find the current component in focus - let components = [...state.components]; - const component = findComponent( - components, - state.canvasFocus.componentId - ); - // find the moved element's former parent - // delete the element from it's former parent's children array - if (component) { - const { directParent, childIndexValue } = findParent( - component, - currentChildId - ); - // BREAKING HERE during manipulation of positions. Sometimes get a null value when manipulating positions - // Only run if the directParent exists - if (directParent) { - if (directParent.children) { - const child = { ...directParent.children[childIndexValue] }; - directParent.children.splice(childIndexValue, 1); - // if the childId is null, this signifies that we are adding a child to the top level component rather than another child element - if (newParentChildId === null) { - component.children.push(child); - } - // if there is a childId (childId here references the direct parent of the new child) find that child and a new child to its children array - else { - const directParent = findChild(component, newParentChildId); - if (directParent?.children) { - directParent.children.push(child); - } - } - } - } - let nextTopSeparatorId = state.nextTopSeparatorId; - components[state.canvasFocus.componentId - 1].children = - manageSeparators.mergeSeparator( - components[state.canvasFocus.componentId - 1].children, - 0 - ); - nextTopSeparatorId = manageSeparators.handleSeparators( - components[state.canvasFocus.componentId - 1].children, - 'change position' - ); - component.code = generateCode( - components, - state.canvasFocus.componentId, - [...state.rootComponents], - state.projectType, - state.HTMLTypes, - state.tailwind, - action.payload.contextParam - ); - state.components = components; - state.nextTopSeparatorId = nextTopSeparatorId; - } - }, - - updateCss: (state, action) => { - const { style } = action.payload; - const components = [...state.components]; - const component = findComponent( - components, - state.canvasFocus.componentId - ); - // closed if statement at the end of the block - if (component && state.canvasFocus.childId) { - const targetChild = findChild(component, state.canvasFocus.childId); - if (targetChild) { - targetChild.style = style; - component.code = generateCode( - components, - state.canvasFocus.componentId, - [...state.rootComponents], - state.projectType, - state.HTMLTypes, - state.tailwind, - action.payload.contextParam - ); - state.components = components; - } - } - }, - - updateAttributes: (state, action) => { - const { attributes } = action.payload; - - const components = [...state.components]; - const component = findComponent( - components, - state.canvasFocus.componentId - ); - // closed if statement at the end of the block - if (component && state.canvasFocus.childId) { - const targetChild = findChild(component, state.canvasFocus.childId); - if (targetChild) { - targetChild.attributes = attributes; - - component.code = generateCode( - components, - state.canvasFocus.componentId, - [...state.rootComponents], - state.projectType, - state.HTMLTypes, - state.tailwind, - action.payload.contextParam - ); - state.components = components; - } - } - }, - - updateEvents: (state, action) => { - const { events } = action.payload; - if (JSON.stringify(events) === '{}') return state; - const components = [...state.components]; - const component = findComponent( - components, - state.canvasFocus.componentId - ); - if (component && state.canvasFocus.childId) { - const targetChild = findChild(component, state.canvasFocus.childId); - const event = Object.keys(events)[0]; - const funcName = events[event]; - if (targetChild) { - targetChild.events[event] = funcName; - - component.code = generateCode( - components, - state.canvasFocus.componentId, - [...state.rootComponents], - state.projectType, - state.HTMLTypes, - state.tailwind, - action.payload.contextParam - ); - state.components = components; - } - } - }, - - deleteEventAction: (state, action) => { - const { event } = action.payload; - const components = [...state.components]; - const component = findComponent( - components, - state.canvasFocus.componentId - ); - if (component && state.canvasFocus.childId) { - const targetChild = findChild(component, state.canvasFocus.childId); - if (targetChild) { - delete targetChild.events[event]; - - component.code = generateCode( - components, - state.canvasFocus.componentId, - [...state.rootComponents], - state.projectType, - state.HTMLTypes, - state.tailwind, - action.payload.contextParam - ); - state.components = components; - } - } - }, - - deletePage: (state, action) => { - const id: number = state.canvasFocus.componentId; - const name: string = state.components[id - 1].name; - const components: Component[] = deleteById(id, name, state); - // rebuild rootComponents with correct page IDs - const rootComponents = updateRoots(components); - const canvasFocus = { componentId: 1, childId: null }; - state.rootComponents = rootComponents; - state.components = components; - state.canvasFocus = canvasFocus; - }, - - deleteReusableComponent: (state, action) => { - const id: number = state.canvasFocus.componentId; - const name: string = state.components[id - 1].name; - // updated list of components after deleting a component - const components: Component[] = deleteById(id, name, state); - const rootComponents: number[] = updateRoots(components); - // iterate over the length of the components array - for (let i = 0; i < components.length; i++) { - //if the component uses context from component being deleted - if (components[i].useContext && components[i].useContext[id]) { - // iterate over children to see where it is being used, then reset that compText/compLink/useState - for (let child of components[i].children) { - if (child.stateUsed) { - if (child.stateUsed.compTextProviderId === id) { - child.attributes.compText = ''; - delete child.stateUsed.compText; - delete child.stateUsed.compTextProviderId; - delete child.stateUsed.compTextPropsId; - } - if (child.stateUsed.compLinkProviderId === id) { - child.attributes.compLink = ''; - delete child.stateUsed.compLink; - delete child.stateUsed.compLinkProviderId; - delete child.stateUsed.compLinkPropsId; - } - } - } - delete components[i].useContext[id]; - } - - // for each component's code, run the generateCode function to - // update the code preview on the app - components[i].code = generateCode( - components, - components[i].id, - rootComponents, - state.projectType, - state.HTMLTypes, - state.tailwind, - action.payload.contextParam - ); - } - const canvasFocus = { componentId: 1, childId: null }; - - state.rootComponents = rootComponents; - state.components = components; - state.canvasFocus = canvasFocus; - state.nextComponentId = id; - }, - setProjectName: (state, action) => { - state.name = action.payload; - }, - changeProjectType: (state, action) => { - // when a project type is changed, both change the project type in state and also regenerate the code for each component - const { projectType } = action.payload; - - const components = [...state.components]; - // also update the name of the root component of the application to fit classic React and next.js/gatsby conventions - if (projectType === 'Next.js' || projectType === 'Gatsby.js') - components[0]['name'] = 'index'; - else components[0]['name'] = 'App'; - components.forEach((component) => { - component.code = generateCode( - components, - component.id, - [...state.rootComponents], - projectType, - state.HTMLTypes, - state.tailwind, - action.payload.contextParam - ); - }); - state.components = components; - state.projectType = projectType; - }, - - resetState: (state, action) => { - const nextChildId = 1; - const nextTopSeparatorId = 1000; - const rootComponents = [1]; - const nextComponentId = 2; - const canvasFocus = { - ...state.canvasFocus, - componentId: 1, - childId: null - }; - const rootComponent = { - ...state.components[0], - code: '
Drag in a component or HTML element into the canvas!
', - children: [], - style: {} - }; - const components = [rootComponent]; - - const stylesheet = ''; - const resetHTMLTypes = HTMLTypes; - - return { - ...state, - nextChildId, - nextTopSeparatorId, - rootComponents, - nextComponentId, - components, - canvasFocus, - stylesheet, - HTMLTypes: resetHTMLTypes - }; - }, - updateProjectName: (state, action) => { - const projectName = action.payload; - state.name = projectName; - }, - updateProjectId: (state, action) => { - const projectId = action.payload; //updates the slice with new _id - state._id = projectId; - }, - updateProjectPublished: (state, action) => { - const projectPublished = action.payload; - state.published = projectPublished; - }, - deleteElement: (state, action) => { - let name: string = ''; - const HTMLTypes: HTMLType[] = [...state.HTMLTypes].filter((el) => { - if (el.id === action.payload.id) { - name = el.tag; - } - return el.id !== action.payload.id; - }); - const components: Component[] = deleteById( - action.payload.id, - name, - state - ); - const rootComponents: number[] = updateRoots(components); - const canvasFocus = { ...state.canvasFocus, childId: null }; - components.forEach((el, i) => { - el.code = generateCode( - components, - components[i].id, - rootComponents, - state.projectType, - state.HTMLTypes, - state.tailwind, - action.payload.contextParam - ); - }); - - state.canvasFocus = canvasFocus; - state.HTMLTypes = HTMLTypes; - }, - - deleteChild: (state, action) => { - // if in-focus instance is a top-level component and not a child, don't delete anything - if (!state.canvasFocus.childId) return state; - // find the current component in focus - const components = [...state.components]; - const component = findComponent( - components, - state.canvasFocus.componentId - ); - // find the moved element's former parent - const { directParent, childIndexValue } = findParent( - component, - state.canvasFocus.childId - ); - - // ------------------------------------------- ALSO added code below ------------------------------------------- - - let canvasFocus = { ...state.canvasFocus, childId: null }; // ADDED to avoid null error - let nextTopSeparatorId = 1000; // ADDED to avoid null error - const childIdDeleteClicked = action.payload.id; // ADDED to ensure no cross-element deletion possible - - // delete the element from its former parent's children array, subject to below conditional to avoid null error - if ( - directParent && - (state.canvasFocus.childId === childIdDeleteClicked || - JSON.stringify(action.payload.id) === '{}') // Ensuring deletion works for mouseclick OR using delete key, from 2 different dispatch sources - ) { - if (directParent.children) { - directParent.children.splice(childIndexValue, 1); - let nextTopSeparatorId = manageSeparators.handleSeparators( - components[canvasFocus.componentId - 1].children, - 'delete' - ); - } - } - - // ------------------------------------------- ALSO added code above ------------------------------------------- - component.code = generateCode( - components, - state.canvasFocus.componentId, - [...state.rootComponents], - state.projectType, - state.HTMLTypes, - state.tailwind, - action.payload.contextParam - ); - state.components = components; - state.canvasFocus = canvasFocus; - state.nextTopSeparatorId = nextTopSeparatorId; - }, - setInitialState: (state, action) => { - // set the canvas focus to be the first component - const canvasFocus = { - ...action.payload.canvasFocus, - componentId: 1, - childId: null - }; - state.canvasFocus = canvasFocus; - }, - //deleted 'convertToJSX' function, which threw errors upon opening - openProject: (state, action) => { - // returning the action.payload is a Redux shortcut that updates the entire app state at the same time - return action.payload; - }, - addElement: (state, action) => { - const HTMLTypes = [...state.HTMLTypes]; - HTMLTypes.push(action.payload); - state.HTMLTypes = HTMLTypes; - state.customElementId += 1; - }, - //Undo & Redo functions are not working properly. Redo & Undo target the last component rather than last added HTML Element. - undo: (state, action) => { - const focusIndex = state.canvasFocus.componentId - 1; - // if the past array is empty, return state - if (state.components[focusIndex].past.length <= 1) { - return { ...state }; - } - // the children array of the focused component will equal the last element of the past array - state.components[focusIndex].children = - state.components[focusIndex].past[ - state.components[focusIndex].past.length - 1 - ]; - // the last element of the past array gets popped off - const poppedEl: Component = state.components[focusIndex].past.pop(); - // the last element of the past array gets popped off and pushed into the future array - state.components[focusIndex].future.push(poppedEl); - //generate code for the Code Preview - state.components.forEach((el, i) => { - el.code = generateCode( - state.components, - state.components[i].id, - state.rootComponents, - state.projectType, - state.HTMLTypes, - state.tailwind, - action.payload.contextParam - ); - }); - - state = state; - }, - redo: (state, action) => { - const focusIndex = state.canvasFocus.componentId - 1; - //if future array is empty, return state - if (state.components[focusIndex].future.length === 0) return { ...state }; - //the children array of the focused component will equal the last element of the future array - state.components[focusIndex].children = - state.components[focusIndex].future[ - state.components[focusIndex].future.length - 1 - ]; - //the last element of the future array gets pushed into the past - const poppedEl: Component = state.components[focusIndex].future.pop(); - //the last element of the future array gets popped out - state.components[focusIndex].past.push(poppedEl); - // generate code for the Code Preview - state.components.forEach((el, i) => { - el.code = generateCode( - state.components, - state.components[i].id, - state.rootComponents, - state.projectType, - state.HTMLTypes, - state.tailwind, - action.payload.contextParam - ); - }); - state = state; - }, - addState: (state, action) => { - // find the current component in focus - const components = [...state.components]; - const currComponent = findComponent( - components, - state.canvasFocus.componentId - ); - //will add update StateProps to current components' array - currComponent.stateProps.push(action.payload.newState); - currComponent.useStateCodes = updateUseStateCodes(currComponent); - currComponent.stateProps.push(action.payload.setNewState); - currComponent.code = generateCode( - components, - state.canvasFocus.componentId, - [...state.rootComponents], - state.projectType, - state.HTMLTypes, - state.tailwind, - action.payload.contextParam - ); - state.components = components; - }, - addPassedInProps: (state, action) => { - //When props are passed from a parent to a child in the State Manager tab, it will update the components available passedinprops - // find the current component in focus - const components = [...state.components]; - const currComponent = findComponent( - components, - state.canvasFocus.componentId - ); - //prevents passing in props more than one time to the current component - for (let i = 0; i < currComponent.passedInProps.length; i++) { - let curr = currComponent.passedInProps[i]; - if (curr.id === action.payload.passedInProps.id) { - return { ...state, components }; - } - } - - //find the parent for deleting instances of where the parent is passing props to children - let parent: Component; - for (let i = 0; i < components.length; i++) { - let currComponent = components[i]; - for (let j = 0; j < currComponent.children.length; j++) { - let currChild = currComponent.children[j]; - if (currChild.typeId === state.canvasFocus.componentId) { - parent = currComponent; - } - } - } - - //search for whether the child exists in the parent's children array - //if so update the passed in props child element with the updates passed in props - parent.children.forEach((child) => { - if (child.name === currComponent.name) { - child.passedInProps.push(action.payload.passedInProps); - } - }); - - // check each components passedInProps property and updating there as well. - currComponent.passedInProps.push(action.payload.passedInProps); - - //update the import codes for the current component - currComponent.useStateCodes = updateUseStateCodes(currComponent); - //update code preview for current component - currComponent.code = generateCode( - components, - state.canvasFocus.componentId, - [...state.rootComponents], - state.projectType, - state.HTMLTypes, - state.tailwind, - action.payload.contextParam - ); - //update code preview for parent component (since we have added it to the children array) - parent.code = generateCode( - components, - parent.id, - [...state.rootComponents], - state.projectType, - state.HTMLTypes, - state.tailwind, - action.payload.contextParam - ); - - // return { ...state, components }; - state.components = components; - }, - deletePassedInProps: (state, action) => { - const components = [...state.components]; - let currComponent = findComponent( - components, - state.canvasFocus.componentId - ); - - //find the parent of the component that we are deleting from - let parent: Component; - for (let i = 0; i < components.length; i++) { - let currComponent = components[i]; - for (let j = 0; j < currComponent.children.length; j++) { - let currChild = currComponent.children[j]; - if (currChild.typeId === state.canvasFocus.componentId) { - parent = currComponent; - } - } - } - - //deletes all instances of passedInProps from the children arrays of the current Component - const deletePassedInPropsChildren = (currComponent: Component) => { - const innerFunc = (currChild: Component | ChildElement) => { - // when there are no children, return up a level - if ( - currChild.children.filter((el) => el.type === 'Component') - .length === 0 - ) - return; - if (currChild.children.length) { - currChild.children - .filter((el) => el.type === 'Component') - .forEach((child, j) => { - child.passedInProps.forEach((prop, k) => { - if (prop.id === action.payload.rowId) { - child.passedInProps.splice(k, 1); - innerFunc(child); - } - }); - }); - } - }; - //for every component we update, generate new code - innerFunc(currComponent); - currComponent.code = generateCode( - components, - currComponent.id, - [...state.rootComponents], - state.projectType, - state.HTMLTypes, - state.tailwind, - action.payload.contextParam - ); - }; - //delete from the components passedInProps array - const deletePassedInProps = (myComponent: Component) => { - if ( - myComponent.children.filter((el) => el.type === 'Component') - .length === 0 - ) { - if (myComponent.passedInProps.length > 0) { - myComponent.passedInProps.forEach((prop, index) => { - if (prop.id === action.payload.rowId) { - myComponent.passedInProps.splice(index, 1); - } - }); - } - myComponent.useStateCodes = updateUseStateCodes(myComponent); - myComponent.code = generateCode( - components, - myComponent.id, - [...state.rootComponents], - state.projectType, - state.HTMLTypes, - state.tailwind, - action.payload.contextParam - ); - return; - } - myComponent.passedInProps.forEach((prop, i) => { - if (prop.id === action.payload.rowId) { - myComponent.passedInProps.splice(i, 1); - myComponent.children - .filter((el) => el.type === 'Component') - .forEach((child, i) => { - let next = components.find((comp) => comp.id === child.typeId); - deletePassedInProps(next); - }); - } - }); - myComponent.useStateCodes = updateUseStateCodes(myComponent); - myComponent.code = generateCode( - components, - myComponent.id, - [...state.rootComponents], - state.projectType, - state.HTMLTypes, - state.tailwind, - action.payload.contextParam - ); - }; - - deletePassedInPropsChildren(parent); - deletePassedInProps(currComponent); - - parent.code = generateCode( - components, - parent.id, - [...state.rootComponents], - state.projectType, - state.HTMLTypes, - state.tailwind, - action.payload.contextParam - ); - state.components = components; - }, - deleteState: (state, action) => { - const components = [...state.components]; - let currComponent = findComponent( - components, - state.canvasFocus.componentId - ); - //updates the stateProps array to reflect total state initialized in component minus the selected state to be deleted - currComponent.stateProps = action.payload.stateProps; - - //CHILDREN ARRAY LOOP (needed for code preview) - //iterate through all components, starting from top, and delete ALL instances of deleted state (provided to us - // in the passedInProps array within the children array of the component - // using the action.payload.rowId (variable name) and action.payload.otherId (setVariable name)) - components.forEach((component) => { - //find all instances of state within child elements and delete state - - component.children.forEach((child) => { - if (child.type === 'Component') { - for (let i = 0; i < child.passedInProps?.length; i++) { - if ( - child.passedInProps[i]['id'] === action.payload.rowId || - child.passedInProps[i]['id'] === action.payload.otherId - ) { - child.passedInProps.splice(i, 1); - i--; - } - } - } - }); - - // COMPONENT LOOP (needed for tables in State Management Tab) - //iterate through all components, starting from top, and delete ALL instances of deleted state (provided to us - // in the passedInProps array within each component - // using the action.payload.rowId (variable name) and action.payload.otherId (setVariable name)) - for (let i = 0; i < component.passedInProps?.length; i++) { - if ( - component.passedInProps[i]['id'] === action.payload.rowId || - component.passedInProps[i]['id'] === action.payload.otherId - ) { - component.passedInProps.splice(i, 1); - i--; - } - } - // curr component = where you are deleting from state from, also is the canvas focus - // curr component id = providerId - // we then iterate through the rest of the components - // check if a useContext if created and if the useContext contains the providerId - // we then delete from the set, statesFromProvider, the row id, and regenerate the code - // Ex: useContext {1: {statesFromProvider: Set, compLink, compText}, 2 : ..., 3 : ...} - if ( - component.useContext && - component.useContext[state.canvasFocus.componentId] - ) { - component.useContext[ - state.canvasFocus.componentId - ].statesFromProvider.delete(action.payload.rowId); - // iterate over children to see where it is being used, then reset that compText/compLink/useState - for (let child of component.children) { - if (child.stateUsed) { - if ( - child.stateUsed.compTextProviderId === currComponent.id && - child.stateUsed.compTextPropsId === action.payload.rowId - ) { - child.attributes.compText = ''; - delete child.stateUsed.compText; - delete child.stateUsed.compTextProviderId; - delete child.stateUsed.compTextPropsId; - } - if ( - child.stateUsed.compLinkProviderId === currComponent.id && - child.stateUsed.compLinkPropsId === action.payload.rowId - ) { - child.attributes.compLink = ''; - delete child.stateUsed.compLink; - delete child.stateUsed.compLinkProviderId; - delete child.stateUsed.compLinkPropsId; - } - } - } - } - - // find parent - let parent; - for (let i = 0; i < components.length; i++) { - let currComponent = components[i]; - for (let j = 0; j < currComponent.children.length; j++) { - let currChild = currComponent.children[j]; - if (currChild.typeId === component.id) { - parent = currComponent; - } - } - } - if (parent) { - parent.code = generateCode( - components, - parent.id, - [...state.rootComponents], - state.projectType, - state.HTMLTypes, - state.tailwind, - action.payload.contextParam - ); - } - - component.useStateCodes = updateUseStateCodes(component); - component.code = generateCode( - components, - component.id, - [...state.rootComponents], - state.projectType, - state.HTMLTypes, - state.tailwind, - action.payload.contextParam - ); - }); - state.components = components; - }, - - toggleLoggedIn: (state, action) => { - state.isLoggedIn = action.payload; - }, - - snapShotAction: (state, action) => { - state.components[action.payload.focusIndex].past.push( - action.payload.deepCopiedState.components[action.payload.focusIndex] - .children - ); - }, - allCooperativeState: (state, action) => { - // return Object.assign({}, state, action.payload); - // it is safe to directly assign the properties from 'action.payload' to the state in Redux Toolkit - Object.assign(state, action.payload); - }, - updateStylesheet: (state, action) => { - state.stylesheet = action.payload; - }, - - // toggles the active code preview editor for conditional rendering - toggleCodePreview: (state) => { - state.codePreview = !state.codePreview; - }, - - // toggles the state of the screenshot trigger to allow a child component to trigger the screenshot of the main component - toggleScreenshotTrigger: (state) => { - state.screenshotTrigger = !state.screenshotTrigger; - } - - // ,configToggle: (state) => { - // state.config.saveFlag = !state.config.saveFlag; - // state.config.saveTimer = !state.config.saveTimer; - // } - } -}); - -// Exports the action creator function to be used with useDispatch - -export const { - addComponent, - addChild, - changeFocus, - changeTailwind, - changePosition, - updateStateUsed, - resetAllState, - updateUseContext, - updateCss, - updateEvents, - deleteEventAction, - deletePage, - deleteReusableComponent, - setProjectName, - changeProjectType, - resetState, - updateProjectName, - updateProjectId, - updateProjectPublished, - deleteElement, - updateAttributes, - deleteChild, - setInitialState, - openProject, - addElement, - undo, - redo, - addState, - addPassedInProps, - deletePassedInProps, - deleteState, - toggleLoggedIn, - //configToggle, - snapShotAction, - allCooperativeState, - updateStylesheet, - toggleCodePreview, - toggleScreenshotTrigger -} = appStateSlice.actions; - -// Exports so we can combine in rootReducer -export default appStateSlice.reducer; +// Main slice for all the component state./// +import { createSlice } from '@reduxjs/toolkit'; +// Import Interfaces for State, and HTML Types +import { + State, + Component, + ChildElement, + HTMLType, + MUIType +} from '../../../interfaces/Interfaces'; +import HTMLTypes from '../../HTMLTypes'; +import MUITypes from '../../MUITypes'; +import generateCode from '../../../helperFunctions/generateCode'; +import manageSeparators from '../../../helperFunctions/manageSeparators'; + +export const initialState: State = { + name: '', + _id: '', + forked: false, + published: false, + isLoggedIn: false, + components: [ + { + id: 1, + name: 'App', + style: {}, + code: '
Drag in a component or HTML element into the canvas!
', + children: [], + isPage: true, + past: [], + future: [], + stateProps: [], + useStateCodes: [], // array of strings for each useState codes + events: {}, // Add the missing 'events' property + passedInProps: [] // Add the missing 'passedInProps' property + } + ], + projectType: 'Classic React', + rootComponents: [1], + canvasFocus: { componentId: 1, childId: null }, + nextComponentId: 2, + nextChildId: 1, + nextTopSeparatorId: 1000, + HTMLTypes: HTMLTypes, // left as is for now + MUITypes: MUITypes, // left as is for now + tailwind: false, + stylesheet: '', + codePreview: false, + screenshotTrigger: false, + customElementId: 10001 +}; + +let separator = initialState.HTMLTypes[1]; + +const findComponent = (components: Component[], componentId: number) => { + return components.find((elem) => elem.id === componentId); +}; + +// Finds a parent +// returns object with parent object and index value of child +const findParent = (component: Component, childId: number) => { + // using a breadth first search to search through instance tree + // We're going to keep track of the nodes we need to search through with an Array + // Initialize this array with the top level node + const nodeArr: (Component | ChildElement)[] = [component]; + // iterate through each node in the array as long as there are elements in the array + while (nodeArr.length > 0) { + // shift off the first value and assign to an element + const currentNode = nodeArr.shift(); + // try to find id of childNode in children + if (currentNode?.children) { + if (currentNode.name !== 'input' && currentNode.name !== 'img') { + for (let i = 0; i <= currentNode.children.length - 1; i++) { + // if match is found return object with both the parent and the index value of the child + if (currentNode.children[i].childId === childId) { + return { directParent: currentNode, childIndexValue: i }; + } + } + // if child node isn't found add each of the current node's children to the search array + currentNode.children.forEach((node: ChildElement) => + nodeArr.push(node) + ); + } + } + } + // if no search is found return -1 + return { directParent: null, childIndexValue: null }; +}; + +// determine if there's a child of a given type in a component +const childTypeExists = ( + type: string, + typeId: number, + component: Component +) => { + const nodeArr = [...component.children]; + // breadth-first search through component tree to see if a child exists + while (nodeArr.length > 0) { + // shift off the first value and assign to an element + const currentNode = nodeArr.shift(); + if (currentNode?.children) { + if (currentNode.type === type && currentNode.typeId === typeId) + return true; + // if child node isn't found add each of the current node's children to the search array + currentNode.children.forEach((node) => nodeArr.push(node)); + } + } + // if no match is found return false + return false; +}; +// find child in component and return child object +const findChild = (component: Component, childId: number) => { + if (childId === null) return component; + const nodeArr = [...component.children]; + // breadth first search through component tree to see if a child exists + while (nodeArr.length > 0) { + // shift off the first value and assign to an element + const currentNode = nodeArr.shift(); + if (currentNode?.children) { + if (currentNode.childId === childId) return currentNode; + // if child node isn't found add each of the current node's children to the search array + if (currentNode.name !== 'input' && currentNode.name !== 'img') + currentNode.children.forEach((node) => nodeArr.push(node)); + } + } + // if no match is found return false + return; +}; +function createHTMLElement(data) { + return { + ...data, + isHTMLElement: true + }; +} + +// update all ids and typeIds to match one another +const updateAllIds = (comp: Component[] | ChildElement[]) => { + // put components' names and ids into an obj + const obj = { spr: 1000, others: 1 }; + // for each of the components, if it has children, iterate through that children array + comp.forEach((el: Component | ChildElement) => { + if (el.children.length > 0) { + for (let i = 0; i < el.children.length; i++) { + // update each child's childId + if (el.children[i].name === 'separator') { + el.children[i].childId = obj['spr']++; + } else { + el.children[i].childId = obj['others']++; + } + // // if the child's name and id exists in the object + // recursively call the updateAllIds function on the child's children array if + // the child's children array is greater than 0 + if (el.children[i].children.length > 0) { + updateAllIds(el.children[i].children); + } + } + } + }); +}; +const updateIds = (components: Component[]) => { + // component IDs should be array index + 1 + components.forEach((comp, i) => (comp.id = i + 1)); + updateAllIds(components); + // create key-value pairs of component names and corresponding IDs + const componentIds = {}; + components.forEach((component) => { + if (!component.isPage) componentIds[component.name] = component.id; + }); + // assign correct ID to components that are children inside of remaining pages + components.forEach((page) => { + if (page.isPage) { + page.children.forEach((child) => { + if (child.type === 'Component') child.typeId = componentIds[child.name]; + }); + } + }); + return components; +}; +// updated compoment updateRoots with TS number type implemented +const updateRoots = (components: Component[]): number[] => { + const roots: number[] = []; + components.forEach((comp) => { + if (comp.isPage) roots.push(comp.id); + }); + return roots; +}; +// updated state property to state from object +const deleteById = (id: number, name: string, state: State): Component[] => { + // name of the component we want to delete + const checkChildren = (child: Component[] | ChildElement[]) => { + // for each of the components in the passed in components array, if the child + // component has a children array, iterate through the array of children + child.forEach((el: Component | ChildElement) => { + if (el.children.length) { + const arr: ChildElement[] = []; + for (let i = 0; i < el.children.length; i++) { + // check to see if the name variable doesn't match the name of the child + if (el.children[i].name !== name) { + // if so, push into the new array the child component + arr.push(el.children[i]); + } + } + // set the children array to be the new array + el.children = arr; + // recursively call the checkChildren function with the updated children array + checkChildren(el.children); + } + }); + }; + // creating a copy of the components array + const copyComp = [...state.components]; + if (copyComp.length) { + checkChildren(copyComp); + } + const filteredArr = [...copyComp].filter((comp) => comp.id != id); + return updateIds(filteredArr); +}; + +const updateUseStateCodes = (currentComponent: Component | ChildElement) => { + // array of snippets of state prop codes + const localStateCode: string[] = []; // avoid never by assigning it to string + currentComponent.stateProps + .filter((n, i) => i % 2 === 0) + .forEach((stateProp) => { + const useStateCode = `const [${stateProp.key}, set${ + stateProp.key.charAt(0).toUpperCase() + stateProp.key.slice(1) + }] = useState<${stateProp.type} | undefined>(${JSON.stringify( + stateProp.value + )})`; + localStateCode.push(useStateCode); + }); + if (currentComponent.name !== 'App' && currentComponent.name !== 'Index') { + currentComponent.passedInProps.forEach((passedInProp) => { + const prop = `const ${passedInProp.key} = props.${passedInProp.key}`; + localStateCode.push(prop); + }); + } + // store localStateCodes in global state context + return localStateCode; +}; + +// Creates new slice for components with applicable reducers +const appStateSlice = createSlice({ + name: 'appState', + initialState, + reducers: { + addComponent: (state, action) => { + if ( + typeof action.payload.componentName !== 'string' || + action.payload.componentName === '' + ) { + return; + } + + const newComponent = { + id: state.components.length + 1, + name: action.payload.componentName, + nextChildId: 1, + style: {}, + attributes: {}, + events: {}, + code: '', + children: [], + isPage: action.payload.root, + past: [], + future: [], + stateProps: [], + useStateCodes: [], + passedInProps: [] + }; + state.components.push(newComponent); + // functionality if the new component will become the root component + if (action.payload.root) state.rootComponents.push(newComponent.id); + // updates the focus to the new component, which redirects to the new blank canvas of said new component + + // change canvas focus to just created component + + const nextComponentId = state.nextComponentId + 1; + newComponent.code = generateCode( + state.components, + newComponent.id, + [...state.rootComponents], + state.projectType, + state.HTMLTypes, + state.MUITypes, + state.tailwind, + action.payload.contextParam + ); + + state.nextComponentId = nextComponentId; + }, + + addChild: (state, action) => { + const { + type, + typeId, + childId + }: { type: string; typeId: number; childId: any } = action.payload; + + // determine the parent component id + const parentComponentId = + action.payload.copyId || state.canvasFocus.componentId; + + // make a copy of the components from state + const components = [...state.components]; + + // find the parent component + const parentComponent = findComponent(components, parentComponentId); + if (!parentComponent) return state; // ensure that parent component exists + + // if type is 'Component', loop through components to find componentName and componentChildren + let componentName: string = ''; + let componentChildren: ChildElement[] = []; + + if (type === 'Component') { + const originalComponent = findComponent(state.components, typeId); + if (originalComponent) { + componentName = originalComponent.name; + componentChildren = originalComponent.children; + if ( + childTypeExists('Component', parentComponentId, originalComponent) + ) + return state; + } + } else if (type === 'Route Link') { + const routeLinkComponent = components.find( + (comp) => comp.id === typeId + ); + if (routeLinkComponent) { + componentName = routeLinkComponent.name; + } + } else if (type === 'HTML Element') { + componentName = state.HTMLTypes.reduce((name, el) => { + if (typeId === el.id) { + name = el.tag; + } + return name; + }, ''); + } else { + componentName = state.MUITypes.reduce((name, el) => { + if (typeId === el.id) { + name = el.tag; + } + return name; + }, ''); + } + + const newChild: ChildElement = { + type, + typeId, + name: componentName, + childId: state.nextChildId, + style: {}, + attributes: {}, + events: {}, + children: componentChildren, // work in progress possible solution // children: componentChildren as ChildElement[], + stateProps: [], //legacy pd: added stateprops and passedinprops + passedInProps: [] + }; + + // added missing properties + const topSeparator: ChildElement = { + type: 'HTML Element', + typeId: separator.id, + name: 'separator', + childId: state.nextTopSeparatorId, + style: separator.style, + attributes: {}, + events: {}, // Added + children: [], + stateProps: [], // Added + passedInProps: [] // Added + }; + // if the childId is null, this signifies that we are adding a child to the top-level component rather than another child element + // we also add a separator before any new child + // if the newChild Element is an input or img type, delete the children key/value pair + // if (newChild.name === 'input' && newChild.name === 'img') + // delete newChild.children; + // let directParent: HTMLElement | any; + if (childId === null) { + parentComponent.children.push(topSeparator, newChild); + } else { + const directParent = findChild(parentComponent, childId); + if (directParent) { + if (directParent.type === 'HTML Element' && type === 'HTML Element') { + directParent.children.push(topSeparator, newChild); + } else { + return { ...state }; + } + } + } + + // update canvasFocus to the new child + const canvasFocus = { + ...state.canvasFocus, + componentId: state.canvasFocus.componentId, + childId: newChild.childId + }; + + // Increment IDs + state.nextChildId += 1; + state.nextTopSeparatorId += 1; + + // Update the components array and potentially other parts of the state + components[parentComponentId - 1].children = + manageSeparators.mergeSeparator( + components[parentComponentId - 1].children, + 1 + ); + + // Generate code for the component + parentComponent.code = generateCode( + components, + parentComponentId, + [...state.rootComponents], + state.projectType, + state.HTMLTypes, + state.MUITypes, + state.tailwind, + action.payload.contextParam + ); + + // Update state + state.components = components; + state.canvasFocus = canvasFocus; + + return state; + }, + + changeTailwind: (state, action) => { + // return { ...state, tailwind: action.payload } + state.tailwind = action.payload; + }, + changeFocus: (state, action) => { + const { componentId, childId } = action.payload; + // makes separators not selectable + if (childId < 1000) { + //update componentId and childId in the state + state.canvasFocus = { ...state.canvasFocus, componentId, childId }; + //makes it so the code preview will update when clicking on a new component + state.components = state.components.map((element) => { + return Object.assign({}, element); + }); + } + }, + + updateStateUsed: (state, action) => { + const { stateUsedObj } = action.payload; + const components = [...state.components]; + const component = findComponent( + components, + state.canvasFocus.componentId + ); + if (component) { + if (state.canvasFocus.childId !== null) { + const targetChild = findChild(component, state.canvasFocus.childId); + if (targetChild) { + targetChild.stateUsed = stateUsedObj; + component.code = generateCode( + components, + state.canvasFocus.componentId, + [...state.rootComponents], + state.projectType, + state.HTMLTypes, + state.MUITypes, + state.tailwind, + action.payload.contextParam + ); + state.components = components; + } + } + } + }, + + updateUseContext: (state, action) => { + const { useContextObj } = action.payload; + const components = [...state.components]; + const component = findComponent( + components, + state.canvasFocus.componentId + ); + if (component) { + component.useContext = useContextObj; + component.code = generateCode( + components, + state.canvasFocus.componentId, + [...state.rootComponents], + state.projectType, + state.HTMLTypes, + state.MUITypes, + state.tailwind, + action.payload.contextParam + ); + state.components = components; + } + }, + + resetAllState: (state) => { + if (state.isLoggedIn) { + Object.assign(state, initialState); + state.isLoggedIn = true; + return; + } + Object.assign(state, initialState); + }, + changePosition: (state, action) => { + const { currentChildId, newParentChildId } = action.payload; + // if the currentChild Id is the same as the newParentId (i.e. a component is trying to drop itself into itself), don't update state + if (currentChildId === newParentChildId) return state; + // find the current component in focus + let components = [...state.components]; + const component = findComponent( + components, + state.canvasFocus.componentId + ); + // find the moved element's former parent + // delete the element from it's former parent's children array + if (component) { + const { directParent, childIndexValue } = findParent( + component, + currentChildId + ); + // BREAKING HERE during manipulation of positions. Sometimes get a null value when manipulating positions + // Only run if the directParent exists + if (directParent) { + if (directParent.children) { + const child = { ...directParent.children[childIndexValue] }; + directParent.children.splice(childIndexValue, 1); + // if the childId is null, this signifies that we are adding a child to the top level component rather than another child element + if (newParentChildId === null) { + component.children.push(child); + } + // if there is a childId (childId here references the direct parent of the new child) find that child and a new child to its children array + else { + const directParent = findChild(component, newParentChildId); + if (directParent?.children) { + directParent.children.push(child); + } + } + } + } + let nextTopSeparatorId = state.nextTopSeparatorId; + components[state.canvasFocus.componentId - 1].children = + manageSeparators.mergeSeparator( + components[state.canvasFocus.componentId - 1].children, + 0 + ); + nextTopSeparatorId = manageSeparators.handleSeparators( + components[state.canvasFocus.componentId - 1].children, + 'change position' + ); + component.code = generateCode( + components, + state.canvasFocus.componentId, + [...state.rootComponents], + state.projectType, + state.HTMLTypes, + state.MUITypes, + state.tailwind, + action.payload.contextParam + ); + state.components = components; + state.nextTopSeparatorId = nextTopSeparatorId; + } + }, + + updateCss: (state, action) => { + const { style } = action.payload; + const components = [...state.components]; + const component = findComponent( + components, + state.canvasFocus.componentId + ); + // closed if statement at the end of the block + if (component && state.canvasFocus.childId) { + const targetChild = findChild(component, state.canvasFocus.childId); + if (targetChild) { + targetChild.style = style; + component.code = generateCode( + components, + state.canvasFocus.componentId, + [...state.rootComponents], + state.projectType, + state.HTMLTypes, + state.MUITypes, + state.tailwind, + action.payload.contextParam + ); + state.components = components; + } + } + }, + + updateAttributes: (state, action) => { + const { attributes } = action.payload; + + const components = [...state.components]; + const component = findComponent( + components, + state.canvasFocus.componentId + ); + // closed if statement at the end of the block + if (component && state.canvasFocus.childId) { + const targetChild = findChild(component, state.canvasFocus.childId); + if (targetChild) { + targetChild.attributes = attributes; + + component.code = generateCode( + components, + state.canvasFocus.componentId, + [...state.rootComponents], + state.projectType, + state.HTMLTypes, + state.MUITypes, + state.tailwind, + action.payload.contextParam + ); + state.components = components; + } + } + }, + + updateEvents: (state, action) => { + const { events } = action.payload; + if (JSON.stringify(events) === '{}') return state; + const components = [...state.components]; + const component = findComponent( + components, + state.canvasFocus.componentId + ); + if (component && state.canvasFocus.childId) { + const targetChild = findChild(component, state.canvasFocus.childId); + const event = Object.keys(events)[0]; + const funcName = events[event]; + if (targetChild) { + targetChild.events[event] = funcName; + + component.code = generateCode( + components, + state.canvasFocus.componentId, + [...state.rootComponents], + state.projectType, + state.HTMLTypes, + state.MUITypes, + state.tailwind, + action.payload.contextParam + ); + state.components = components; + } + } + }, + + deleteEventAction: (state, action) => { + const { event } = action.payload; + const components = [...state.components]; + const component = findComponent( + components, + state.canvasFocus.componentId + ); + if (component && state.canvasFocus.childId) { + const targetChild = findChild(component, state.canvasFocus.childId); + if (targetChild) { + delete targetChild.events[event]; + + component.code = generateCode( + components, + state.canvasFocus.componentId, + [...state.rootComponents], + state.projectType, + state.HTMLTypes, + state.MUITypes, + state.tailwind, + action.payload.contextParam + ); + state.components = components; + } + } + }, + + deletePage: (state, action) => { + const id: number = state.canvasFocus.componentId; + const name: string = state.components[id - 1].name; + const components: Component[] = deleteById(id, name, state); + // rebuild rootComponents with correct page IDs + const rootComponents = updateRoots(components); + const canvasFocus = { componentId: 1, childId: null }; + state.rootComponents = rootComponents; + state.components = components; + state.canvasFocus = canvasFocus; + }, + + deleteReusableComponent: (state, action) => { + const id: number = state.canvasFocus.componentId; + const name: string = state.components[id - 1].name; + // updated list of components after deleting a component + const components: Component[] = deleteById(id, name, state); + const rootComponents: number[] = updateRoots(components); + // iterate over the length of the components array + for (let i = 0; i < components.length; i++) { + //if the component uses context from component being deleted + if (components[i].useContext && components[i].useContext[id]) { + // iterate over children to see where it is being used, then reset that compText/compLink/useState + for (let child of components[i].children) { + if (child.stateUsed) { + if (child.stateUsed.compTextProviderId === id) { + child.attributes.compText = ''; + delete child.stateUsed.compText; + delete child.stateUsed.compTextProviderId; + delete child.stateUsed.compTextPropsId; + } + if (child.stateUsed.compLinkProviderId === id) { + child.attributes.compLink = ''; + delete child.stateUsed.compLink; + delete child.stateUsed.compLinkProviderId; + delete child.stateUsed.compLinkPropsId; + } + } + } + delete components[i].useContext[id]; + } + + // for each component's code, run the generateCode function to + // update the code preview on the app + components[i].code = generateCode( + components, + components[i].id, + rootComponents, + state.projectType, + state.HTMLTypes, + state.MUITypes, + state.tailwind, + action.payload.contextParam + ); + } + const canvasFocus = { componentId: 1, childId: null }; + + state.rootComponents = rootComponents; + state.components = components; + state.canvasFocus = canvasFocus; + state.nextComponentId = id; + }, + setProjectName: (state, action) => { + state.name = action.payload; + }, + changeProjectType: (state, action) => { + // when a project type is changed, both change the project type in state and also regenerate the code for each component + const { projectType } = action.payload; + + const components = [...state.components]; + // also update the name of the root component of the application to fit classic React and next.js/gatsby conventions + if (projectType === 'Next.js' || projectType === 'Gatsby.js') + components[0]['name'] = 'index'; + else components[0]['name'] = 'App'; + components.forEach((component) => { + component.code = generateCode( + components, + component.id, + [...state.rootComponents], + projectType, + state.HTMLTypes, + state.MUITypes, + state.tailwind, + action.payload.contextParam + ); + }); + state.components = components; + state.projectType = projectType; + }, + + resetState: (state, action) => { + const nextChildId = 1; + const nextTopSeparatorId = 1000; + const rootComponents = [1]; + const nextComponentId = 2; + const canvasFocus = { + ...state.canvasFocus, + componentId: 1, + childId: null + }; + const rootComponent = { + ...state.components[0], + code: '
Drag in a component or HTML element into the canvas!
', + children: [], + style: {} + }; + const components = [rootComponent]; + + const stylesheet = ''; + const resetHTMLTypes = HTMLTypes; + const resetMUITypes = MUITypes; + + return { + ...state, + nextChildId, + nextTopSeparatorId, + rootComponents, + nextComponentId, + components, + canvasFocus, + stylesheet, + HTMLTypes: resetHTMLTypes, + MUITypes: resetMUITypes + }; + }, + updateProjectName: (state, action) => { + const projectName = action.payload; + state.name = projectName; + }, + updateProjectId: (state, action) => { + const projectId = action.payload; //updates the slice with new _id + state._id = projectId; + }, + updateProjectPublished: (state, action) => { + const projectPublished = action.payload; + state.published = projectPublished; + }, + deleteElement: (state, action) => { + let name: string = ''; + const HTMLTypes: HTMLType[] = [...state.HTMLTypes].filter((el) => { + if (el.id === action.payload.id) { + name = el.tag; + } + return el.id !== action.payload.id; + }); + // const MUITypes: MUIType[] = [...state.MUITypes].filter((el) => { + // if (el.id === action.payload.id) { + // name = el.tag; + // } + // return el.id !== action.payload.id; + // }); + const components: Component[] = deleteById( + action.payload.id, + name, + state + ); + const rootComponents: number[] = updateRoots(components); + const canvasFocus = { ...state.canvasFocus, childId: null }; + components.forEach((el, i) => { + el.code = generateCode( + components, + components[i].id, + rootComponents, + state.projectType, + state.HTMLTypes, + state.MUITypes, + state.tailwind, + action.payload.contextParam + ); + }); + + state.canvasFocus = canvasFocus; + state.HTMLTypes = HTMLTypes; + // state.MUITypes = MUITypes; + }, + + deleteChild: (state, action) => { + // if in-focus instance is a top-level component and not a child, don't delete anything + if (!state.canvasFocus.childId) return state; + // find the current component in focus + const components = [...state.components]; + const component = findComponent( + components, + state.canvasFocus.componentId + ); + // find the moved element's former parent + const { directParent, childIndexValue } = findParent( + component, + state.canvasFocus.childId + ); + + // ------------------------------------------- ALSO added code below ------------------------------------------- + + let canvasFocus = { ...state.canvasFocus, childId: null }; // ADDED to avoid null error + let nextTopSeparatorId = 1000; // ADDED to avoid null error + const childIdDeleteClicked = action.payload.id; // ADDED to ensure no cross-element deletion possible + + // delete the element from its former parent's children array, subject to below conditional to avoid null error + if ( + directParent && + (state.canvasFocus.childId === childIdDeleteClicked || + JSON.stringify(action.payload.id) === '{}') // Ensuring deletion works for mouseclick OR using delete key, from 2 different dispatch sources + ) { + if (directParent.children) { + directParent.children.splice(childIndexValue, 1); + let nextTopSeparatorId = manageSeparators.handleSeparators( + components[canvasFocus.componentId - 1].children, + 'delete' + ); + } + } + + // ------------------------------------------- ALSO added code above ------------------------------------------- + component.code = generateCode( + components, + state.canvasFocus.componentId, + [...state.rootComponents], + state.projectType, + state.HTMLTypes, + state.MUITypes, + state.tailwind, + action.payload.contextParam + ); + state.components = components; + state.canvasFocus = canvasFocus; + state.nextTopSeparatorId = nextTopSeparatorId; + }, + setInitialState: (state, action) => { + // set the canvas focus to be the first component + const canvasFocus = { + ...action.payload.canvasFocus, + componentId: 1, + childId: null + }; + state.canvasFocus = canvasFocus; + }, + //deleted 'convertToJSX' function, which threw errors upon opening + openProject: (state, action) => { + // returning the action.payload is a Redux shortcut that updates the entire app state at the same time + return action.payload; + }, + addElement: (state, action) => { + const HTMLTypes = [...state.HTMLTypes]; + HTMLTypes.push(action.payload); + state.HTMLTypes = HTMLTypes; + state.customElementId += 1; + }, + //Undo & Redo functions are not working properly. Redo & Undo target the last component rather than last added HTML Element. + undo: (state, action) => { + const focusIndex = state.canvasFocus.componentId - 1; + // if the past array is empty, return state + if (state.components[focusIndex].past.length <= 1) { + return { ...state }; + } + // the children array of the focused component will equal the last element of the past array + state.components[focusIndex].children = + state.components[focusIndex].past[ + state.components[focusIndex].past.length - 1 + ]; + // the last element of the past array gets popped off + const poppedEl: Component = state.components[focusIndex].past.pop(); + // the last element of the past array gets popped off and pushed into the future array + state.components[focusIndex].future.push(poppedEl); + //generate code for the Code Preview + state.components.forEach((el, i) => { + el.code = generateCode( + state.components, + state.components[i].id, + state.rootComponents, + state.projectType, + state.HTMLTypes, + state.MUITypes, + state.tailwind, + action.payload.contextParam + ); + }); + + state = state; + }, + redo: (state, action) => { + const focusIndex = state.canvasFocus.componentId - 1; + //if future array is empty, return state + if (state.components[focusIndex].future.length === 0) return { ...state }; + //the children array of the focused component will equal the last element of the future array + state.components[focusIndex].children = + state.components[focusIndex].future[ + state.components[focusIndex].future.length - 1 + ]; + //the last element of the future array gets pushed into the past + const poppedEl: Component = state.components[focusIndex].future.pop(); + //the last element of the future array gets popped out + state.components[focusIndex].past.push(poppedEl); + // generate code for the Code Preview + state.components.forEach((el, i) => { + el.code = generateCode( + state.components, + state.components[i].id, + state.rootComponents, + state.projectType, + state.HTMLTypes, + state.MUITypes, + state.tailwind, + action.payload.contextParam + ); + }); + state = state; + }, + addState: (state, action) => { + // find the current component in focus + const components = [...state.components]; + const currComponent = findComponent( + components, + state.canvasFocus.componentId + ); + //will add update StateProps to current components' array + currComponent.stateProps.push(action.payload.newState); + currComponent.useStateCodes = updateUseStateCodes(currComponent); + currComponent.stateProps.push(action.payload.setNewState); + currComponent.code = generateCode( + components, + state.canvasFocus.componentId, + [...state.rootComponents], + state.projectType, + state.HTMLTypes, + state.MUITypes, + state.tailwind, + action.payload.contextParam + ); + state.components = components; + }, + addPassedInProps: (state, action) => { + //When props are passed from a parent to a child in the State Manager tab, it will update the components available passedinprops + // find the current component in focus + const components = [...state.components]; + const currComponent = findComponent( + components, + state.canvasFocus.componentId + ); + //prevents passing in props more than one time to the current component + for (let i = 0; i < currComponent.passedInProps.length; i++) { + let curr = currComponent.passedInProps[i]; + if (curr.id === action.payload.passedInProps.id) { + return { ...state, components }; + } + } + + //find the parent for deleting instances of where the parent is passing props to children + let parent: Component; + for (let i = 0; i < components.length; i++) { + let currComponent = components[i]; + for (let j = 0; j < currComponent.children.length; j++) { + let currChild = currComponent.children[j]; + if (currChild.typeId === state.canvasFocus.componentId) { + parent = currComponent; + } + } + } + + //search for whether the child exists in the parent's children array + //if so update the passed in props child element with the updates passed in props + parent.children.forEach((child) => { + if (child.name === currComponent.name) { + child.passedInProps.push(action.payload.passedInProps); + } + }); + + // check each components passedInProps property and updating there as well. + currComponent.passedInProps.push(action.payload.passedInProps); + + //update the import codes for the current component + currComponent.useStateCodes = updateUseStateCodes(currComponent); + //update code preview for current component + currComponent.code = generateCode( + components, + state.canvasFocus.componentId, + [...state.rootComponents], + state.projectType, + state.HTMLTypes, + state.MUITypes, + state.tailwind, + action.payload.contextParam + ); + //update code preview for parent component (since we have added it to the children array) + parent.code = generateCode( + components, + parent.id, + [...state.rootComponents], + state.projectType, + state.HTMLTypes, + state.MUITypes, + state.tailwind, + action.payload.contextParam + ); + + // return { ...state, components }; + state.components = components; + }, + deletePassedInProps: (state, action) => { + const components = [...state.components]; + let currComponent = findComponent( + components, + state.canvasFocus.componentId + ); + + //find the parent of the component that we are deleting from + let parent: Component; + for (let i = 0; i < components.length; i++) { + let currComponent = components[i]; + for (let j = 0; j < currComponent.children.length; j++) { + let currChild = currComponent.children[j]; + if (currChild.typeId === state.canvasFocus.componentId) { + parent = currComponent; + } + } + } + + //deletes all instances of passedInProps from the children arrays of the current Component + const deletePassedInPropsChildren = (currComponent: Component) => { + const innerFunc = (currChild: Component | ChildElement) => { + // when there are no children, return up a level + if ( + currChild.children.filter((el) => el.type === 'Component') + .length === 0 + ) + return; + if (currChild.children.length) { + currChild.children + .filter((el) => el.type === 'Component') + .forEach((child, j) => { + child.passedInProps.forEach((prop, k) => { + if (prop.id === action.payload.rowId) { + child.passedInProps.splice(k, 1); + innerFunc(child); + } + }); + }); + } + }; + //for every component we update, generate new code + innerFunc(currComponent); + currComponent.code = generateCode( + components, + currComponent.id, + [...state.rootComponents], + state.projectType, + state.HTMLTypes, + state.MUITypes, + state.tailwind, + action.payload.contextParam + ); + }; + //delete from the components passedInProps array + const deletePassedInProps = (myComponent: Component) => { + if ( + myComponent.children.filter((el) => el.type === 'Component') + .length === 0 + ) { + if (myComponent.passedInProps.length > 0) { + myComponent.passedInProps.forEach((prop, index) => { + if (prop.id === action.payload.rowId) { + myComponent.passedInProps.splice(index, 1); + } + }); + } + myComponent.useStateCodes = updateUseStateCodes(myComponent); + myComponent.code = generateCode( + components, + myComponent.id, + [...state.rootComponents], + state.projectType, + state.HTMLTypes, + state.MUITypes, + state.tailwind, + action.payload.contextParam + ); + return; + } + myComponent.passedInProps.forEach((prop, i) => { + if (prop.id === action.payload.rowId) { + myComponent.passedInProps.splice(i, 1); + myComponent.children + .filter((el) => el.type === 'Component') + .forEach((child, i) => { + let next = components.find((comp) => comp.id === child.typeId); + deletePassedInProps(next); + }); + } + }); + myComponent.useStateCodes = updateUseStateCodes(myComponent); + myComponent.code = generateCode( + components, + myComponent.id, + [...state.rootComponents], + state.projectType, + state.HTMLTypes, + state.MUITypes, + state.tailwind, + action.payload.contextParam + ); + }; + + deletePassedInPropsChildren(parent); + deletePassedInProps(currComponent); + + parent.code = generateCode( + components, + parent.id, + [...state.rootComponents], + state.projectType, + state.HTMLTypes, + state.MUITypes, + state.tailwind, + action.payload.contextParam + ); + state.components = components; + }, + deleteState: (state, action) => { + const components = [...state.components]; + let currComponent = findComponent( + components, + state.canvasFocus.componentId + ); + //updates the stateProps array to reflect total state initialized in component minus the selected state to be deleted + currComponent.stateProps = action.payload.stateProps; + + //CHILDREN ARRAY LOOP (needed for code preview) + //iterate through all components, starting from top, and delete ALL instances of deleted state (provided to us + // in the passedInProps array within the children array of the component + // using the action.payload.rowId (variable name) and action.payload.otherId (setVariable name)) + components.forEach((component) => { + //find all instances of state within child elements and delete state + + component.children.forEach((child) => { + if (child.type === 'Component') { + for (let i = 0; i < child.passedInProps?.length; i++) { + if ( + child.passedInProps[i]['id'] === action.payload.rowId || + child.passedInProps[i]['id'] === action.payload.otherId + ) { + child.passedInProps.splice(i, 1); + i--; + } + } + } + }); + + // COMPONENT LOOP (needed for tables in State Management Tab) + //iterate through all components, starting from top, and delete ALL instances of deleted state (provided to us + // in the passedInProps array within each component + // using the action.payload.rowId (variable name) and action.payload.otherId (setVariable name)) + for (let i = 0; i < component.passedInProps?.length; i++) { + if ( + component.passedInProps[i]['id'] === action.payload.rowId || + component.passedInProps[i]['id'] === action.payload.otherId + ) { + component.passedInProps.splice(i, 1); + i--; + } + } + // curr component = where you are deleting from state from, also is the canvas focus + // curr component id = providerId + // we then iterate through the rest of the components + // check if a useContext if created and if the useContext contains the providerId + // we then delete from the set, statesFromProvider, the row id, and regenerate the code + // Ex: useContext {1: {statesFromProvider: Set, compLink, compText}, 2 : ..., 3 : ...} + if ( + component.useContext && + component.useContext[state.canvasFocus.componentId] + ) { + component.useContext[ + state.canvasFocus.componentId + ].statesFromProvider.delete(action.payload.rowId); + // iterate over children to see where it is being used, then reset that compText/compLink/useState + for (let child of component.children) { + if (child.stateUsed) { + if ( + child.stateUsed.compTextProviderId === currComponent.id && + child.stateUsed.compTextPropsId === action.payload.rowId + ) { + child.attributes.compText = ''; + delete child.stateUsed.compText; + delete child.stateUsed.compTextProviderId; + delete child.stateUsed.compTextPropsId; + } + if ( + child.stateUsed.compLinkProviderId === currComponent.id && + child.stateUsed.compLinkPropsId === action.payload.rowId + ) { + child.attributes.compLink = ''; + delete child.stateUsed.compLink; + delete child.stateUsed.compLinkProviderId; + delete child.stateUsed.compLinkPropsId; + } + } + } + } + + // find parent + let parent; + for (let i = 0; i < components.length; i++) { + let currComponent = components[i]; + for (let j = 0; j < currComponent.children.length; j++) { + let currChild = currComponent.children[j]; + if (currChild.typeId === component.id) { + parent = currComponent; + } + } + } + if (parent) { + parent.code = generateCode( + components, + parent.id, + [...state.rootComponents], + state.projectType, + state.HTMLTypes, + state.MUITypes, + state.tailwind, + action.payload.contextParam + ); + } + + component.useStateCodes = updateUseStateCodes(component); + component.code = generateCode( + components, + component.id, + [...state.rootComponents], + state.projectType, + state.HTMLTypes, + state.MUITypes, + state.tailwind, + action.payload.contextParam + ); + }); + state.components = components; + }, + + toggleLoggedIn: (state, action) => { + state.isLoggedIn = action.payload; + }, + + snapShotAction: (state, action) => { + state.components[action.payload.focusIndex].past.push( + action.payload.deepCopiedState.components[action.payload.focusIndex] + .children + ); + }, + allCooperativeState: (state, action) => { + // return Object.assign({}, state, action.payload); + // it is safe to directly assign the properties from 'action.payload' to the state in Redux Toolkit + Object.assign(state, action.payload); + }, + updateStylesheet: (state, action) => { + state.stylesheet = action.payload; + }, + + // toggles the active code preview editor for conditional rendering + toggleCodePreview: (state) => { + state.codePreview = !state.codePreview; + }, + + // toggles the state of the screenshot trigger to allow a child component to trigger the screenshot of the main component + toggleScreenshotTrigger: (state) => { + state.screenshotTrigger = !state.screenshotTrigger; + } + + // ,configToggle: (state) => { + // state.config.saveFlag = !state.config.saveFlag; + // state.config.saveTimer = !state.config.saveTimer; + // } + } +}); + +// Exports the action creator function to be used with useDispatch + +export const { + addComponent, + addChild, + changeFocus, + changeTailwind, + changePosition, + updateStateUsed, + resetAllState, + updateUseContext, + updateCss, + updateEvents, + deleteEventAction, + deletePage, + deleteReusableComponent, + setProjectName, + changeProjectType, + resetState, + updateProjectName, + updateProjectId, + updateProjectPublished, + deleteElement, + updateAttributes, + deleteChild, + setInitialState, + openProject, + addElement, + undo, + redo, + addState, + addPassedInProps, + deletePassedInProps, + deleteState, + toggleLoggedIn, + //configToggle, + snapShotAction, + allCooperativeState, + updateStylesheet, + toggleCodePreview, + toggleScreenshotTrigger +} = appStateSlice.actions; + +// Exports so we can combine in rootReducer +export default appStateSlice.reducer; diff --git a/package-lock.json b/package-lock.json index 52eabcc3..92a81fea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -54,7 +54,6 @@ "html2canvas": "^1.4.1", "identity-obj-proxy": "^3.0.0", "js-cookie": "^3.0.5", - "jsdoc": "^4.0.2", "jszip": "^3.10.1", "localforage": "^1.10.0", "lodash": "^4.17.21", @@ -136,6 +135,7 @@ "html-webpack-plugin": "^4.5.2", "jest": "^28.1.3", "jest-environment-jsdom": "^28.1.3", + "jsdoc": "^4.0.2", "mini-css-extract-plugin": "^2.7.6", "mongodb": "^3.5.9", "nodemon": "^3.0.2", @@ -10954,6 +10954,7 @@ "version": "0.2.8", "resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.8.tgz", "integrity": "sha512-5e+SFVavj1ORKlKaKr2BmTOekmXbelU7dC0cDkQLqag7xfuTPuGMUFx7KWJuv4bYZrTsoL2Z18VVCOKYxzoHcg==", + "dev": true, "dependencies": { "lodash": "^4.17.21" }, @@ -14746,7 +14747,8 @@ "node_modules/@types/linkify-it": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.5.tgz", - "integrity": "sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw==" + "integrity": "sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw==", + "dev": true }, "node_modules/@types/long": { "version": "4.0.2", @@ -14757,6 +14759,7 @@ "version": "12.2.3", "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz", "integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==", + "dev": true, "dependencies": { "@types/linkify-it": "*", "@types/mdurl": "*" @@ -14765,7 +14768,8 @@ "node_modules/@types/mdurl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.5.tgz", - "integrity": "sha512-6L6VymKTzYSrEf4Nev4Xa1LCHKrlTlYCBMTlQKFuddo1CvQcE52I0mwfOJayueUC7MJuXOeHTcIU683lzd0cUA==" + "integrity": "sha512-6L6VymKTzYSrEf4Nev4Xa1LCHKrlTlYCBMTlQKFuddo1CvQcE52I0mwfOJayueUC7MJuXOeHTcIU683lzd0cUA==", + "dev": true }, "node_modules/@types/mime": { "version": "1.3.5", @@ -18102,7 +18106,8 @@ "node_modules/bluebird": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", - "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true }, "node_modules/bluebird-lst": { "version": "1.0.9", @@ -18638,6 +18643,7 @@ "version": "0.9.0", "resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.9.0.tgz", "integrity": "sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A==", + "dev": true, "dependencies": { "lodash": "^4.17.15" }, @@ -27152,6 +27158,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz", "integrity": "sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA==", + "dev": true, "dependencies": { "xmlcreate": "^2.0.4" } @@ -27244,6 +27251,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-4.0.2.tgz", "integrity": "sha512-e8cIg2z62InH7azBBi3EsSEqrKx+nUtAS5bBcYTSpZFA+vhNPyhv8PTFZ0WsjOPDj04/dOLlm08EDcQJDqaGQg==", + "dev": true, "dependencies": { "@babel/parser": "^7.20.15", "@jsdoc/salty": "^0.2.1", @@ -27272,6 +27280,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, "engines": { "node": ">=8" } @@ -27543,6 +27552,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz", "integrity": "sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==", + "dev": true, "dependencies": { "graceful-fs": "^4.1.9" } @@ -27652,6 +27662,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", + "dev": true, "dependencies": { "uc.micro": "^1.0.1" } @@ -28431,6 +28442,7 @@ "version": "12.3.2", "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", + "dev": true, "dependencies": { "argparse": "^2.0.1", "entities": "~2.1.0", @@ -28446,6 +28458,7 @@ "version": "8.6.7", "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-8.6.7.tgz", "integrity": "sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA==", + "dev": true, "peerDependencies": { "@types/markdown-it": "*", "markdown-it": "*" @@ -28454,12 +28467,14 @@ "node_modules/markdown-it/node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true }, "node_modules/markdown-it/node_modules/entities": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==", + "dev": true, "funding": { "url": "https://github.com/fb55/entities?sponsor=1" } @@ -28468,6 +28483,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", + "dev": true, "bin": { "marked": "bin/marked.js" }, @@ -28496,7 +28512,8 @@ "node_modules/mdurl": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", - "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==" + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", + "dev": true }, "node_modules/media-typer": { "version": "0.3.0", @@ -32392,6 +32409,7 @@ "version": "0.2.4", "resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.4.tgz", "integrity": "sha512-JRrFk1D4OQ4SqovXOgdav+K8EAhSB/LJZqCz8tbX0KObcdeM15Ss59ozWMBWmmINMagCwmqn4ZNryUGpBsl6Jw==", + "dev": true, "dependencies": { "lodash": "^4.17.21" } @@ -34921,7 +34939,8 @@ "node_modules/uc.micro": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", - "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==" + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", + "dev": true }, "node_modules/uid-safe": { "version": "2.1.5", @@ -36424,7 +36443,8 @@ "node_modules/xmlcreate": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz", - "integrity": "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==" + "integrity": "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==", + "dev": true }, "node_modules/xmlhttprequest-ssl": { "version": "2.0.0", diff --git a/package.json b/package.json index ecc69404..9b0350bb 100644 --- a/package.json +++ b/package.json @@ -167,7 +167,6 @@ "html2canvas": "^1.4.1", "identity-obj-proxy": "^3.0.0", "js-cookie": "^3.0.5", - "jsdoc": "^4.0.2", "jszip": "^3.10.1", "localforage": "^1.10.0", "lodash": "^4.17.21", @@ -249,6 +248,7 @@ "html-webpack-plugin": "^4.5.2", "jest": "^28.1.3", "jest-environment-jsdom": "^28.1.3", + "jsdoc": "^4.0.2", "mini-css-extract-plugin": "^2.7.6", "mongodb": "^3.5.9", "nodemon": "^3.0.2",