diff --git a/CHANGE_LOG.md b/CHANGE_LOG.md index 101ce12c2..64ebcb155 100644 --- a/CHANGE_LOG.md +++ b/CHANGE_LOG.md @@ -3,6 +3,17 @@

ReacType Change Log

+**Version 12.0.0 Changes** + +-Context Visualizer: You can now visually see what component is consuming which context. As you click on the interactive tree, the component assigned to the context will be revealed. +-React 18: Updated to React 18 +-Export Feature: Created an exportable context file, integrated with original codebase. +Ready to go code: Added boilerplate codes to components based on which contexts they are consuming. + +**A note to future contributors** + +Attempted to implement Facebook and Google OAuth via passport but as of Electron’s current version, neither of them not compatible with electron. + **Version 11.0.0 Changes:** - Added Next.js functionality diff --git a/README.md b/README.md index 90e7df329..f7b451dc8 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,9 @@ How to use - **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. - **Delete Components**: Delete components after focusing on them in the right panel. Be careful when deleting components: Upon deletion, all instances of the component will be removed within the application/project. +- **Context Visualizer**: You can now visually see what component is consuming which context. As you click on the interactive tree, the component assigned to the context will be revealed. +- **Context Code Preview**: Once contexts have been assigned to the desired components, click ‘Export’ to incorporate context into your existing codebase so you can save it as a file. +- **Ready to go code**: Added boilerplate codes to components based on which contexts they are consuming. - **Add Custom Elements**: Create custom elements or add provided HTML elements into the application. Once the project is exported, the HTML tags generated in the code preview will function as expected. You can specify functionality for custom elements in the code preview. The tutorial on HTML Elements explains more on how to do this. - **Delete Custom HTML Elements**: Delete custom HTML elements by clicking on the ‘X’ button adjacent to the element. Be careful when deleting custom elements: All instances of the element will be deleted within the application/project. - **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. @@ -107,6 +110,8 @@ How to use [Anthony Torrero](https://www.linkedin.com/in/anthony-torrero-4b8798159/) [@Anthonytorrero](https://github.com/Anthonytorrero) +[Bianca Picasso](linkedin.com/in/bianca-picasso) [@BiancaPicasso](https://github.com/BiancaPicasso) + [Brian Han](https://www.linkedin.com/in/brianjisoohan/) [@brianjshan](https://github.com/brianjshan) [Bryan Chau](https://www.linkedin.com/in/chaubryan1/) [@bchauu](https://github.com/bchauu) @@ -139,6 +144,8 @@ How to use [Fredo Chen](https://www.linkedin.com/in/fredochen/) [@fredosauce](https://github.com/fredosauce) +[Huy Pham](linkedin.com/in/huypham048) [@huypham048](https://github.com/huypham048) + [Jonathan Calvo Ramirez](https://www.linkedin.com/in/jonathan-calvo/) [@jonocr](https://github.com/jonocr) [Jesse Zuniga](https://linkedin.com/in/jesse-zuniga) [@jzuniga206](https://github.com/jzuniga206) @@ -149,6 +156,8 @@ How to use [Katrina Henderson](https://www.linkedin.com/in/katrinahenderson/) [@kchender](https://github.com/kchender) +[Ken Bains](linkedin.com/in/ken-bains) [@ken-Bains](https://github.com/ken-Bains) + [Kevin Park](https://www.linkedin.com/in/xkevinpark/) [@xkevinpark](https://github.com/xkevinpark) [Khuong Nguyen](https://www.linkedin.com/in/khuong-nguyen/) [@khuongdn16](https://github.com/khuongdn16) @@ -171,6 +180,8 @@ How to use [Ron Fu](https://www.linkedin.com/in/ronfu)[@rfvisuals](https://github.com/rfvisuals) +[Salvatore Saluga](linkedin.com/in/salvatore-saluga) [@SalSaluga](https://github.com/SalSaluga) + [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) diff --git a/__tests__/contextReducer.test.js b/__tests__/contextReducer.test.js new file mode 100644 index 000000000..9a2f11dd4 --- /dev/null +++ b/__tests__/contextReducer.test.js @@ -0,0 +1,165 @@ +import subject from '../app/src/redux/reducers/slice/contextReducer'; + +describe('Context Reducer', () => { + let state; + + beforeEach(() => { + state = { + allContext: [] + }; + }); + + describe('default state', () => { + it('should return a default state when given an undefined input', () => { + expect(subject(undefined, { type: undefined })).toEqual(state); + }); + }); + + describe('unrecognized action types', () => { + it('should return the original state without any duplication', () => { + expect(subject(state, { type: 'REMOVE_STATE' })).toBe(state); + }); + }); + + describe('ADD_CONTEXT', () => { + const action = { + type: 'ADD_CONTEXT', + payload: { + name: 'Theme Context' + } + }; + + it('adds a context', () => { + const { allContext } = subject(state, action); + expect(allContext[0]).toEqual({ + name: 'Theme Context', + values: [], + components: [] + }); + }); + + it('returns a state object not strictly equal to the original', () => { + const newState = subject(state, action); + expect(newState).not.toBe(state); + }); + + it('should immutably update the nested state object', () => { + const { allContext } = subject(state, action); + expect(allContext).not.toBe(state.allContext); + }); + }); + + describe('ADD_CONTEXT_VALUES', () => { + beforeEach(() => { + state = { + allContext: [ + { + name: 'Theme Context', + values: [], + components: [] + } + ] + }; + }); + + const action = { + type: 'ADD_CONTEXT_VALUES', + payload: { + name: 'Theme Context', + inputKey: 'Theme Color', + inputValue: 'Dark' + } + }; + + it('adds a key-value pair to values array of the specified context', () => { + const { allContext } = subject(state, action); + expect(allContext[0].values.length).toEqual(1); + expect(allContext[0].values[0].key).toEqual('Theme Color'); + expect(allContext[0].values[0].value).toEqual('Dark'); + }); + + it('includes an allContext not strictly equal to the original', () => { + const { allContext } = subject(state, action); + + expect(allContext).not.toBe(state.allContext); + }); + }); + + describe('DELETE CONTEXT', () => { + let action; + beforeEach(() => { + state = { + allContext: [ + { + name: 'Theme Context', + values: [], + components: [] + }, + { + name: 'To be deleted', + values: [], + components: [] + } + ] + }; + + action = { + type: 'DELETE_CONTEXT', + payload: { + name: 'Theme Context' + } + }; + }); + + it('removes specified context from the state', () => { + const { allContext } = subject(state, action); + + expect(allContext.length).toEqual(1); + }); + + it('includes an allContext not strictly equal to the original', () => { + const { allContext } = subject(state, action); + + expect(allContext).not.toBe(state.allContext); + }); + }); + + describe('ADD_COMPONENT_TO_CONTEXT', () => { + beforeEach(() => { + state = { + allContext: [ + { + name: 'Theme Context', + values: [], + components: [] + } + ] + }; + }); + + const action = { + type: 'ADD_COMPONENT_TO_CONTEXT', + payload: { + context: { + name: 'Theme Context' + }, + component: { + name: 'Main Component' + } + } + }; + + it('adds a new component to the specified context', () => { + const { allContext } = subject(state, action); + + expect(allContext[0].components.length).toEqual(1); + expect(allContext[0].components[0]).toEqual('Main Component'); + }); + + it('includes an allContext not strictly equal to the original', () => { + const { allContext } = subject(state, action); + + expect(allContext).not.toBe(state.allContext); + }); + }); +}); diff --git a/__tests__/contextReducer.test.ts b/__tests__/contextReducer.test.ts new file mode 100644 index 000000000..9a2f11dd4 --- /dev/null +++ b/__tests__/contextReducer.test.ts @@ -0,0 +1,165 @@ +import subject from '../app/src/redux/reducers/slice/contextReducer'; + +describe('Context Reducer', () => { + let state; + + beforeEach(() => { + state = { + allContext: [] + }; + }); + + describe('default state', () => { + it('should return a default state when given an undefined input', () => { + expect(subject(undefined, { type: undefined })).toEqual(state); + }); + }); + + describe('unrecognized action types', () => { + it('should return the original state without any duplication', () => { + expect(subject(state, { type: 'REMOVE_STATE' })).toBe(state); + }); + }); + + describe('ADD_CONTEXT', () => { + const action = { + type: 'ADD_CONTEXT', + payload: { + name: 'Theme Context' + } + }; + + it('adds a context', () => { + const { allContext } = subject(state, action); + expect(allContext[0]).toEqual({ + name: 'Theme Context', + values: [], + components: [] + }); + }); + + it('returns a state object not strictly equal to the original', () => { + const newState = subject(state, action); + expect(newState).not.toBe(state); + }); + + it('should immutably update the nested state object', () => { + const { allContext } = subject(state, action); + expect(allContext).not.toBe(state.allContext); + }); + }); + + describe('ADD_CONTEXT_VALUES', () => { + beforeEach(() => { + state = { + allContext: [ + { + name: 'Theme Context', + values: [], + components: [] + } + ] + }; + }); + + const action = { + type: 'ADD_CONTEXT_VALUES', + payload: { + name: 'Theme Context', + inputKey: 'Theme Color', + inputValue: 'Dark' + } + }; + + it('adds a key-value pair to values array of the specified context', () => { + const { allContext } = subject(state, action); + expect(allContext[0].values.length).toEqual(1); + expect(allContext[0].values[0].key).toEqual('Theme Color'); + expect(allContext[0].values[0].value).toEqual('Dark'); + }); + + it('includes an allContext not strictly equal to the original', () => { + const { allContext } = subject(state, action); + + expect(allContext).not.toBe(state.allContext); + }); + }); + + describe('DELETE CONTEXT', () => { + let action; + beforeEach(() => { + state = { + allContext: [ + { + name: 'Theme Context', + values: [], + components: [] + }, + { + name: 'To be deleted', + values: [], + components: [] + } + ] + }; + + action = { + type: 'DELETE_CONTEXT', + payload: { + name: 'Theme Context' + } + }; + }); + + it('removes specified context from the state', () => { + const { allContext } = subject(state, action); + + expect(allContext.length).toEqual(1); + }); + + it('includes an allContext not strictly equal to the original', () => { + const { allContext } = subject(state, action); + + expect(allContext).not.toBe(state.allContext); + }); + }); + + describe('ADD_COMPONENT_TO_CONTEXT', () => { + beforeEach(() => { + state = { + allContext: [ + { + name: 'Theme Context', + values: [], + components: [] + } + ] + }; + }); + + const action = { + type: 'ADD_COMPONENT_TO_CONTEXT', + payload: { + context: { + name: 'Theme Context' + }, + component: { + name: 'Main Component' + } + } + }; + + it('adds a new component to the specified context', () => { + const { allContext } = subject(state, action); + + expect(allContext[0].components.length).toEqual(1); + expect(allContext[0].components[0]).toEqual('Main Component'); + }); + + it('includes an allContext not strictly equal to the original', () => { + const { allContext } = subject(state, action); + + expect(allContext).not.toBe(state.allContext); + }); + }); +}); diff --git a/app/src/components/App.tsx b/app/src/components/App.tsx index d1e0467da..119950398 100644 --- a/app/src/components/App.tsx +++ b/app/src/components/App.tsx @@ -49,16 +49,16 @@ export const App = (): JSX.Element => { } else { console.log( 'No user project found in localforage, setting initial state blank' - ); - } - }); - } - }, []); + ); + } + }); + } + }, []); useEffect(() => { // provide config properties to legacy projects so new edits can be auto saved if (state.config === undefined) { - state.config = {saveFlag:true, saveTimer:false}; - }; + state.config = { saveFlag: true, saveTimer: false }; + } // New project save configuration to optimize server load and minimize Ajax requests if (state.config.saveFlag) { state.config.saveFlag = false; @@ -82,7 +82,7 @@ export const App = (): JSX.Element => { state.config.saveFlag = true; }, 15000); } - }, [state]) + }, [state]); return (
diff --git a/app/src/components/ContextAPIManager/AssignTab/AssignContainer.tsx b/app/src/components/ContextAPIManager/AssignTab/AssignContainer.tsx new file mode 100644 index 000000000..993dfa7e2 --- /dev/null +++ b/app/src/components/ContextAPIManager/AssignTab/AssignContainer.tsx @@ -0,0 +1,144 @@ +import React, { useContext, useState, Fragment, useEffect } from 'react'; +import DataTable from '../CreateTab/components/DataTable'; +import { useStore, useDispatch } from 'react-redux'; +import ContextDropDown from './components/ContextDropDown'; +import ComponentDropDown from './components/ComponentDropDrown'; +import Divider from '@mui/material/Divider'; +import Grid from '@mui/material/Grid'; +import ComponentTable from './components/ComponentTable'; +import { Button } from '@mui/material'; +import DoubleArrowIcon from '@mui/icons-material/DoubleArrow'; +import * as actions from '../../../redux/actions/actions'; +import StateContext from '../../../context/context'; + +const AssignContainer = () => { + const store = useStore(); + const dispatch = useDispatch(); + + const [state, setState] = useState([]); + const defaultTableData = [{ key: 'Key', value: 'Value' }]; + const [tableState, setTableState] = React.useState(defaultTableData); + const [contextInput, setContextInput] = React.useState(null); + const [componentInput, setComponentInput] = React.useState(null); + const [componentTable, setComponentTable] = useState([]); + const [stateContext, dispatchContext] = useContext(StateContext); + + //fetching data from redux store + useEffect(() => { + setState(store.getState().contextSlice); + }, []); + + const renderTable = targetContext => { + if (targetContext === null || !targetContext.values) { + setTableState(defaultTableData); + } else { + setTableState(targetContext.values); + } + }; + + //construct data for table displaying component table + const renderComponentTable = targetComponent => { + //target Component is main + + const listOfContexts = []; + if ( + !Array.isArray(state) && + targetComponent !== null && + targetComponent.name + ) { + state.allContext.forEach(context => { + if (context.components.includes(targetComponent.name)) { + listOfContexts.push(context.name); + } + }); + setComponentTable(listOfContexts); + } + }; + + //handling assignment of contexts to components + const handleAssignment = () => { + if ( + contextInput === '' || + contextInput === null || + componentInput === '' || + componentInput === null + ) + return; + dispatch( + actions.addComponentToContext({ + context: contextInput, + component: componentInput + }) + ); + //trigger generateCode(), update code preview tab + dispatchContext({ + type: 'DELETE ELEMENT', + payload: 'FAKE_ID' + }); + + setState(store.getState().contextSlice); + renderComponentTable(componentInput); + }; + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default AssignContainer; diff --git a/app/src/components/ContextAPIManager/AssignTab/components/ComponentDropDrown.tsx b/app/src/components/ContextAPIManager/AssignTab/components/ComponentDropDrown.tsx new file mode 100644 index 000000000..00bd3922a --- /dev/null +++ b/app/src/components/ContextAPIManager/AssignTab/components/ComponentDropDrown.tsx @@ -0,0 +1,95 @@ +import React, { Fragment, useState, useEffect, useContext } from 'react'; +import TextField from '@mui/material/TextField'; +import Autocomplete, { createFilterOptions } from '@mui/material/Autocomplete'; +import Box from '@mui/material/Box'; +import StateContext from '../../../../context/context'; + +const filter = createFilterOptions(); + +const ComponentDropDown = ({ + contextStore, + renderComponentTable, + componentInput, + setComponentInput +}) => { + const { allContext } = contextStore; + const [componentList] = useContext(StateContext); + + const onChange = (event, newValue) => { + if (typeof newValue === 'string') { + setComponentInput({ + name: newValue + }); + } else if (newValue && newValue.inputValue) { + // Create a new contextInput from the user input + //console.log(newValue,newValue.inputValue) + setComponentInput({ + name: newValue.inputValue, + values: [] + }); + renderComponentTable(newValue); + } else { + setComponentInput(newValue); + renderComponentTable(newValue); + } + }; + + const filterOptions = (options, params) => { + // setBtnDisabled(true); + const filtered = filter(options, params); + const { inputValue } = params; + // Suggest the creation of a new contextInput + const isExisting = options.some(option => inputValue === option.name); + if (inputValue !== '' && !isExisting) { + filtered.push({ + inputValue, + name: `Add "${inputValue}"` + }); + + // setBtnDisabled(false); + } + + return filtered; + }; + + const getOptionLabel = option => { + // Value selected with enter, right from the input + if (typeof option === 'string') { + return option; + } + // Add "xxx" option created dynamically + if (option.inputValue) { + return option.inputValue; + } + // Regular option + return option.name; + }; + + const renderOption = (props, option) =>
  • {option.name}
  • ; + + return ( + + + ( + + )} + /> + + + ); +}; + +export default ComponentDropDown; diff --git a/app/src/components/ContextAPIManager/AssignTab/components/ComponentTable.tsx b/app/src/components/ContextAPIManager/AssignTab/components/ComponentTable.tsx new file mode 100644 index 000000000..e436fc0fd --- /dev/null +++ b/app/src/components/ContextAPIManager/AssignTab/components/ComponentTable.tsx @@ -0,0 +1,52 @@ +import React from 'react'; +import Table from '@mui/material/Table'; +import TableBody from '@mui/material/TableBody'; +import TableContainer from '@mui/material/TableContainer'; +import TableHead from '@mui/material/TableHead'; +import TableRow from '@mui/material/TableRow'; +import Paper from '@mui/material/Paper'; +import { styled } from '@mui/material/styles'; +import TableCell, { tableCellClasses } from '@mui/material/TableCell'; + +const StyledTableCell = styled(TableCell)(({ theme }) => ({ + [`&.${tableCellClasses.head}`]: { + backgroundColor: theme.palette.common.black, + color: theme.palette.common.white + }, + [`&.${tableCellClasses.body}`]: { + fontSize: 14 + } +})); + +const StyledTableRow = styled(TableRow)(({ theme }) => ({ + '&:nth-of-type(odd)': { + backgroundColor: theme.palette.action.hover + }, + // hide last border + '&:last-child td, &:last-child th': { + border: 0 + } +})); + +export default function DataTable({ target }) { + return ( + + + + + Contexts Consumed + + + + {target.map((data, index) => ( + + + {data} + + + ))} + +
    +
    + ); +} diff --git a/app/src/components/ContextAPIManager/AssignTab/components/ContextDropDown.tsx b/app/src/components/ContextAPIManager/AssignTab/components/ContextDropDown.tsx new file mode 100644 index 000000000..37459a45f --- /dev/null +++ b/app/src/components/ContextAPIManager/AssignTab/components/ContextDropDown.tsx @@ -0,0 +1,95 @@ +import React, { Fragment, useState, useEffect } from 'react'; +import TextField from '@mui/material/TextField'; +import Autocomplete, { createFilterOptions } from '@mui/material/Autocomplete'; +import Button from '@mui/material/Button'; +import Box from '@mui/material/Box'; +import { Typography } from '@mui/material'; + +const filter = createFilterOptions(); + +const ContextDropDown = ({ + contextStore, + renderTable, + contextInput, + setContextInput +}) => { + const { allContext } = contextStore; + + const onChange = (event, newValue) => { + if (typeof newValue === 'string') { + setContextInput({ + name: newValue + }); + } else if (newValue && newValue.inputValue) { + // Create a new contextInput from the user input + //console.log(newValue,newValue.inputValue) + setContextInput({ + name: newValue.inputValue, + values: [] + }); + renderTable(newValue); + } else { + setContextInput(newValue); + renderTable(newValue); + } + }; + + const filterOptions = (options, params) => { + // setBtnDisabled(true); + const filtered = filter(options, params); + const { inputValue } = params; + // Suggest the creation of a new contextInput + const isExisting = options.some(option => inputValue === option.name); + if (inputValue !== '' && !isExisting) { + filtered.push({ + inputValue, + name: `Add "${inputValue}"` + }); + + // setBtnDisabled(false); + } + + return filtered; + }; + + const getOptionLabel = option => { + // Value selected with enter, right from the input + if (typeof option === 'string') { + return option; + } + // Add "xxx" option created dynamically + if (option.inputValue) { + return option.inputValue; + } + // Regular option + return option.name; + }; + + const renderOption = (props, option) =>
  • {option.name}
  • ; + + return ( + + + ( + + )} + /> + + + ); +}; + +export default ContextDropDown; diff --git a/app/src/components/ContextAPIManager/AssignTab/components/ContextTable.tsx b/app/src/components/ContextAPIManager/AssignTab/components/ContextTable.tsx new file mode 100644 index 000000000..9c306de08 --- /dev/null +++ b/app/src/components/ContextAPIManager/AssignTab/components/ContextTable.tsx @@ -0,0 +1,72 @@ +import * as React from 'react'; +import { styled } from '@mui/material/styles'; +import Table from '@mui/material/Table'; +import TableBody from '@mui/material/TableBody'; +import TableCell, { tableCellClasses } from '@mui/material/TableCell'; +import TableContainer from '@mui/material/TableContainer'; +import TableHead from '@mui/material/TableHead'; +import TableRow from '@mui/material/TableRow'; +import Paper from '@mui/material/Paper'; + +const StyledTableCell = styled(TableCell)(({ theme }) => ({ + [`&.${tableCellClasses.head}`]: { + backgroundColor: theme.palette.common.black, + color: theme.palette.common.white + }, + [`&.${tableCellClasses.body}`]: { + fontSize: 14 + } +})); + +const StyledTableRow = styled(TableRow)(({ theme }) => ({ + '&:nth-of-type(odd)': { + backgroundColor: theme.palette.action.hover + }, + // hide last border + '&:last-child td, &:last-child th': { + border: 0 + } +})); + +function createData( + name: string, + calories: number, + fat: number, + carbs: number, + protein: number +) { + return { name, calories, fat, carbs, protein }; +} + +const rows = [ + createData('Frozen yoghurt', 159, 6.0, 24, 4.0), + createData('Ice cream sandwich', 237, 9.0, 37, 4.3), + createData('Eclair', 262, 16.0, 24, 6.0), + createData('Cupcake', 305, 3.7, 67, 4.3), + createData('Gingerbread', 356, 16.0, 49, 3.9) +]; +{/*
    */} +export default function ContextTable() { + return ( + + + + + Context + Component + + + + {rows.map(row => ( + + + {row.name} + + {row.calories} + + ))} + +
    +
    + ); +} diff --git a/app/src/components/ContextAPIManager/ContextManager.tsx b/app/src/components/ContextAPIManager/ContextManager.tsx new file mode 100644 index 000000000..958112c8a --- /dev/null +++ b/app/src/components/ContextAPIManager/ContextManager.tsx @@ -0,0 +1,56 @@ +import React, { useContext } from 'react'; +import { makeStyles } from '@material-ui/styles'; +import Box from '@mui/material/Box'; +import Tab from '@mui/material/Tab'; +import TabContext from '@mui/lab/TabContext'; +import TabList from '@mui/lab/TabList'; +import TabPanel from '@mui/lab/TabPanel'; + +import CreateContainer from './CreateTab/CreateContainer'; +import AssignContainer from './AssignTab/AssignContainer'; +import DisplayContainer from './DisplayTab/DisplayContainer'; + +const useStyles = makeStyles({ + contextContainer: { + backgroundColor: 'white', + height: '100%' + } +}); + +const ContextManager = (props): JSX.Element => { + const classes = useStyles(); + const [value, setValue] = React.useState('1'); + + const handleChange = (event: React.SyntheticEvent, newValue: string) => { + setValue(newValue); + }; + + return ( + +
    + + + + + + + + + + + + + + + + + + + + +
    +
    + ); +}; + +export default ContextManager; diff --git a/app/src/components/ContextAPIManager/CreateTab/CreateContainer.tsx b/app/src/components/ContextAPIManager/CreateTab/CreateContainer.tsx new file mode 100644 index 000000000..7353526d7 --- /dev/null +++ b/app/src/components/ContextAPIManager/CreateTab/CreateContainer.tsx @@ -0,0 +1,122 @@ +import React, { useEffect, useState, useContext } from 'react'; +import { useStore } from 'react-redux'; +import { useDispatch } from 'react-redux'; +import Divider from '@mui/material/Divider'; +import Grid from '@mui/material/Grid'; +import DataTable from './components/DataTable'; +import AddDataForm from './components/AddDataForm'; +import AddContextForm from './components/AddContextForm'; +import * as actions from '../../../redux/actions/actions'; +import { Typography } from '@mui/material'; +import StateContext from '../../../context/context'; + +const CreateContainer = () => { + const defaultTableData = [{ key: 'Enter Key', value: 'Enter value' }]; + const store = useStore(); + const [state, setState] = useState([]); + const [tableState, setTableState] = React.useState(defaultTableData); + const [contextInput, setContextInput] = React.useState(null); + const [stateContext, dispatchContext] = useContext(StateContext); + + //pulling data from redux store + useEffect(() => { + setState(store.getState().contextSlice); + }, []); + + const dispatch = useDispatch(); + + //update data store when user adds a new context + const handleClickSelectContext = () => { + //prevent user from adding duplicate context + for (let i = 0; i < state.allContext.length; i += 1) { + if (state.allContext[i].name === contextInput.name) { + return; + } + } + setContextInput(''); + dispatch(actions.addContextActionCreator(contextInput)); + setState(store.getState().contextSlice); + }; + + //update data store when user add new key-value pair to context + const handleClickInputData = ({ name }, { inputKey, inputValue }) => { + dispatch( + actions.addContextValuesActionCreator({ name, inputKey, inputValue }) + ); + setState(store.getState().contextSlice); + }; + + //update data store when user deletes context + const handleDeleteContextClick = () => { + dispatch(actions.deleteContext(contextInput)); + setContextInput(''); + setState(store.getState().contextSlice); + setTableState(defaultTableData); + dispatchContext({ + type: 'DELETE ELEMENT', + payload: 'FAKE_ID' + }); + }; + + //re-render data table when there's new changes + const renderTable = targetContext => { + if ( + targetContext === null || + targetContext === undefined || + !targetContext.values + ) { + // if (targetContext === null || targetContext === undefined) { + setTableState(defaultTableData); + } else { + setTableState(targetContext.values); + } + }; + return ( + <> + + + + + + + + + + + + + + + + + Context Data Table + + + + + + ); +}; + +export default CreateContainer; diff --git a/app/src/components/ContextAPIManager/CreateTab/components/AddContextForm.tsx b/app/src/components/ContextAPIManager/CreateTab/components/AddContextForm.tsx new file mode 100644 index 000000000..4bba060ac --- /dev/null +++ b/app/src/components/ContextAPIManager/CreateTab/components/AddContextForm.tsx @@ -0,0 +1,129 @@ +import React, { Fragment, useState, useEffect, useContext } from 'react'; +import TextField from '@mui/material/TextField'; +import Autocomplete, { createFilterOptions } from '@mui/material/Autocomplete'; +import Button from '@mui/material/Button'; +import Box from '@mui/material/Box'; +import { Typography } from '@mui/material'; +import StateContext from '../../../../context/context'; + +const filter = createFilterOptions(); + +const AddContextForm = ({ + contextStore, + handleClickSelectContext, + handleDeleteContextClick, + renderTable, + contextInput, + setContextInput +}) => { + const { allContext } = contextStore; + const [btnDisabled, setBtnDisabled] = useState(false); + const [state, dispatch] = useContext(StateContext); + + const handleClick = () => { + if (contextInput === '' || contextInput === null) return; + handleClickSelectContext(); + + //need to trigger the generate code functionality to update the code preview tab. Sending dummy data to trigger with a DELELTE ELEMENT dispatch method + dispatch({ + type: 'DELETE ELEMENT', + payload: 'FAKE_ID' + }); + }; + + const onChange = (event, newValue) => { + if (typeof newValue === 'string') { + setContextInput({ + name: newValue + }); + } else if (newValue && newValue.inputValue) { + // Create a new contextInput from the user input + //console.log(newValue,newValue.inputValue) + setContextInput({ + name: newValue.inputValue, + values: [] + }); + renderTable(newValue); + } else { + setContextInput(newValue); + renderTable(newValue); + } + }; + + const filterOptions = (options, params) => { + // setBtnDisabled(true); + const filtered = filter(options, params); + const { inputValue } = params; + // Suggest the creation of a new contextInput + const isExisting = options.some(option => inputValue === option.name); + if (inputValue !== '' && !isExisting) { + filtered.push({ + inputValue, + name: `Add "${inputValue}"` + }); + + // setBtnDisabled(false); + } + + return filtered; + }; + + const getOptionLabel = option => { + // Value selected with enter, right from the input + if (typeof option === 'string') { + return option; + } + // Add "xxx" option created dynamically + if (option.inputValue) { + return option.inputValue; + } + // Regular option + return option.name; + }; + + const renderOption = (props, option) =>
  • {option.name}
  • ; + + return ( + + + Context Input + + + ( + + )} + /> + + + {/* */} + + + ); +}; + +export default AddContextForm; diff --git a/app/src/components/ContextAPIManager/CreateTab/components/AddDataForm.tsx b/app/src/components/ContextAPIManager/CreateTab/components/AddDataForm.tsx new file mode 100644 index 000000000..b21439053 --- /dev/null +++ b/app/src/components/ContextAPIManager/CreateTab/components/AddDataForm.tsx @@ -0,0 +1,59 @@ +import React, { Fragment, useState, useEffect } from 'react'; +import TextField from '@mui/material/TextField'; +import Button from '@mui/material/Button'; +import Box from '@mui/material/Box'; +import { Typography } from '@mui/material'; + +const AddDataForm = ({ handleClickInputData, contextInput }) => { + //const [contextInput, setContextInput] = React.useState(null); + const defaultInputData = {inputKey: '', inputValue: ''}; + const [dataContext, setDataContext] = React.useState(defaultInputData); + + const saveData = () => { + setDataContext(defaultInputData); + handleClickInputData(contextInput, dataContext) + } + + const handleChange = e => { + setDataContext(prevDataContext => { + return { + ...prevDataContext, + [e.target.name]: e.target.value + }; + }); + }; + + return ( + + + Add context data + + + handleChange(e)} + /> + handleChange(e)} + /> + + + + ); +}; + +export default AddDataForm; diff --git a/app/src/components/ContextAPIManager/CreateTab/components/DataTable.tsx b/app/src/components/ContextAPIManager/CreateTab/components/DataTable.tsx new file mode 100644 index 000000000..790c9fd30 --- /dev/null +++ b/app/src/components/ContextAPIManager/CreateTab/components/DataTable.tsx @@ -0,0 +1,68 @@ +import React from 'react'; +import Table from '@mui/material/Table'; +import TableBody from '@mui/material/TableBody'; +import TableContainer from '@mui/material/TableContainer'; +import TableHead from '@mui/material/TableHead'; +import TableRow from '@mui/material/TableRow'; +import Paper from '@mui/material/Paper'; +import { styled } from '@mui/material/styles'; +import TableCell, { tableCellClasses } from '@mui/material/TableCell'; +import TableFooter from '@mui/material/TableFooter'; + +const StyledTableCell = styled(TableCell)(({ theme }) => ({ + [`&.${tableCellClasses.head}`]: { + backgroundColor: theme.palette.common.black, + color: theme.palette.common.white + }, + [`&.${tableCellClasses.body}`]: { + fontSize: 14 + } +})); + +const StyledTableRow = styled(TableRow)(({ theme }) => ({ + '&:nth-of-type(odd)': { + backgroundColor: theme.palette.action.hover + }, + // hide last border + '&:last-child td, &:last-child th': { + border: 0 + } +})); + +export default function DataTable({ target, contextInput }) { + return ( + <> + + + + + {/* Key */} + + {contextInput ? contextInput.name : 'Context Name'} + + + + + {target.map((data, index) => ( + + + {data.key} + + {data.value} + + ))} + + {/* + + {contextInput ? contextInput.name : 'Context Name'} + + */} +
    +
    + + ); +} diff --git a/app/src/components/ContextAPIManager/DisplayTab/DisplayContainer.tsx b/app/src/components/ContextAPIManager/DisplayTab/DisplayContainer.tsx new file mode 100644 index 000000000..c28cd1016 --- /dev/null +++ b/app/src/components/ContextAPIManager/DisplayTab/DisplayContainer.tsx @@ -0,0 +1,47 @@ +import React, { useEffect, useState } from 'react'; +import { useStore } from 'react-redux'; +import { Chart } from 'react-google-charts'; +import Grid from '@mui/material/Grid'; + +const DisplayContainer = () => { + const store = useStore(); + const { allContext } = store.getState().contextSlice; + const [contextData, setContextData] = useState([]); + + //build data for Google charts, tree rendering + useEffect(() => { + transformData(allContext); + }, []); + + const transformData = contexts => { + const formattedData = contexts + .map(el => { + return el.components.map(component => { + return [`App ${el.name} ${component}`]; + }); + }) + .flat(); + setContextData([['Phrases'], ...formattedData]); + }; + + const options = { + wordtree: { + format: 'implicit', + word: 'App' + } + }; + return ( + + + + + + ); +}; +export default DisplayContainer; diff --git a/app/src/components/bottom/BottomTabs.tsx b/app/src/components/bottom/BottomTabs.tsx index a392b3225..005b3ee8c 100644 --- a/app/src/components/bottom/BottomTabs.tsx +++ b/app/src/components/bottom/BottomTabs.tsx @@ -5,8 +5,9 @@ import Tabs from '@material-ui/core/Tabs'; import Tab from '@material-ui/core/Tab'; import CodePreview from './CodePreview'; import StylesEditor from './StylesEditor'; -import CustomizationPanel from '../../containers/CustomizationPanel' -import CreationPanel from './CreationPanel' +import CustomizationPanel from '../../containers/CustomizationPanel'; +import CreationPanel from './CreationPanel'; +import ContextManager from '../ContextAPIManager/ContextManager'; import Box from '@material-ui/core/Box'; import Tree from '../../tree/TreeChart'; import FormControl from '@material-ui/core/FormControl'; @@ -43,8 +44,17 @@ const BottomTabs = (props): JSX.Element => { Arrow.renderArrow(state.canvasFocus.childId); return ( -
    - +
    + { classes={{ root: classes.tabRoot, selected: classes.tabSelected }} label="Component Tree" /> + -
    - +
    +
    @@ -101,6 +122,7 @@ const BottomTabs = (props): JSX.Element => { {tab === 2 && } {tab === 3 && } {tab === 4 && } + {tab === 5 && }
    ); }; @@ -110,8 +132,7 @@ const useStyles = makeStyles(theme => ({ flexGrow: 1, height: '100%', color: '#E8E8E8', - boxShadow: '0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23)', - + boxShadow: '0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23)' }, rootLight: { backgroundColor: '#003366' @@ -126,7 +147,7 @@ const useStyles = makeStyles(theme => ({ minHeight: '50%' }, tabsIndicator: { - backgroundColor: 'white', + backgroundColor: 'white' }, tabRoot: { textTransform: 'initial', @@ -155,7 +176,7 @@ const useStyles = makeStyles(theme => ({ fontWeight: theme.typography.fontWeightMedium }, '&:focus': { - color: 'white', + color: 'white' } }, tabSelected: {}, @@ -168,7 +189,7 @@ const useStyles = makeStyles(theme => ({ switch: { marginRight: '10px', marginTop: '2px' - }, + }, projectTypeWrapper: { marginTop: '10px', marginBotton: '10px' @@ -180,4 +201,3 @@ const useStyles = makeStyles(theme => ({ })); export default BottomTabs; - diff --git a/app/src/components/bottom/CodePreview.tsx b/app/src/components/bottom/CodePreview.tsx index 458f01460..c917d2bcb 100644 --- a/app/src/components/bottom/CodePreview.tsx +++ b/app/src/components/bottom/CodePreview.tsx @@ -19,27 +19,24 @@ import store from '../../redux/store'; const CodePreview: React.FC<{ theme: string | null; setTheme: any | null; - }> = ({ theme, setTheme }) => { - - +}> = ({ theme, setTheme }) => { const ref = useRef(); - + /** - * Starts the Web Assembly service. - */ + * Starts the Web Assembly service. + */ const startService = async () => { ref.current = await esbuild.startService({ worker: true, - wasmURL: 'https://unpkg.com/esbuild-wasm@0.8.27/esbuild.wasm', - }) - } + wasmURL: 'https://unpkg.com/esbuild-wasm@0.8.27/esbuild.wasm' + }); + }; const wrapper = useRef(); const dimensions = useResizeObserver(wrapper); - const {height } = - dimensions || 0; + const { height } = dimensions || 0; - const [state,] = useContext(StateContext); + const [state] = useContext(StateContext); const [, setDivHeight] = useState(0); let currentComponent = state.components.find( (elem: Component) => elem.id === state.canvasFocus.componentId @@ -53,49 +50,51 @@ const CodePreview: React.FC<{ useEffect(() => { setDivHeight(height); - }, [height]) + }, [height]); useEffect(() => { - - setInput(currentComponent.code); - store.dispatch({type: "CODE_PREVIEW_INPUT", payload: currentComponent.code}); - }, [state.components]) + setInput(currentComponent.code); + store.dispatch({ + type: 'CODE_PREVIEW_INPUT', + payload: currentComponent.code + }); + }, [state.components]); /** - * Handler thats listens to changes in code editor - * @param {string} data - Code entered by the user - */ - const handleChange = async (data) => { + * Handler thats listens to changes in code editor + * @param {string} data - Code entered by the user + */ + const handleChange = async data => { setInput(data); - store.dispatch({type: "CODE_PREVIEW_INPUT", payload: data}); - if(!ref.current) { + store.dispatch({ type: 'CODE_PREVIEW_INPUT', payload: data }); + if (!ref.current) { return; } let result = await ref.current.build({ entryPoints: ['index.js'], bundle: true, write: false, - incremental:true, + incremental: true, minify: true, - plugins: [ - unpkgPathPlugin(), - fetchPlugin(data) - ], + plugins: [unpkgPathPlugin(), fetchPlugin(data)], define: { 'process.env.NODE_ENV': '"production"', global: 'window' } - }) - store.dispatch({type: "CODE_PREVIEW_SAVE", payload: result.outputFiles[0].text}); - } + }); + store.dispatch({ + type: 'CODE_PREVIEW_SAVE', + payload: result.outputFiles[0].text + }); + }; return (
    -
    ); }; export default CodePreview; - - - - diff --git a/app/src/components/bottom/CreationPanel.tsx b/app/src/components/bottom/CreationPanel.tsx index 6b5c55bd8..bd1547edb 100644 --- a/app/src/components/bottom/CreationPanel.tsx +++ b/app/src/components/bottom/CreationPanel.tsx @@ -10,6 +10,7 @@ const CreationPanel = (props): JSX.Element => { const {style} = useContext(styleContext); return (
    + diff --git a/app/src/components/login/FBPassWord.tsx b/app/src/components/login/FBPassWord.tsx index d19dc7861..6601e9e3b 100644 --- a/app/src/components/login/FBPassWord.tsx +++ b/app/src/components/login/FBPassWord.tsx @@ -195,7 +195,7 @@ const SignUp: React.FC = props => { > Sign Up + - + Already have an account? Sign In diff --git a/app/src/components/login/SignUp.tsx b/app/src/components/login/SignUp.tsx index f0de334aa..ef657e855 100644 --- a/app/src/components/login/SignUp.tsx +++ b/app/src/components/login/SignUp.tsx @@ -310,7 +310,7 @@ const SignUp: React.FC = props => { > Sign Up - + Already have an account? Sign In diff --git a/app/src/components/main/DemoRender.tsx b/app/src/components/main/DemoRender.tsx index 3635215a2..4df4bcd49 100644 --- a/app/src/components/main/DemoRender.tsx +++ b/app/src/components/main/DemoRender.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect, useRef, useContext } from 'react'; -import { BrowserRouter as Router, Switch, Route, Link } from "react-router-dom"; +import { BrowserRouter as Router, Switch, Route, Link } from 'react-router-dom'; import Box from '@material-ui/core/Box'; import cssRefresher from '../../helperFunctions/cssRefresh'; import { useSelector } from 'react-redux'; @@ -17,12 +17,12 @@ const DemoRender = (): JSX.Element => { (elem: Component) => elem.id === state.canvasFocus.componentId ); - // Create React ref to inject transpiled code in inframe + // Create React ref to inject transpiled code in inframe const iframe = useRef(); const demoContainerStyle = { width: '100%', backgroundColor: '#FBFBFB', - border: '2px Solid grey', + border: '2px Solid grey' }; const html = ` @@ -55,14 +55,19 @@ const DemoRender = (): JSX.Element => { `; //Switch between components when clicking on a link in the live render - window.onmessage = (event) => { - if(event.data === undefined) return; - const component:string = event.data?.split('/').at(-1); - const componentId = component && state.components?.find((el) => { - return el.name.toLowerCase() === component.toLowerCase(); - }).id; - componentId && dispatch({ type: 'CHANGE FOCUS', payload: {componentId, childId: null}}) - + window.onmessage = event => { + if (event.data === undefined) return; + const component: string = event.data?.split('/').at(-1); + const componentId = + component && + state.components?.find(el => { + return el.name.toLowerCase() === component.toLowerCase(); + }).id; + componentId && + dispatch({ + type: 'CHANGE FOCUS', + payload: { componentId, childId: null } + }); }; // This function is the heart of DemoRender it will take the array of components stored in state and dynamically construct the desired React component for the live demo @@ -78,17 +83,81 @@ const DemoRender = (): JSX.Element => { const classRender = element.attributes.cssClasses; const activeLink = element.attributes.compLink; let renderedChildren; - if (elementType !== 'input' && elementType !== 'img' && elementType !== 'Image' && element.children.length > 0) { + if ( + elementType !== 'input' && + elementType !== 'img' && + elementType !== 'Image' && + element.children.length > 0 + ) { renderedChildren = componentBuilder(element.children); } - if (elementType === 'input') componentsToRender.push(); - else if (elementType === 'img') componentsToRender.push(); - else if (elementType === 'Image') componentsToRender.push(); - else if (elementType === 'a' || elementType === 'Link') componentsToRender.push({innerText}{renderedChildren}); - else if (elementType === 'Switch') componentsToRender.push({renderedChildren}); - else if (elementType === 'Route') componentsToRender.push({renderedChildren}); - else componentsToRender.push({innerText}{renderedChildren} - ); + if (elementType === 'input') + componentsToRender.push( + + ); + else if (elementType === 'img') + componentsToRender.push( + + ); + else if (elementType === 'Image') + componentsToRender.push( + + ); + else if (elementType === 'a' || elementType === 'Link') + componentsToRender.push( + + {innerText} + {renderedChildren} + + ); + else if (elementType === 'Switch') + componentsToRender.push({renderedChildren}); + else if (elementType === 'Route') + componentsToRender.push( + + {renderedChildren} + + ); + else + componentsToRender.push( + + {innerText} + {renderedChildren} + + ); key += 1; } } @@ -96,10 +165,12 @@ const DemoRender = (): JSX.Element => { }; let code = ''; - const currComponent = state.components.find(element => element.id === state.canvasFocus.componentId); + const currComponent = state.components.find( + element => element.id === state.canvasFocus.componentId + ); componentBuilder(currComponent.children).forEach(element => { - try{ + try { code += ReactDOMServer.renderToString(element); } catch { return; @@ -108,17 +179,23 @@ const DemoRender = (): JSX.Element => { useEffect(() => { cssRefresher(); - }, []) + }, []); useEffect(() => { iframe.current.contentWindow.postMessage(code, '*'); - }, [code]) + }, [code]); return (
    -