diff --git a/README.md b/README.md index 315d1668b..cbabd22fa 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ Download for [MacOS](https://github.com/team-reactype/ReacType/releases), [Windo ### How to use -- **Sign-in page**: Sign up for an account, sign in via Github, or just continue as a guest. Registered users enjoy additional project-saving functionality. +- **Sign-in page**: Sign up for an account or just continue as a guest. Registered users enjoy additional project-saving functionality. - **Tutorial**: Click ‘Tutorial’ from the Help tab’s dropdown menu (at the top left of the application) to view a tutorial. - **Start a project (only after registration)**: Registered users can create a new project and select whether they want their project to be a Next.js, Gatsby.js, or classic React project. Also, registered users can save projects to return to them at a later time. - **Add Components**: Create components on the right panel. Components can be associated with a route, or they can be used within other components. @@ -34,6 +34,7 @@ Download for [MacOS](https://github.com/team-reactype/ReacType/releases), [Windo - **Create Instances on the Canvas**: Each component has its own canvas. Add an element to a component by dragging it onto the canvas. Div components are arbitrarily nestable and useful for complex layouts. Next.js and Gatsby.js projects have Link components to enable client-side navigation to other routes. - **Component Tree**: Click on the Component Tree tab next to the Code Preview tab to view the component tree hierarchy. - **Update Styling**: Select an element on the canvas to update its basic style attributes on the right panel. As you create new instances and add styling, watch as your code dynamically generates in the code preview in the bottom panel. +- **Using State in Elements**: As of 9.0.0, you can now select an HTML element on the canvas and then navigate to the customization panel to click a button to pass a variable from state into your element's text or link field. - **User Preference Features**: With the click of a button, toggle between light mode and dark mode, depending on your preference. - **Export project**: Click the “Export Project’ button to export the project’s application files into a TypeScript file. The exported project is fully functional with Webpack, Express server, routing, etc., and will match what is mocked on the canvas. - **Export project with Tests**: Click the "includes tests" checkbox while exporting to include pre-configured Webpack, Jest, and Typescript files along with tests for your project. @@ -43,11 +44,17 @@ Download for [MacOS](https://github.com/team-reactype/ReacType/releases), [Windo - Modernized and cleaner UI, including enhanced dark mode - Tutorial has been updated to reflect other modifications - **New with version 8.0.0:** + **New with version 9.0.0:** ![CSSEditor](https://raw.githubusercontent.com/open-source-labs/ReacType/master/resources/export_tests_images/export-new.gif) -### Features +### 9.0.0 New Features +- **React Router**: Drag-and-drop LinkTo and Router elements (located below the HTML elements list) to implement fully-functional React Router components into your application to dynamically and quickly render components in the live demo render and in your exported application. +- **Global State Management**: For the first time in ReacType history, users can utilize +- **Composite Data Structures in State**: State creation in ReacType can now handle composite data types, which includes arrays, objects, and any amount of nesting of composite data types within other composite data types (i.e. arrays of objects and objects with objects as values). +- **Additional Improvements with Local State Management**: +- **Annotations**: + - **OAuth via Github**: Sign up with your github accounts. - **Live Render Demo**: Live render demo in React using Electron's sandbox environment. Updates in realtime to reflect canvas structure and customization options. @@ -81,7 +88,7 @@ Download for [MacOS](https://github.com/team-reactype/ReacType/releases), [Windo - Delete Project: Command + Backspace - Open Project: Command + o -**Windows**: +**Windows**: - Export Project: Control + e - Undo: Control + z - Redo: Control + Shift + z @@ -90,7 +97,7 @@ Download for [MacOS](https://github.com/team-reactype/ReacType/releases), [Windo - Delete HTML Tag on Canvas: Backspace - Delete Project: Control + Backspace - Open Project: Control + o - + #### Contributors [Alex Yu](https://www.linkedin.com/in/alexjihunyu/) [@buddhajjigae](https://github.com/buddhajjigae) @@ -111,8 +118,12 @@ Download for [MacOS](https://github.com/team-reactype/ReacType/releases), [Windo [Chelsey Fewer](https://www.linkedin.com/in/chelsey-fewer/) [@chelseyeslehc](https://github.com/chelseyeslehc) +[Chris Tang] (https://www.linkedin.com/in/chrisjtang/) [@chrisjtang](https://github.com/chrisjtang) + [Christian Padilla](https://linkedin.com/in/ChristianEdwardPadilla) [@ChristianEdwardPadilla](https://github.com/ChristianEdwardPadilla) +[Crystal Lim] (https://linkedin.com/in/crystallim) [@crlim](https://github.com/crlim) + [Danial Reilley](https://linkedin.com/in/daniel-reilley) [@dreille](https://github.com/dreille) @@ -156,6 +167,8 @@ Download for [MacOS](https://github.com/team-reactype/ReacType/releases), [Windo [Philip Hua](https://www.linkedin.com/in/philip-minh-hua) [@pmhua](https://github.com/pmhua) +[Ron Fu] (https://www.linkedin.com/in/ronfu)[@rfvisuals] (https://github.com/rfvisuals) + [Sean Sadykoff](https://www.linkedin.com/in/sean-sadykoff/) [@sean1292](https://github.com/sean1292) [Shana Hoehn](https://www.linkedin.com/in/shana-hoehn-70297b169/) [@slhoehn](https://github.com/slhoehn) @@ -172,6 +185,8 @@ Download for [MacOS](https://github.com/team-reactype/ReacType/releases), [Windo [Tyler Sullberg](https://www.linkedin.com/in/tyler-sullberg) [@tsully](https://github.com/tsully) +[William Cheng] (https://www.linkedin.com/in/william-cheng-0723/) [@williamcheng12345](https://github.com/WilliamCheng12345) + [William Rittwage](https://www.linkedin.com/in/william-rittwage) [@wbrittwage](https://github.com/wbrittwage) diff --git a/app/electron/main.js b/app/electron/main.js index 88cf5e2b4..3dfe330b4 100644 --- a/app/electron/main.js +++ b/app/electron/main.js @@ -73,7 +73,7 @@ async function createWindow() { webPreferences: { zoomFactor: 0.7, // enable devtools when in development mode - devTools: isDev, + devTools: true, // crucial security feature - blocks rendering process from having access to node modules nodeIntegration: false, // web workers will not have access to node diff --git a/app/src/components/bottom/UseStateModal.tsx b/app/src/components/bottom/UseStateModal.tsx new file mode 100644 index 000000000..bbb367c0e --- /dev/null +++ b/app/src/components/bottom/UseStateModal.tsx @@ -0,0 +1,87 @@ +import React, {useRef, useState, useContext, useEffect } from 'react'; +import Modal from '@material-ui/core/Modal'; +import StateContext from '../../context/context'; +import TableStateProps from '../right/TableStateProps'; + +function UseStateModal({ updateAttributeWithState, attributeToChange, childId }) { + const [state, dispatch] = useContext(StateContext); + const [open, setOpen] = useState(false); + const [displayObject, setDisplayObject] = useState(null) + const [stateKey, setStateKey] = useState(''); + const [statePropsId, setStatePropsId] = useState(-1); + const [componentProviderId, setComponentProviderId] = useState(1); + + // make tabs to choose which component to get state from + const componentTabs = []; + for (let i = 0; i < state.components.length; i ++) { + componentTabs.push() + } + + // table to choose state from + const body = ( +
+
+ Choose State Source + +
+
+
+ {componentTabs} +
+
+ { + // if object or array => show sub table + if (table.row.type === "object") { + if (statePropsId < 0) setStatePropsId(table.row.id); + setStateKey(stateKey + table.row.key + '.'); + setDisplayObject(table.row.value); + } else if (table.row.type === "array") { + if (statePropsId < 0) setStatePropsId(table.row.id); + setStateKey(stateKey + table.row.key) + setDisplayObject(table.row.value); + } else { + // if not object or array => update state + setDisplayObject(null); + updateAttributeWithState(attributeToChange, componentProviderId, statePropsId > 0 ? statePropsId : table.row.id, table.row, stateKey + table.row.key); + setStateKey('') + setStatePropsId(-1); + setOpen(false); + } + }} + deleteHandler={() => func()} + isThemeLight={true} + /> +
+
+
+ ); + + return ( +
+ + {body} +
+ ); +} + +export default UseStateModal; diff --git a/app/src/components/left/DragDropPanel.tsx b/app/src/components/left/DragDropPanel.tsx index ca8b1a446..37205458e 100644 --- a/app/src/components/left/DragDropPanel.tsx +++ b/app/src/components/left/DragDropPanel.tsx @@ -14,7 +14,7 @@ Central state contains all available HTML elements (stored in the HTMLTypes prop initialState.tsx. Hook state: - -tag: + -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 => { @@ -37,25 +37,46 @@ const DragDropPanel = (props): JSX.Element => { payload: id }); }; - + // filter out separator so that it will not appear on the html panel - const htmlTypesToRender = state.HTMLTypes.filter(type => type.name !== 'separator'); + const htmlTypesToRender = state.HTMLTypes.filter(type => type.name !== 'separator' && type.name !== 'Route'); return (
- {htmlTypesToRender.map(option => ( - - ))} +

HTML ELEMENTS

+ {htmlTypesToRender.map(option => { + if(option.id !== 17 && option.id !== 18) { + return ( + + ); + } + + })} +

REACT ROUTER

+ {htmlTypesToRender.map(option => { + if(option.id === 17 || option.id === 18) { + return ( + + ); + } + })}
diff --git a/app/src/components/left/HTMLItem.tsx b/app/src/components/left/HTMLItem.tsx index 6dcd8a8d5..c28b85adf 100644 --- a/app/src/components/left/HTMLItem.tsx +++ b/app/src/components/left/HTMLItem.tsx @@ -35,7 +35,7 @@ const useStyles = makeStyles({ darkThemeFontColor: { color: '#fff' } - + }); const HTMLItem : React.FC<{ @@ -45,9 +45,9 @@ const HTMLItem : React.FC<{ handleDelete: (id: number) => void; isThemeLight: boolean; }> = ({ name, id, Icon, handleDelete, isThemeLight }) => { - + const classes = useStyles(); - + const [modal, setModal] = useState(null); const [{ isDragging }, drag] = useDrag({ item: { @@ -122,11 +122,11 @@ const HTMLItem : React.FC<{ // updated the id's to reflect the new element types input and label return ( // HTML Elements - { id <= 16 && + { id <= 18 &&

{name}

} - {id > 16 && + {id > 18 &&

{name}

diff --git a/app/src/components/main/AddRoute.tsx b/app/src/components/main/AddRoute.tsx new file mode 100644 index 000000000..16ea9ea19 --- /dev/null +++ b/app/src/components/main/AddRoute.tsx @@ -0,0 +1,31 @@ +import { AddRoutes } from '../../interfaces/Interfaces' +import React, { + useRef, useState, useContext, useEffect, +} from 'react'; +import StateContext from '../../context/context'; + +function AddRoute({ + id, + name +}: AddRoutes) { + const [state, dispatch] = useContext(StateContext); + + const handleClick = (id) => { + dispatch({ + type: 'ADD CHILD', + payload: { + type: 'HTML Element', + typeId: -1, + childId: id // this is the id of the parent to attach it to + } + }); + } + + return ( +
+ +
+ ); +} + +export default AddRoute; diff --git a/app/src/components/main/Annotation.tsx b/app/src/components/main/Annotation.tsx index c5404a9c3..66949adde 100644 --- a/app/src/components/main/Annotation.tsx +++ b/app/src/components/main/Annotation.tsx @@ -36,7 +36,7 @@ function Annotation({ }; /** - * Handles when text exists in the textarea of the modal. + * Handles when text exists in the textarea of the modal. * If text exists/does not exist, corresponding button changes colors. * Sets hook value to what is contained in the textarea */ @@ -53,7 +53,7 @@ function Annotation({ } /** - * This handler will find the specific anno for the corresponding component on the canvas in the childrenArray - + * This handler will find the specific anno for the corresponding component on the canvas in the childrenArray - * where the canvas components are placed */ const handleFindAnno = (array, id) => { @@ -65,7 +65,7 @@ function Annotation({ } else if (currentElement.children.length > 0) { // temp is to prevent a return of empty string since canvas element should always exist and allows the // recursion to continue - const temp = handleFindAnno(currentElement.children, id) + const temp = handleFindAnno(currentElement.children, id) if (temp != '') { return temp; } @@ -84,7 +84,7 @@ function Annotation({ } handleAnnoChange(event); }, []) - + const body = (
Notes for: {name} ( {id} ) diff --git a/app/src/components/main/Canvas.tsx b/app/src/components/main/Canvas.tsx index 13d9b12c8..844e34df0 100644 --- a/app/src/components/main/Canvas.tsx +++ b/app/src/components/main/Canvas.tsx @@ -16,7 +16,7 @@ function Canvas() { const currentComponent: Component = state.components.find( (elem: Component) => elem.id === state.canvasFocus.componentId ); - + // changes focus of the canvas to a new component / child const changeFocus = (componentId?: number, childId?: number | null) => { dispatch({ type: 'CHANGE FOCUS', payload: { componentId, childId } }); @@ -24,10 +24,10 @@ function Canvas() { // onClickHandler is responsible for changing the focused component and child component function onClickHandler(event) { event.stopPropagation(); - // note: a null value for the child id means that we are focusing on the top-level component rather than any child + // note: a null value for the child id means that we are focusing on the top-level component rather than any child changeFocus(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 diff --git a/app/src/components/main/DemoRender.tsx b/app/src/components/main/DemoRender.tsx index 732b4cf65..9eacfdbc7 100644 --- a/app/src/components/main/DemoRender.tsx +++ b/app/src/components/main/DemoRender.tsx @@ -2,6 +2,7 @@ import React, { useState, useCallback, useContext, useEffect, } from 'react'; +import { BrowserRouter as Router, Switch, Route, Link } from "react-router-dom"; import Button from '@material-ui/core/Button'; import Box from '@material-ui/core/Box'; import StateContext from '../../context/context'; @@ -34,9 +35,13 @@ const DemoRender = (props): JSX.Element => { if (elementType !== 'input' && elementType !== 'img' && element.children.length > 0) { renderedChildren = componentBuilder(element.children); } + if (elementType === 'input') componentsToRender.push(); else if (elementType === 'img') componentsToRender.push(); else if (elementType === 'a') componentsToRender.push({innerText}); + else if (elementType === 'Switch') componentsToRender.push({renderedChildren}); + else if (elementType === 'Route') componentsToRender.push({renderedChildren}); + else if (elementType === 'LinkTo') componentsToRender.push({innerText}); else componentsToRender.push({innerText}{renderedChildren}); key += 1; } @@ -57,9 +62,12 @@ const DemoRender = (props): JSX.Element => { return ( -
- {components.map((component, index) => component)} -
+ +
+ {components.map((component, index) => component)} +
+
+ ); }; diff --git a/app/src/components/main/DirectChildComponent.tsx b/app/src/components/main/DirectChildComponent.tsx index a3fc0833e..7d3c9aef4 100644 --- a/app/src/components/main/DirectChildComponent.tsx +++ b/app/src/components/main/DirectChildComponent.tsx @@ -9,9 +9,8 @@ import { ItemTypes } from '../../constants/ItemTypes'; import StateContext from '../../context/context'; import { combineStyles } from '../../helperFunctions/combineStyles'; import globalDefaultStyle from '../../public/styles/globalDefaultStyles'; -import renderChildren from '../../helperFunctions/renderChildren' -function DirectChildComponent({ childId, type, typeId, style }: ChildElement) { +function DirectChildComponent({ childId, type, typeId, style, name }: ChildElement) { const [state, dispatch] = useContext(StateContext); // find the top-level component corresponding to this instance of the component @@ -64,13 +63,17 @@ function DirectChildComponent({ childId, type, typeId, style }: ChildElement) { interactiveStyle ); - return ( + + // Renders name and not children of subcomponents to clean up Canvas view when dragging components + // into the main canvas. To render html elements on canvas, import and invoke renderChildren + return (
- {renderChildren(referencedComponent.children)} + {name} + {` (${childId})`}
); } diff --git a/app/src/components/main/DirectChildHTMLNestable.tsx b/app/src/components/main/DirectChildHTMLNestable.tsx index e3a0edf8b..edba86d0f 100644 --- a/app/src/components/main/DirectChildHTMLNestable.tsx +++ b/app/src/components/main/DirectChildHTMLNestable.tsx @@ -9,6 +9,8 @@ import renderChildren from '../../helperFunctions/renderChildren'; import Annotation from './Annotation' import validateNewParent from '../../helperFunctions/changePositionValidation' import componentNest from '../../helperFunctions/componentNestValidation' +// import addRoute from '../../helperFunctions/addRoute'; +import AddRoute from './AddRoute'; function DirectChildHTMLNestable({ childId, @@ -18,6 +20,7 @@ function DirectChildHTMLNestable({ children, name, annotations, + attributes }: ChildElement) { const [state, dispatch] = useContext(StateContext); const ref = useRef(null); @@ -79,7 +82,7 @@ const snapShotFunc = () => { childId: childId, } }); - } + } } // if item is not a new instance, change position of element dragged inside div so that the div is the new parent else { @@ -95,7 +98,7 @@ const snapShotFunc = () => { } } }, - + collect: (monitor: any) => { return { isOver: !!monitor.isOver({ shallow: true }) @@ -132,10 +135,17 @@ const snapShotFunc = () => { drag(drop(ref)); + const routeButton = []; + if (typeId === 17) { + routeButton.push(); + } + return (
{HTMLType.placeHolderShort} {` ( ${childId} )`} + {attributes && attributes.compLink ? ` ${attributes.compLink}` : ''} + {routeButton} { + const [state, dispatch] = useContext(StateContext); const classes = useStyles(); - const [state] = useContext(StateContext); + const [inputKey, setInputKey] = useState(""); const [inputValue, setInputValue] = useState(""); const [inputType, setInputType] = useState(""); - - const [stateProps, setStateProps] = useState([]); - + const [errorStatus, setErrorStatus] = useState(false); + const [errorMsg, setErrorMsg] = useState(''); // get currentComponent by using currently focused component's id const currentId = state.canvasFocus.componentId; const currentComponent = state.components[currentId - 1]; @@ -45,12 +45,16 @@ const StatePropsPanel = ({ isThemeLight }): JSX.Element => { // convert value to correct type based on user input const typeConversion = (value, type) => { switch (type) { - case "String": + case "string": return String(value); - case "Number": + case "number": return Number(value); - case "Boolean": + case "boolean": return Boolean(value); + case "array": + return JSON.parse(value); + case "object": + return JSON.parse(value); default: return value; } @@ -62,47 +66,49 @@ const StatePropsPanel = ({ isThemeLight }): JSX.Element => { setInputValue(""); setInputType(""); }; + //reset error warning + const resetError = () => { + setErrorStatus(false); + }; // submit new stateProps entries to state context + let currKey; const submitNewState = (e) => { e.preventDefault(); const statesArray = currentComponent.stateProps; + //loop though array, access each obj at key property + let keyToInt = parseInt(inputKey[0]); + if(!isNaN(keyToInt)) { + setErrorStatus(true); + setErrorMsg('Key name can not start with int.'); + return; + } + + //return alert('key can not start with number'); const newState = { // check if array is not empty => true find last elem in array. get id and increment by 1 || else 1 id: statesArray.length > 0 ? statesArray[statesArray.length-1].id + 1 : 1, key: inputKey, value: typeConversion(inputValue, inputType), type: inputType, - }; - // store this newStateProp obj to our Component's stateProps array - currentComponent.stateProps.push(newState); - // reset newStateProp to empty for future new state prop entries - updateUseStateCodes(); + }; + + dispatch({ + type: 'ADD STATE', + payload: {newState: newState} + }); + resetError(); clearForm(); }; - - // generates React Hook code snippets for each new stateProp entry - const updateUseStateCodes = () => { - // array of snippets of state prop codes - const localStateCode = []; - currentComponent.stateProps.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); - }); - // store localStateCodes in global state context - currentComponent.useStateCodes = localStateCode; - }; - - // find table row using its id and if it exists, populate form with its details + + // find table row using its id and if it exists, populate form with its details const handlerRowSelect = (table) => { let exists = false; currentComponent.stateProps.forEach((stateProp) => { // if stateProp id matches current row's id (table.row.id), flip exists to true if (stateProp.id === table.row.id) exists = true; - }); + }); // if row id exists, populate form with corresponding inputs (key, value, type) from table row if (exists) { setInputKey(table.row.key); @@ -111,14 +117,6 @@ const StatePropsPanel = ({ isThemeLight }): JSX.Element => { } else clearForm(); }; - // find & delete table row using its id - const handlerRowDelete = (id:any) => { - // iterate and filter out stateProps with matching row id - currentComponent.stateProps = currentComponent.stateProps.filter(element => element.id !== id); - updateUseStateCodes(); - setStateProps(currentComponent.stateProps.slice()); - }; - return (
@@ -129,8 +127,9 @@ const StatePropsPanel = ({ isThemeLight }): JSX.Element => { label="key:" variant="outlined" value={inputKey} + error={errorStatus} onChange={(e) => setInputKey(e.target.value)} - className={isThemeLight ? `${classes.rootLight} ${classes.inputTextLight}` : `${classes.rootDark} ${classes.inputTextDark}`} + className={isThemeLight ? `${classes.rootLight} ${classes.inputTextLight}` : `${classes.rootDark} ${classes.inputTextDark}`} /> { variant="outlined" value={inputValue} onChange={(e) => setInputValue(e.target.value)} - className={isThemeLight ? `${classes.rootLight} ${classes.inputTextLight}` : `${classes.rootDark} ${classes.inputTextDark}`} + className={isThemeLight ? `${classes.rootLight} ${classes.inputTextLight}` : `${classes.rootDark} ${classes.inputTextDark}`} /> - Type @@ -168,6 +167,9 @@ const StatePropsPanel = ({ isThemeLight }): JSX.Element => { Array + + Object + Undefined @@ -181,8 +183,8 @@ const StatePropsPanel = ({ isThemeLight }): JSX.Element => {

- @@ -196,7 +198,7 @@ const StatePropsPanel = ({ isThemeLight }): JSX.Element => {

Current State Name: {state.components[state.canvasFocus.componentId - 1].name}

- +
); diff --git a/app/src/components/right/TableStateProps.tsx b/app/src/components/right/TableStateProps.tsx index 746a84bd3..5d7003d74 100644 --- a/app/src/components/right/TableStateProps.tsx +++ b/app/src/components/right/TableStateProps.tsx @@ -8,83 +8,119 @@ import Button from '@material-ui/core/Button'; import ClearIcon from '@material-ui/icons/Clear'; import StateContext from '../../context/context'; import { makeStyles } from '@material-ui/core/styles'; - import { StatePropsPanelProps } from '../../interfaces/Interfaces'; -const getColumns = (props) => { - const { deleteHandler } : StatePropsPanelProps = props; - return [ +const TableStateProps = props => { + const [state, dispatch] = useContext(StateContext); + const classes = useStyles(); + const [editRowsModel] = useState({}); + const [gridColumns, setGridColumns] = useState([]); + + // const [rows, setRows] = useState([]); + + const columnTabs = [ { field: 'id', headerName: 'ID', width: 70, - editable: false, + editable: false }, { field: 'key', headerName: 'Key', width: 90, - editable: true, + editable: true }, { field: 'value', headerName: 'Value', width: 90, - editable: true, + editable: true }, { field: 'type', headerName: 'Type', width: 90, - editable: false, + editable: false }, { field: 'delete', headerName: 'X', width: 70, editable: false, - renderCell: function renderCell(params:any) { - const getIdRow = () => { - const { api } = params; - const fields = api.getAllColumns().map((c: any) => c.field).filter((c : any) => c !== '__check__' && !!c); - return params.getValue(fields[0]); - }; - return ( - ); - }, - }, + } + } ]; -}; -const TableStateProps = (props) => { - const classes = useStyles(); - const [state] = useContext(StateContext); - const [editRowsModel] = useState ({}); - const [gridColumns, setGridColumns] = useState([]); - + const deleteState = selectedId => { + // get the current focused component + // remove the state that the button is clicked + // send a dispatch to rerender the table + const currentId = state.canvasFocus.componentId; + const currentComponent = state.components[currentId - 1]; + + const filtered = currentComponent.stateProps.filter( + element => element.id !== selectedId + ); + dispatch({ + type: 'DELETE STATE', + payload: { stateProps: filtered, rowId: selectedId } + }); + }; useEffect(() => { - setGridColumns(getColumns(props)); - }, [props.isThemeLight]) - // get currentComponent by using currently focused component's id - const currentId = state.canvasFocus.componentId; - const currentComponent = state.components[currentId - 1]; - - const rows = currentComponent.stateProps.slice(); + setGridColumns(columnTabs); + }, [props.isThemeLight]); + + const { selectHandler }: StatePropsPanelProps = props; - const { selectHandler } : StatePropsPanelProps = props; - - // when component gets mounted, sets the gridColumn + // the delete button needs to be updated to remove + // the states from the current focused component useEffect(() => { - setGridColumns(getColumns(props)); - }, []); - + if (props.canDeleteState) { + setGridColumns(columnTabs); + } else { + setGridColumns(columnTabs.slice(0, gridColumns.length - 1)); + } + }, [state.canvasFocus.componentId]); + + + // rows to show are either from current component or from a given provider + let rows = []; + if (!props.providerId) { + const currentId = state.canvasFocus.componentId; + const currentComponent = state.components[currentId - 1]; + rows = currentComponent.stateProps.slice(); + } else { + const providerComponent = state.components[props.providerId - 1]; + // changed to get whole object + if (props.displayObject){ + const displayObject = props.displayObject; + // format for DataGrid + let id=1; + for (const key in displayObject) { + // if key is a number make it a string with brackets aroung number + const newKey = isNaN(key) ? key : '[' + key + ']'; + const type = Array.isArray(displayObject[key]) ? 'array' : typeof (displayObject[key]); + rows.push({ id: id++, key: newKey, value: displayObject[key], type: type}); + } + } else { + rows = providerComponent.stateProps.slice(); + } + } + + return (
{ columns={gridColumns} pageSize={5} editRowsModel={editRowsModel} - onRowClick = {selectHandler} + onRowClick={selectHandler} className={props.isThemeLight ? classes.themeLight : classes.themeDark} />
); }; - const useStyles = makeStyles({ themeLight: { color: 'rgba(0,0,0,0.54)', '& .MuiTablePagination-root': { color: 'rbga(0,0,0,0.54)' - }, + } }, themeDark: { color: 'white', diff --git a/app/src/containers/CustomizationPanel.tsx b/app/src/containers/CustomizationPanel.tsx index eab20b9fd..e8fcc93c7 100644 --- a/app/src/containers/CustomizationPanel.tsx +++ b/app/src/containers/CustomizationPanel.tsx @@ -29,6 +29,7 @@ import ProjectManager from '../components/right/ProjectManager'; import StateContext from '../context/context'; import FormSelector from '../components/form/Selector'; import { config } from 'ace-builds'; +import UseStateModal from '../components/bottom/UseStateModal'; // 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 => { @@ -49,6 +50,9 @@ const CustomizationPanel = ({ isThemeLight }): JSX.Element => { const [deleteComponentError, setDeleteComponentError] = useState(false); const { style } = useContext(styleContext); const [modal, setModal] = useState(null); + const [useContextObj, setUseContextObj] = useState({}); + const [stateUsedObj, setStateUsedObj] = useState({}); + const resetFields = () => { const childrenArray = state.components[0].children; @@ -203,8 +207,42 @@ const CustomizationPanel = ({ isThemeLight }): JSX.Element => { return isLinked; }; - // dispatch to 'UPDATE CSS' called when save button is clicked, - // passing in style object constructed from all changed input values + + const updateAttributeWithState = (attributeName, 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; + // update/create stateUsed.compText + setStateUsedObj({...stateUsedObj, compText: stateKey, compTextProviderId: componentProviderId, compTextPropsId: statePropsId}); + setCompText(newInput); + setUseContextObj(newContextObj); + } + + if (attributeName === 'compLink') { + newContextObj[componentProviderId].compLink = statePropsId; + + // update/create stateUsed.compLink + setStateUsedObj({...stateUsedObj, compLink: stateKey, compLinkProviderId: componentProviderId, compLinkPropsId: statePropsId}); + setCompLink(newInput); + setUseContextObj(newContextObj); + } + } + const handleSave = (): Object => { const styleObj: any = {}; if (displayMode !== '') styleObj.display = displayMode; @@ -220,6 +258,16 @@ const CustomizationPanel = ({ isThemeLight }): JSX.Element => { if (compLink !== '') attributesObj.compLink = compLink; if (cssClasses !== '') attributesObj.cssClasses = cssClasses; + dispatch({ + type: 'UPDATE STATE USED', + payload: {stateUsedObj: stateUsedObj} + }) + + dispatch({ + type: 'UPDATE USE CONTEXT', + payload: { useContextObj: useContextObj} + }) + dispatch({ type: 'UPDATE CSS', payload: { style: styleObj } @@ -298,7 +346,6 @@ const CustomizationPanel = ({ isThemeLight }): JSX.Element => { key={'not delete'} button onClick={closeModal} - ƒ style={{ border: '1px solid #3f51b5', marginBottom: '2%', @@ -517,7 +564,6 @@ const CustomizationPanel = ({ isThemeLight }): JSX.Element => {
-
{ />
+
+ +
{ />
+
+ +
{ + const code = generateUnformattedCode( + components, + componentId, + rootComponents, + projectType, + HTMLTypes + ); + return formatCode(code); +}; + // generate code based on the component hierarchy const generateUnformattedCode = ( comps: Component[], @@ -23,12 +41,15 @@ const generateUnformattedCode = ( const components = [...comps]; // find the component that we're going to generate code for - const currentComponent = components.find(elem => elem.id === componentId); + 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; 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) => { @@ -47,30 +68,36 @@ const generateUnformattedCode = ( // 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; + 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 === 'div' || - referencedHTML.tag === 'separator' || + referencedHTML.tag === 'separator' || referencedHTML.tag === 'form' || referencedHTML.tag === 'ul' || referencedHTML.tag === 'ol' || referencedHTML.tag === 'menu' || - referencedHTML.tag === 'li' + referencedHTML.tag === 'li' || + referencedHTML.tag === 'LinkTo' || + 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 === 'LinkTo') + importReactRouter = true; return child; } else if (child.type === 'Route Link') { links = true; child.name = components.find( (comp: Component) => comp.id === child.typeId - ).name; - return child; - } + ).name; + return child; + } }); return enrichedChildren; }; @@ -79,62 +106,86 @@ const generateUnformattedCode = ( const formatStyles = (styleObj: any) => { if (Object.keys(styleObj).length === 0) return ``; const formattedStyles = []; + let styleString; for (let i in styleObj) { - const styleString = i + ': ' + "'" + styleObj[i] + "'"; - formattedStyles.push(styleString); + if(i === 'style') { + styleString = i + '=' + '{' + JSON.stringify(styleObj[i]) + '}'; + formattedStyles.push(styleString); + } } - return ' style={{' + formattedStyles.join(',') + '}}'; + return formattedStyles; }; // function to dynamically add classes, ids, and styles to an element if it exists. const elementTagDetails = (childElement: object) => { let customizationDetails = ""; - if (childElement.childId) customizationDetails += (' ' + `id="${+childElement.childId}"`); - if (childElement.attributes && childElement.attributes.cssClasses) customizationDetails += (' ' + `className="${childElement.attributes.cssClasses}"`); + if (childElement.childId && childElement.tag !== 'Route') customizationDetails += (' ' + `id="${+childElement.childId}"`); + if (childElement.attributes && childElement.attributes.cssClasses) { + customizationDetails += (' ' + `className="${childElement.attributes.cssClasses}"`); + } if (childElement.style && Object.keys(childElement.style).length > 0) customizationDetails +=(' ' + formatStyles(childElement)); 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 = '' + 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: object, level: number = 2) => { let innerText = ''; - let activeLink = ''; - if (childElement.attributes && childElement.attributes.compText) innerText = childElement.attributes.compText; - if (childElement.attributes && childElement.attributes.compLink) activeLink = childElement.attributes.compLink; + let activeLink = '""'; - const nestable = childElement.tag === 'div' || - childElement.tag === 'form' || - childElement.tag === 'ol' || + 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 === 'div' || + childElement.tag === 'form' || + childElement.tag === 'ol' || childElement.tag === 'ul' || childElement.tag === 'menu' || - childElement.tag === 'li'; + 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))}`; + 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))}`; + 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 === 'LinkTo') { + return `${levelSpacer(level, 5)}${innerText} + ${tabSpacer(level)}${writeNestedElements(childElement.children, level + 1)} + ${tabSpacer(level - 1)}${levelSpacer(2, (3 + level))}`; } else if (nestable) { - return `${levelSpacer(level, 5)}<${childElement.tag}${elementTagDetails(childElement)}>${innerText} + const routePath = (childElement.tag === 'Route') ? (' ' + 'exact path=' + activeLink) : ''; + return `${levelSpacer(level, 5)}<${childElement.tag}${elementTagDetails(childElement)}${routePath}>${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 @@ -162,19 +213,17 @@ const generateUnformattedCode = ( .join('') }`; }; - + // function to properly incorporate the user created state that is stored in the application state const writeStateProps = (stateArray: any) => { let stateToRender = ''; for (const element of stateArray) { - stateToRender += levelSpacer(2, 3) + element + ';' + stateToRender += levelSpacer(2, 2) + element + ';' } return stateToRender } - const enrichedChildren: any = getEnrichedChildren(currentComponent); - - const next = true; + const enrichedChildren: any = getEnrichedChildren(currComponent); // import statements differ between root (pages) and regular components (components) const importsMapped = @@ -192,47 +241,84 @@ const generateUnformattedCode = ( }) .join('\n'); - const stateful = true; - const classBased = false; + const createState = (stateProps) => { + let state = '{'; + + stateProps.forEach((ele) => { + state += ele.key + ':' + JSON.stringify(ele.value) + ', '; + }); + + state = state.substring(0, state.length - 2) + '}'; + + return state; + } + + // Generate import + let importContext = ''; + if(currComponent.useContext) { + for (const providerId of Object.keys(currComponent.useContext)) { + const providerComponent = components[parseInt(providerId) - 1]; + importContext += `import ${providerComponent.name}Context from './${providerComponent.name}.tsx'\n \t\t` ; + } + } + + if (currComponent.useContext) { + for (const providerId of Object.keys(currComponent.useContext)) { + const statesFromProvider = currComponent.useContext[parseInt(providerId)].statesFromProvider; //{1: {Set, compLink, compText}, 2 : {}...} + const providerComponent = components[parseInt(providerId) - 1]; + providers += 'const ' + providerComponent.name.toLowerCase() + 'Context = useContext(' + providerComponent.name + 'Context);\n \t\t' ; + + for (let i = 0; i < providerComponent.stateProps.length; i++) { + if(statesFromProvider.has(providerComponent.stateProps[i].id)) { + context += + 'const ' + + providerComponent.stateProps[i].key + + ' = ' + + providerComponent.name.toLowerCase() + + 'Context.' + + providerComponent.stateProps[i].key + + '; \n \t\t'; + } + } + } + } // create final component code. component code differs between classic react, next.js, gatsby.js // classic react code if (projectType === 'Classic React') { return ` - ${stateful && !classBased ? `import React, {useState} from 'react';` : ''} - ${classBased ? `import React, {Component} from 'react';` : ''} - ${!stateful && !classBased ? `import React from 'react';` : ''} + ${`import React, { useState, createContext, useContext } from 'react';`} + ${importReactRouter ? `import { BrowserRouter as Router, Route, Switch, Link } from 'react-router-dom';`: ``} ${importsMapped} + ${importContext} + ${providers} + ${context} + ${`const ${currComponent.name} = (props: any): JSX.Element => {`} + ${` const [value, setValue] = useState("INITIAL VALUE");${writeStateProps(currComponent.useStateCodes)}`} ${ - classBased - ? `class ${currentComponent.name} extends Component {` - : `const ${currentComponent.name} = (props: any): JSX.Element => {` + isRoot && currComponent.stateProps.length !== 0 + ? ` const ${currComponent.name}Context = createContext(${createState(currComponent.stateProps)});` + : `` } - - ${ - stateful && !classBased - ? `const [value, setValue] = useState("INITIAL VALUE")${writeStateProps(currentComponent.useStateCodes)}; - ` - : `` - } - ${ - classBased && stateful - ? `constructor(props) { - super(props); - this.state = {} - }` - : `` - } - ${classBased ? `render(): JSX.Element {` : ``} - - return ( -
- ${writeNestedElements(enrichedChildren)} -
- ); - } - ${classBased ? `}` : ``} - export default ${currentComponent.name}; + ${!importReactRouter + ? ` return ( + <${currComponent.name}Context.Provider value=""> +
+ \t${writeNestedElements(enrichedChildren)} +
+ + );` + : ` return ( + <${currComponent.name}Context.Provider value=""> + +
+ \t${writeNestedElements(enrichedChildren)} +
+
+ + );`} + ${`}\n`} + export default ${currComponent.name}; `; } // next.js component code @@ -243,7 +329,7 @@ const generateUnformattedCode = ( import Head from 'next/head' ${links ? `import Link from 'next/link'` : ``} - const ${currentComponent.name} = (props): JSX.Element => { + const ${currComponent.name} = (props): JSX.Element => { const [value, setValue] = useState("INITIAL VALUE"); @@ -252,18 +338,18 @@ const generateUnformattedCode = ( ${ isRoot ? ` - ${currentComponent.name} + ${currComponent.name} ` : `` } -
+
${writeNestedElements(enrichedChildren)}
); } - export default ${currentComponent.name}; + export default ${currComponent.name}; `; } else { // gatsby component code @@ -272,29 +358,29 @@ const generateUnformattedCode = ( ${importsMapped} import { StaticQuery, graphql } from 'gatsby'; ${links ? `import { Link } from 'gatsby'` : ``} - - const ${currentComponent.name} = (props: any): JSX.Element => { - const [value, setValue] = useState("INITIAL VALUE"); + const ${currComponent.name} = (props: any): JSX.Element => { + + const[value, setValue] = useState("INITIAL VALUE"); return ( <> ${ isRoot ? ` - ${currentComponent.name} + ${currComponent.name} ` : `` } -
+
${writeNestedElements(enrichedChildren)}
); } - export default ${currentComponent.name}; + export default ${currComponent.name}; `; } }; @@ -319,22 +405,6 @@ const formatCode = (code: string) => { } }; -// generate code based on component hierarchy and then return the rendered code -const generateCode = ( - components: Component[], - componentId: number, - rootComponents: number[], - projectType: string, - HTMLTypes: HTMLType[] -) => { - const code = generateUnformattedCode( - components, - componentId, - rootComponents, - projectType, - HTMLTypes - ); - return formatCode(code); -}; + export default generateCode; diff --git a/app/src/helperFunctions/localStorage.ts b/app/src/helperFunctions/localStorage.ts index aad436be6..7cae7f0c3 100644 --- a/app/src/helperFunctions/localStorage.ts +++ b/app/src/helperFunctions/localStorage.ts @@ -6,5 +6,5 @@ // }; // export const loadState = () => // localforage.getItem('state-v1.0.1').then(value => { -// // console.log('The value stored in local storage: ', value); +// // ('The value stored in local storage: ', value); // }); diff --git a/app/src/helperFunctions/renderChildren.tsx b/app/src/helperFunctions/renderChildren.tsx index bc76404f5..bb8b7154b 100644 --- a/app/src/helperFunctions/renderChildren.tsx +++ b/app/src/helperFunctions/renderChildren.tsx @@ -18,7 +18,7 @@ const renderChildren = (children: ChildElement[]) => { 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. + // Removed style from prop drills so that styling isn't applied to canvas items. // Also added keys & removed an unnecessary div around DirChildNestables that were causing errors. if (type === 'Component') { return ( @@ -34,7 +34,7 @@ const renderChildren = (children: ChildElement[]) => { } // ommitted orderedlists, unorderedlists, and menus, ommitted li items as non-nestable types because they can be nested within. // child is a non-nestable type of HTML element (everything except for divs and forms) - else if (type === 'HTML Element' && typeId !== 11 && typeId !== 1000 && typeId !== 2 && typeId !== 3 && typeId !== 14 && typeId !== 15 && typeId !== 16) { + else if (type === 'HTML Element' && typeId !== 11 && typeId !== 1000 && typeId !== 2 && typeId !== 3 && typeId !== 14 && typeId !== 15 && typeId !== 16 && typeId !== 17 && typeId !== 18 && typeId !== -1) { return ( { } // Added Orderedlists, Unorderedlists, and Menus, changed lists to nestable because they are nestable. // child is a nestable type of HTML element (divs and forms) - else if (type === 'HTML Element' && (typeId === 11 || typeId === 2 || typeId === 3 || typeId === 14 || typeId === 15 || typeId === 16)) { - return ( + else if (type === 'HTML Element' && (typeId === 11 || typeId === 2 || typeId === 3 || typeId === 14 || typeId === 15 || typeId === 16 || typeId === 17 || typeId === 18 || typeId === -1)) { + return ( { key={'DirChildHTMLNest' + childId.toString() + name} name={name} annotations={annotations} + attributes={attributes} /> ); } diff --git a/app/src/interfaces/Interfaces.ts b/app/src/interfaces/Interfaces.ts index 942e41ac9..57971823d 100644 --- a/app/src/interfaces/Interfaces.ts +++ b/app/src/interfaces/Interfaces.ts @@ -25,6 +25,7 @@ export interface ChildElement { attributes?: object; children?: ChildElement[]; annotations?: string; + stateUsed?: object; } export interface Component { id: number; @@ -36,9 +37,13 @@ export interface Component { isPage: boolean; past: any[]; future: any[]; - stateProps: StateProp[]; // state: [ { key: value, type }, {key: value, type}, {key: value, type} ] + stateProps: StateProp[]; // state: [ { id, key, value, type }, ...] annotations?: string; useStateCodes: string[]; + useContext?: object // structure --> {providerId: {attribute: stateId, ....}, ...} + // example: + // {1: {compText: 1, compLink: 2}} + // {1: {compText: 1}, 2: {compLink: 1}, ....} } export interface StateProp { @@ -99,3 +104,8 @@ export interface StatePropsPanelProps { selectHandler: (table: any) => void; deleteHandler: (id: number | any) => void; } + +export interface AddRoutes { + id: number; + name: string; +} diff --git a/app/src/public/styles/style.css b/app/src/public/styles/style.css index 78b0fee83..de9b82e47 100644 --- a/app/src/public/styles/style.css +++ b/app/src/public/styles/style.css @@ -608,3 +608,62 @@ a.nav_link:hover { right: 30px ; z-index: 999; } + +.useState-btn { + color: rgb(241, 240, 240); + background-color: #0099E6; + font-family: Arial, Helvetica, sans-serif; +} + +.useState-btn { + color: rgb(241, 240, 240); + background-color: #0099E6; + border: 1px solid #186BB4; + border-radius: 3; + box-shadow: "0 0px 0px 2px #1a1a1a"; + padding: 2px 2px 2px 2px; +} + +/* UseStateModal Styling */ + +.useState-position { + display: flex; + flex-direction: column; + position: fixed; + align-items: center; + top: 30%; + left: 30%; +} + +.useState-header { + font-size: 35px; + background-color: #003366; + color: rgb(241, 240, 240); + width: 600px; + border: 3px; + border-style: solid; + border-color: black; + 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; +} \ No newline at end of file diff --git a/app/src/reducers/componentReducer.ts b/app/src/reducers/componentReducer.ts index 9cc07d5f2..f440628a6 100644 --- a/app/src/reducers/componentReducer.ts +++ b/app/src/reducers/componentReducer.ts @@ -8,6 +8,8 @@ import { import initialState from '../context/initialState'; import generateCode from '../helperFunctions/generateCode'; import manageSeparators from '../helperFunctions/manageSeparators'; +import addRoute from '../helperFunctions/addRoute'; +import cloneDeep from '../helperFunctions/cloneDeep'; let separator = initialState.HTMLTypes[1]; @@ -195,6 +197,21 @@ const reducer = (state: State, action: Action) => { arrayOfElements[i] = initialState.HTMLTypes[i]; } }; + // () ? returnthis : elsereturnthis + // '' + dfsldkjfslkf + const updateUseStateCodes = (currentComponent) => { + // array of snippets of state prop codes + const localStateCode = []; + + currentComponent.stateProps.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); + }); + // store localStateCodes in global state context + return localStateCode; + }; switch (action.type) { case 'ADD COMPONENT': { @@ -308,6 +325,7 @@ const reducer = (state: State, action: Action) => { children: [] }; + // 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 @@ -434,6 +452,53 @@ const reducer = (state: State, action: Action) => { return { ...state }; } + case 'UPDATE STATE USED': { + + const { stateUsedObj } = action.payload; + + const components = [...state.components]; + + const component = findComponent( + components, + state.canvasFocus.componentId + ); + const targetChild = findChild(component, state.canvasFocus.childId); + targetChild.stateUsed = stateUsedObj; + + component.code = generateCode( + components, + state.canvasFocus.componentId, + [...state.rootComponents], + state.projectType, + state.HTMLTypes + ); + + return { ...state, components }; + } + + case 'UPDATE USE CONTEXT': { + const { useContextObj } = action.payload; + + const components = [...state.components]; + const component = findComponent( + components, + state.canvasFocus.componentId + ); + component.useContext = useContextObj; + + component.code = generateCode( + components, + state.canvasFocus.componentId, + [...state.rootComponents], + state.projectType, + state.HTMLTypes + ); + + + return {...state, components } + + } + case 'UPDATE CSS': { const { style } = action.payload; const components = [...state.components]; @@ -455,6 +520,7 @@ const reducer = (state: State, action: Action) => { return { ...state, components }; } + case 'UPDATE ATTRIBUTES': { const { attributes } = action.payload; const components = [...state.components]; @@ -533,6 +599,29 @@ const reducer = (state: State, action: Action) => { // 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( @@ -730,9 +819,91 @@ const reducer = (state: State, action: Action) => { ...state }; } + case 'ADD STATE' : { + // if (!state.canvasFocus.childId) return state; + // find the current component in focus + const components = [...state.components]; + const currComponent = findComponent( + components, + state.canvasFocus.componentId + ); + + currComponent.stateProps.push(action.payload.newState); + currComponent.useStateCodes = updateUseStateCodes(currComponent); + + currComponent.code = generateCode( + components, + state.canvasFocus.componentId, + [...state.rootComponents], + state.projectType, + state.HTMLTypes + ); + return { ...state, components}; + } + + case 'DELETE STATE' : { + const components = [...state.components]; + let currComponent = findComponent( + components, + state.canvasFocus.componentId + ); + + currComponent.stateProps = action.payload.stateProps; + currComponent.useStateCodes = updateUseStateCodes(currComponent); + + components.forEach((component) => { + // 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; + } + } + } + + component.code = generateCode( + components, + component.id, + [...state.rootComponents], + state.projectType, + state.HTMLTypes + ); + } + }); + + currComponent.code = generateCode( + components, + state.canvasFocus.componentId, + [...state.rootComponents], + state.projectType, + state.HTMLTypes + ); + return { ...state, components}; + } + default: return state; } + + }; export default reducer; diff --git a/app/src/tree/TreeChart.tsx b/app/src/tree/TreeChart.tsx index 64df30dd1..449f0ff04 100644 --- a/app/src/tree/TreeChart.tsx +++ b/app/src/tree/TreeChart.tsx @@ -37,7 +37,9 @@ function TreeChart({ data }) { // data is components from state - passed in from i -= 1; } // if element has a children array and that array has length, recursive call - else if ((arr[i].name === 'div' || arr[i].name === 'form' || arr[i].type === 'Component') && arr[i].children.length) { + else if ((arr[i].name === 'div' || arr[i].name === 'form' || arr[i].type === 'Component' || arr[i].name === 'LinkTo' + || arr[i].name === 'Switch' || arr[i].name === 'Route' || arr[i].name === 'menu' + || arr[i].name === 'ul' || arr[i].name === 'ol' || arr[i].name === 'li') && arr[i].children.length) { // if element is a component, replace it with deep clone of latest version (to update with new HTML elements) if (arr[i].type === 'Component') arr[i] = cloneDeep(data.find(component => component.name === arr[i].name)); removeSeparators(arr[i].children); diff --git a/package.json b/package.json index 76684ffb2..7db908328 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "reactype", - "version": "8.0.0", + "version": "9.0.0", "description": "Prototyping tool for React/Typescript Applications.", "private": true, "main": "app/electron/main.js", @@ -15,7 +15,9 @@ "Brian Han", "Charles Finocchiaro", "Chelsey Fewer", + "Chris Tang", "Christian Padilla", + "Crystal Lim", "Daryl Foster", "Diego Vazquez", "Edward Park", @@ -32,6 +34,7 @@ "Luke Madden", "Mitchel Severe", "Natalie Vick", + "Ron Fu", "Sean Sadykoff", "Shana Hoehn", "Shlomo Porges", @@ -40,6 +43,7 @@ "Tolga Mizrakci", "Tony Ito-Cole", "Tyler Sullberg", + "William Cheng", "William Yoon" ], "scripts": { @@ -132,8 +136,8 @@ "cors": "^2.8.5", "d3": "^6.2.0", "dotenv": "^8.2.0", - "electron-debug": "^3.1.0", - "electron-devtools-installer": "^2.2.4", + "electron-debug": "^3.2.0", + "electron-devtools-installer": "^3.2.0", "electron-splashscreen": "^1.0.0", "electron-window-manager": "^1.0.6", "enzyme": "^3.4.1", @@ -165,15 +169,15 @@ "resize-observer-polyfill": "^1.5.1", "seamless-immutable": "^7.1.4", "source-map-support": "^0.5.19", - "spectron": "^11.1.0", + "spectron": "^15.0.0", "uniqid": "^5.3.0", "uuid": "^8.2.0" }, "devDependencies": { - "@babel/core": "^7.10.4", - "@babel/preset-env": "^7.10.4", - "@babel/preset-react": "^7.10.4", - "@babel/preset-typescript": "^7.10.4", + "@babel/core": "^7.16.0", + "@babel/preset-env": "^7.16.0", + "@babel/preset-react": "^7.16.0", + "@babel/preset-typescript": "^7.16.0", "@testing-library/jest-dom": "^5.11.5", "@testing-library/react": "^10.4.6", "@types/chai": "^4.2.11", @@ -181,17 +185,17 @@ "@types/enzyme-adapter-react-16": "^1.0.6", "@types/jest": "^25.2.3", "apollo": "^2.32.5", - "babel-eslint": "^8.2.6", - "babel-jest": "^25.2.4", - "babel-loader": "^8.1.0", - "babel-plugin-module-resolver": "^4.0.0", + "babel-eslint": "^10.1.0", + "babel-jest": "^27.3.1", + "babel-loader": "^8.2.3", + "babel-plugin-module-resolver": "^4.1.0", "concurrently": "^5.1.0", "cross-env": "^5.2.1", "csp-html-webpack-plugin": "^4.0.0", "css-loader": "^2.1.1", "dotenv-webpack": "^5.0.1", - "electron": "^9.4.2", - "electron-builder": "^22.7.0", + "electron": "^15.3.0", + "electron-builder": "^22.13.1", "enzyme-to-json": "^3.5.0", "eslint": "^4.19.1", "eslint-config-airbnb-base": "^13.2.0", @@ -206,10 +210,10 @@ "mini-css-extract-plugin": "^0.9.0", "mongodb": "^3.5.9", "mongoose": "^5.9.22", - "node-sass": "^4.13.1", + "node-sass": "^6.0.1", "nodemon": "^2.0.4", - "postcss-loader": "^2.1.6", - "sass-loader": "^7.0.3", + "postcss-loader": "^6.2.0", + "sass-loader": "^12.3.0", "style-loader": "^0.20.3", "supertest": "^4.0.2", "ts-jest": "^25.5.1",