├── .DS_Store ├── .babelrc ├── .eslintrc.json ├── .gitignore ├── .travis.yml ├── LICENSE.md ├── README.md ├── __mocks__ ├── appStateMocks.ts └── localStorageMock.ts ├── __tests__ ├── reducerCreateAndDeleteComponentsAndChildren.test.ts └── reducerReduxStoreConfig.test.ts ├── images ├── actions.PNG ├── addordeletecomponent.PNG ├── addreduxconn.PNG ├── createcomponent.PNG ├── createinterface.PNG ├── deletecomponent.PNG ├── export.PNG ├── household.PNG ├── interfacefields.PNG ├── store.PNG └── storeselections.PNG ├── jest.config.js ├── main.js ├── package-lock.json ├── package.json ├── preducks-logo-text.svg ├── preducks-logo-text@2x.png ├── src ├── .DS_Store ├── actionTypes │ └── index.js ├── actions │ └── components.ts ├── components │ ├── Actions.tsx │ ├── App.tsx │ ├── BottomPanel.tsx │ ├── BottomTabs.tsx │ ├── CodePreview.tsx │ ├── ComponentReduxSetup.tsx │ ├── DataTable.tsx │ ├── ErrorMessage.tsx │ ├── HTMLComponentPanel.tsx │ ├── HtmlAttr.tsx │ ├── HtmlChild.tsx │ ├── Interface.tsx │ ├── Interfaces.tsx │ ├── LeftColExpansionPanel.tsx │ ├── NewTreeDisplay.tsx │ ├── Reducer.tsx │ ├── Reducers.tsx │ ├── RightPanel.tsx │ ├── SimpleModal.tsx │ ├── Store.tsx │ ├── StoreItemHeader.tsx │ ├── TypeSelect.tsx │ └── theme.ts ├── containers │ ├── AppContainer.tsx │ ├── LeftContainer.tsx │ └── MainContainer.tsx ├── index.js ├── localStorage.js ├── public │ ├── LICENSE │ ├── README.md │ ├── _headers │ ├── icons │ │ ├── mac │ │ │ └── preducks.icns │ │ ├── png │ │ │ ├── 128x128.png │ │ │ ├── 16x16.png │ │ │ ├── 24x24.png │ │ │ ├── 256x256.png │ │ │ ├── 32x32.png │ │ │ ├── 48x48.png │ │ │ ├── 512x512.png │ │ │ ├── 64x64.png │ │ │ └── 96x96.png │ │ └── win │ │ │ └── preducks.ico │ ├── images │ │ ├── TreeImageComponent.png │ │ └── file structure photo.png │ ├── index.html │ └── styles │ │ └── index.css ├── reducers │ ├── componentReducer.ts │ └── index.js ├── store.js └── utils │ ├── InterfaceDefinitions.ts │ ├── cloneDeep.ts │ ├── colors.util.ts │ ├── componentReducer.util.ts │ ├── componentRender.util.ts │ ├── createApplication.util.ts │ ├── createComponentFiles.util.ts │ ├── createModal.util.tsx │ ├── createReduxFiles.util.ts │ ├── dummyData.ts │ ├── formatter.util.ts │ ├── getSelectable.util.ts │ ├── htmlElements.util.ts │ └── validateInput.util.ts ├── tsconfig.json ├── tslint.json ├── webpack.common.js ├── webpack.dev.js └── webpack.prod.js /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/preducks/1ef20cbca94cfebdc62ef99a86292269a7275052/.DS_Store -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "env", 5 | { 6 | "modules": false 7 | } 8 | ], 9 | "react", 10 | "stage-0" 11 | // "@babel/preset-typescript" 12 | ], 13 | "plugins": ["transform-es2015-modules-commonjs"] 14 | } 15 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["plugin:react/recommended", "airbnb-base"], 3 | "parserOptions": { 4 | "ecmaFeatures": { 5 | "jsx": true 6 | }, 7 | "ecmaVersion": 2018, 8 | "sourceType": "module" 9 | }, 10 | "plugins": ["import", "react", "jest", "jsx-a11y", "babel"], 11 | "parser": "babel-eslint", 12 | "env": { 13 | "browser": true, 14 | "node": true, 15 | "es6": true, 16 | "jest": true 17 | }, 18 | "rules": { 19 | "class-methods-use-this": "off", 20 | "linebreak-style": 0 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | .* 4 | !.gitignore -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '10' 4 | - '12' 5 | services: 6 | - xvfb 7 | # before_script: 8 | # - 'export DISPLAY=:99.0' 9 | # - 'sh -e /etc/init.d/xvfb start' 10 | # - sleep 3 # give xvfb some time to start 11 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 preducks 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/oslabs-beta/preducks/pulls) 2 | ![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg) 3 | [![Netlify Status](https://api.netlify.com/api/v1/badges/b0647ae0-9b38-4cf5-9b66-932bb2b02699/deploy-status)](https://app.netlify.com/sites/preducks/deploys) 4 | 5 | ![preduck](https://raw.githubusercontent.com/palgorhythm/preducks/master/preducks-logo-text.svg?sanitize=true) 6 | 7 | **preducks** is a visual prototyping tool for developers employing **React** component architecture and **Redux** state management, alongside the comprehensive type checking of **TypeScript**. 8 | In other words, **you can create prototypes and export React / Typescript code!** 9 | 10 | **preducks** allows the user to _visualize_ their application architecture dynamically, employing an _application tree_ and a _real-time component code preview_. The user can create components and load _instances_ of these components, as well as nested HTML elements, onto the tree. The user can also specify the desired shape of their _Redux store_ and _reducers_, as well as _interfaces_ to describe the desired shape of their data, and connections between their components and the store using _Redux hooks_. This architecture can then be _exported_ as TypeScript application files to be used as a starter template for any repository. 11 | 12 | ## how to use: 13 | 14 | #### creating React components: 15 | 16 | To create a new React component, type the name of your component into the _add component_ box and click the _+_ button next to it.\ 17 | ![create component](/images/createcomponent.PNG)\ 18 | \ 19 | To add a component as a child of another component, click on the parent component in the list of components, and press the _+_ button next to the component you wish to add as a child. To remove a child from a parent, click on the parent in the list, and press the _-_ button which appears next to the component you wish to remove.\ 20 | You will not be able to add a component's parents or 21 | grandparents as its children.\ 22 | ![add or remove child](/images/addordeletecomponent.PNG)\ 23 | \ 24 | To completely delete a component, click on it in the list of components and click the _DELETE COMPONENT_ button.\ 25 | ![delete component](/images/deletecomponent.PNG) 26 | 27 | #### creating interfaces for the Redux store: 28 | 29 | Since our app uses TypeScript, we give you the opportunity to create TypeScript interfaces to describe the shapes of the data you wish to use in your Redux store. By default, we allow you to choose from the types _number_, _string_, _boolean_, and _any_. You can specify whether you want a value to be a single value of this type, or an array of these types. Once you define an interface, you will be able to use it as the data type of later values you define along with the default ones we provide. All of your interfaces will be exported in a TypeScript file, and they will be imported in components that need them.\ 30 | To create an interface, type in the name of the interface in the _new interface_ field and press the _+_ button next to it.\ 31 | ![create interface](/images/createinterface.PNG)\ 32 | \ 33 | You can add fields to an interface by entering a name and type for a field, and can also specify if you want that field to be an array, then clicking the plus button next to the form fields. Here we create an interface to describe a _person_ data type. We also create a _household_ interface that uses the _person_ interface we just made in a few of its fields.\ 34 | ![add to interface](/images/interfacefields.PNG)\ 35 | ![interface2](/images/household.PNG)\ 36 | To delete a field, click the trashcan icon that appears next to the field when you hover over it.\ 37 | To delete the entire interface, click on the trashcan icon that appears next to the interface's name. 38 | 39 | #### creating reducers for the Redux store 40 | 41 | The form to create reducers appears below the form to create interfaces. You create one the same way as interfaces. Just type the name of the reducer into the field and click the _+_ button. You can make multiple reducers. We combine them for you into one Redux store.\ 42 | Once you create a reducer, you can start adding properties to its store. Specify a name for the propety, its type, whether or not it's an array, and an initial value. This will be used as the initial state for your Redux store, and we also generate a TypeScript interface to describe the shape of your store.\ 43 | ![store](/images/store.PNG)\ 44 | \ 45 | Under the store configuration options, you can also define action creators. This is mainly just to create the boilerplate and type definitions; you'll have to add the logic for the action creators yourself once you export your project. You can provide a name for your action creator, specify whether or not it's asynchronous, give a name and type to the parameters expected by the action creator, and specify the type of its payload.\ 46 | ![actions](/images/actions.PNG)\ 47 | \ 48 | As before, you can delete individual store properties or actions by clicking on the trashcan icon that appears when you hover over them, and you can delete the entire reducer by clicking on the trashcan icon that appears next to the reducer name. 49 | 50 | #### connecting React components to the Redux store: 51 | 52 | You can connect a component to the Redux store (and also add local state to a component if necessary) using the _local state & redux connection_ tab at the bottom of the app.\ 53 | We use Redux hooks (_useSelector()_) to connect components to the Redux store rather than using a _mapStateToProps()_ function and wrapping the exported component in _connect()_.\ 54 | You can use the dropdowns to select which store properties you want your component to connect to, and to import actions you want your component to be able to dispatch. (Again, we use the _useDispatch()_ hook to give you access to _dispatch()_ rather than using a _mapDispatchToProps()_ function).\ 55 | ![addreduxconn](/images/addreduxconn.PNG)\ 56 | \ 57 | When you add store selections or actions to a component, they will show up on a list near the dropdowns. Here, you can remove anything you added from the component.\ 58 | ![store selections](/images/storeselections.PNG)\ 59 | \ 60 | You can also add pieces of local state to your component (we use only functional components in our app, so we use the _useState()_ hook instead of creating _this.state_ in a constructor). As usual, you supply the name, type, and value of the state you want to add. You can delete pieces of state with the trashcan icon next to the specific piece in the list of local state, and edit information about state using the pencil icon. 61 | 62 | #### exporting your project: 63 | 64 | In the bottom left corner, there will be two buttons:\ 65 | ![export](/images/export.PNG)\ 66 | A red button to clear your workspace (this will clear any changes you've made) and a green button to export your project. The export button will download a zip file which will contain the full boilerplate code for your app, based on all the information you've entered. Obviously, you'll have to add most of the application logic yourself, but using our tool should greatly reduce the time you have to spend setting up and organizing the hierarchy of your components and application state. 67 | 68 | ## running your own version 69 | 70 | - **Fork** and **Clone** repository. 71 | - open project directory 72 | - install dependencies 73 | 74 | ```bash 75 | npm install 76 | ``` 77 | 78 | - run application 79 | 80 | ```bash 81 | npm start 82 | ``` 83 | 84 | - for development experience 85 | 86 | ```bash 87 | npm run dev 88 | ``` 89 | 90 | ## License 91 | 92 | This project is licensed under the MIT License - see the [LICENSE.md](https://github.com/oslabs-beta/preducks/blob/development/LICENSE.md) file for details. 93 | -------------------------------------------------------------------------------- /__mocks__/appStateMocks.ts: -------------------------------------------------------------------------------- 1 | import cloneDeep from '../src/utils/cloneDeep'; 2 | 3 | const appComponent = { 4 | id: 1, 5 | stateful: false, 6 | componentState: [], 7 | title: 'App', 8 | color: '#FF6D00', 9 | props: [], 10 | nextPropId: 1, 11 | childrenArray: [], 12 | nextChildId: 1, 13 | focusChildId: 0, 14 | selectors: [], 15 | actions: [], 16 | }; 17 | 18 | const appComponent2 = { 19 | id: 1, 20 | stateful: false, 21 | componentState: [ 22 | {name: 'test', type: 'string', initialValue: 'test'} 23 | ], 24 | title: 'App', 25 | color: '#FF6D00', 26 | props: [], 27 | nextPropId: 1, 28 | childrenArray: [], 29 | nextChildId: 1, 30 | focusChildId: 0, 31 | selectors: ['test.selector'], 32 | actions: ['test.action'], 33 | }; 34 | 35 | const initialApplicationFocusChild = { 36 | childId: 0, 37 | componentName: null, 38 | childType: null, 39 | childSort: 0, 40 | childComponentId: 0, 41 | color: null, 42 | htmlElement: null, 43 | HTMLInfo: null, 44 | }; 45 | 46 | export const initialAppStateMock = { 47 | totalComponents: 1, 48 | nextId: 2, 49 | successOpen: false, 50 | errorOpen: false, 51 | focusComponent: appComponent, 52 | selectableChildren: [], 53 | ancestors: [], 54 | initialApplicationFocusChild, 55 | focusChild: cloneDeep(initialApplicationFocusChild), 56 | components: [appComponent], 57 | appDir: '', 58 | loading: false, 59 | storeConfig: { interfaces: {}, reducers: {} }, 60 | }; 61 | 62 | export const initialAppStateMock2 = { 63 | totalComponents: 1, 64 | nextId: 2, 65 | successOpen: false, 66 | errorOpen: false, 67 | focusComponent: appComponent2, 68 | selectableChildren: [], 69 | ancestors: [], 70 | initialApplicationFocusChild, 71 | focusChild: cloneDeep(initialApplicationFocusChild), 72 | components: [appComponent2], 73 | appDir: '', 74 | loading: false, 75 | storeConfig: { interfaces: { 76 | test: { type1: 'string', type2: 'number' } 77 | }, reducers: { 78 | test: { store: {}, actions: {} } 79 | } }, 80 | }; 81 | 82 | 83 | export const aboutToAddChildMock = { 84 | totalComponents: 2, 85 | nextId: 3, 86 | successOpen: false, 87 | errorOpen: false, 88 | focusComponent: { 89 | id: 1, 90 | stateful: false, 91 | componentState: [], 92 | title: 'App', 93 | color: '#FF6D00', 94 | props: [], 95 | nextPropId: 1, 96 | childrenArray: [], 97 | nextChildId: 1, 98 | focusChildId: 0, 99 | selectors: [], 100 | actions: [], 101 | }, 102 | selectableChildren: [2], 103 | ancestors: [], 104 | initialApplicationFocusChild: { 105 | childId: 0, 106 | componentName: null, 107 | childType: null, 108 | childSort: 0, 109 | childComponentId: 0, 110 | color: null, 111 | htmlElement: null, 112 | HTMLInfo: null, 113 | }, 114 | focusChild: { 115 | childId: 0, 116 | componentName: null, 117 | childType: null, 118 | childSort: 0, 119 | childComponentId: 0, 120 | color: null, 121 | htmlElement: null, 122 | HTMLInfo: null, 123 | }, 124 | components: [ 125 | { 126 | id: 1, 127 | stateful: false, 128 | componentState: [], 129 | title: 'App', 130 | color: '#FF6D00', 131 | props: [], 132 | nextPropId: 1, 133 | childrenArray: [], 134 | nextChildId: 1, 135 | focusChildId: 0, 136 | selectors: [], 137 | actions: [], 138 | }, 139 | { 140 | id: 2, 141 | stateful: false, 142 | componentState: [], 143 | title: 'Child', 144 | color: '#7395AE', 145 | props: [], 146 | nextPropId: 1, 147 | childrenArray: [], 148 | nextChildId: 1, 149 | focusChildId: 0, 150 | selectors: [], 151 | actions: [], 152 | }, 153 | ], 154 | appDir: '', 155 | loading: false, 156 | storeConfig: { 157 | interfaces: {}, 158 | reducers: {}, 159 | }, 160 | }; 161 | 162 | export const aboutToDeleteComponentMock = { 163 | totalComponents: 2, 164 | nextId: 3, 165 | successOpen: false, 166 | errorOpen: false, 167 | focusComponent: { 168 | id: 2, 169 | stateful: false, 170 | componentState: [], 171 | title: 'Test', 172 | color: '#E27D60', 173 | props: [], 174 | nextPropId: 1, 175 | childrenArray: [], 176 | nextChildId: 1, 177 | focusChildId: 0, 178 | selectors: [], 179 | actions: [], 180 | }, 181 | selectableChildren: [1], 182 | ancestors: [], 183 | initialApplicationFocusChild: { 184 | childId: 0, 185 | componentName: null, 186 | childType: null, 187 | childSort: 0, 188 | childComponentId: 0, 189 | color: null, 190 | htmlElement: null, 191 | HTMLInfo: null, 192 | }, 193 | focusChild: { 194 | childId: 0, 195 | componentName: null, 196 | childType: null, 197 | childSort: 0, 198 | childComponentId: 0, 199 | color: null, 200 | htmlElement: null, 201 | HTMLInfo: null, 202 | }, 203 | components: [ 204 | { 205 | id: 1, 206 | stateful: false, 207 | componentState: [], 208 | title: 'App', 209 | color: '#FF6D00', 210 | props: [], 211 | nextPropId: 1, 212 | childrenArray: [], 213 | nextChildId: 1, 214 | focusChildId: 0, 215 | selectors: [], 216 | actions: [], 217 | }, 218 | { 219 | id: 2, 220 | stateful: false, 221 | componentState: [], 222 | title: 'Test', 223 | color: '#E27D60', 224 | props: [], 225 | nextPropId: 1, 226 | childrenArray: [], 227 | nextChildId: 1, 228 | focusChildId: 0, 229 | selectors: [], 230 | actions: [], 231 | }, 232 | ], 233 | appDir: '', 234 | loading: false, 235 | storeConfig: { 236 | interfaces: {}, 237 | reducers: {}, 238 | }, 239 | }; 240 | -------------------------------------------------------------------------------- /__mocks__/localStorageMock.ts: -------------------------------------------------------------------------------- 1 | export default new (class { 2 | store = {}; 3 | 4 | setItem = (key, val) => { 5 | this.store[key] = val; 6 | }; 7 | 8 | getItem = key => this.store[key]; 9 | 10 | removeItem = (key) => { 11 | delete this.store[key]; 12 | }; 13 | 14 | clear = () => { 15 | this.store = {}; 16 | }; 17 | })(); 18 | -------------------------------------------------------------------------------- /__tests__/reducerCreateAndDeleteComponentsAndChildren.test.ts: -------------------------------------------------------------------------------- 1 | // import configureMockStore from 'redux-mock-store'; 2 | import componentReducer from '../src/reducers/componentReducer'; 3 | import { 4 | initialAppStateMock, 5 | aboutToAddChildMock, 6 | aboutToDeleteComponentMock, 7 | } from '../__mocks__/appStateMocks'; 8 | import * as types from '../src/actionTypes'; 9 | 10 | describe('initialization stuff', () => { 11 | it('should return the initial state if no recognized action argument is given', () => { 12 | expect(componentReducer(undefined, {})).toEqual(initialAppStateMock); 13 | }); 14 | }); 15 | 16 | describe('addComponent', () => { 17 | it('should handle addComponent with submitted title Test', () => { 18 | const addComponentResult = componentReducer(initialAppStateMock, { 19 | type: types.ADD_COMPONENT, 20 | payload: { title: 'Test' }, 21 | }); 22 | expect(addComponentResult.nextId).toEqual(3); 23 | expect(addComponentResult.focusComponent.id).toEqual(2); 24 | expect(addComponentResult.focusComponent.title).toEqual('Test'); 25 | expect(addComponentResult.components[1].id).toEqual(2); 26 | expect(addComponentResult.components[1].title).toEqual('Test'); 27 | expect(addComponentResult.totalComponents).toEqual(2); 28 | expect(addComponentResult.selectableChildren[0]).toEqual(1); 29 | }); 30 | 31 | it('should take only alpha chars from name and capitalize first letter of each word', () => { 32 | const addComponentResult = componentReducer(initialAppStateMock, { 33 | type: types.ADD_COMPONENT, 34 | payload: { title: '34*&;first;;98

' }, 35 | }); 36 | expect(addComponentResult.nextId).toEqual(3); 37 | expect(addComponentResult.focusComponent.id).toEqual(2); 38 | expect(addComponentResult.focusComponent.title).toEqual('FirstPP'); 39 | expect(addComponentResult.components[1].id).toEqual(2); 40 | expect(addComponentResult.components[1].title).toEqual('FirstPP'); 41 | expect(addComponentResult.totalComponents).toEqual(2); 42 | expect(addComponentResult.selectableChildren[0]).toEqual(1); 43 | }); 44 | 45 | it('should not add a component when submitted title is empty', () => { 46 | const addComponentResult = componentReducer(initialAppStateMock, { 47 | type: types.ADD_COMPONENT, 48 | payload: { title: '' }, 49 | }); 50 | expect(addComponentResult.nextId).toEqual(2); 51 | expect(addComponentResult.focusComponent.id).toEqual(1); 52 | expect(addComponentResult.focusComponent.title).toEqual('App'); 53 | expect(addComponentResult.components[0].id).toEqual(1); 54 | expect(addComponentResult.components[0].title).toEqual('App'); 55 | expect(addComponentResult.totalComponents).toEqual(1); 56 | expect(addComponentResult.selectableChildren).toEqual([]); 57 | }); 58 | 59 | it('should not add a component when a component with that name already exists', () => { 60 | global.alert = jest.fn(); 61 | const addComponentResult = componentReducer(initialAppStateMock, { 62 | type: types.ADD_COMPONENT, 63 | payload: { title: 'App' }, 64 | }); 65 | expect(addComponentResult.nextId).toEqual(2); 66 | expect(addComponentResult.focusComponent.id).toEqual(1); 67 | expect(addComponentResult.focusComponent.title).toEqual('App'); 68 | expect(addComponentResult.components[0].id).toEqual(1); 69 | expect(addComponentResult.components[0].title).toEqual('App'); 70 | expect(addComponentResult.totalComponents).toEqual(1); 71 | expect(addComponentResult.selectableChildren).toEqual([]); 72 | }); 73 | }); 74 | 75 | describe('addChild', () => { 76 | it('should add a component child correctly', () => { 77 | const addChildResult = componentReducer(aboutToAddChildMock, { 78 | type: types.ADD_CHILD, 79 | payload: { title: 'Child', childType: 'COMP', HTMLInfo: {} }, 80 | }); 81 | expect(addChildResult.focusComponent.childrenArray.length).toEqual(1); 82 | expect(addChildResult.focusComponent.focusChildId).toEqual(1); 83 | expect(addChildResult.focusComponent.nextChildId).toEqual(2); 84 | expect(addChildResult.focusChild.childId).toEqual(1); 85 | expect(addChildResult.focusChild.componentName).toEqual('Child'); 86 | expect(addChildResult.focusChild.childType).toEqual('COMP'); 87 | expect(addChildResult.focusChild.childSort).toEqual(1); 88 | expect(addChildResult.focusChild.childComponentId).toEqual(2); 89 | expect(addChildResult.components[1].childrenArray[0].componentName).toEqual('Child'); 90 | }); 91 | 92 | it('should add an HTML child correctly', () => { 93 | const addChildResult = componentReducer(aboutToAddChildMock, { 94 | type: types.ADD_CHILD, 95 | payload: { title: 'Image', childType: 'Image', HTMLInfo: {} }, 96 | }); 97 | expect(addChildResult.focusComponent.childrenArray.length).toEqual(1); 98 | expect(addChildResult.focusComponent.focusChildId).toEqual(1); 99 | expect(addChildResult.focusComponent.nextChildId).toEqual(2); 100 | expect(addChildResult.focusChild.childId).toEqual(1); 101 | expect(addChildResult.focusChild.componentName).toEqual('Image'); 102 | expect(addChildResult.focusChild.childType).toEqual('HTML'); 103 | expect(addChildResult.focusChild.childSort).toEqual(1); 104 | expect(addChildResult.focusChild.childComponentId).toEqual(null); 105 | expect(addChildResult.components[1].childrenArray[0].componentName).toEqual('Image'); 106 | }); 107 | }); 108 | 109 | describe('deleteChild', () => { 110 | it('should delete a component child correctly', () => { 111 | const addChildResult = componentReducer(aboutToAddChildMock, { 112 | type: types.ADD_CHILD, 113 | payload: { title: 'Child', childType: 'COMP', HTMLInfo: {} }, 114 | }); 115 | 116 | const deleteChildResult = componentReducer(addChildResult, { 117 | type: types.DELETE_CHILD, 118 | payload: 2, // this is confusing bc this is actually the component's ID, not its child ID 119 | }); 120 | 121 | expect(deleteChildResult.focusComponent.childrenArray.length).toEqual(0); 122 | expect(deleteChildResult.focusChild.childId).toEqual(0); 123 | expect(deleteChildResult.components[1].childrenArray.length).toEqual(0); 124 | }); 125 | 126 | it('should delete an HTML child correctly', () => { 127 | const addChildResult = componentReducer(aboutToAddChildMock, { 128 | type: types.ADD_CHILD, 129 | payload: { title: 'Image', childType: 'Image', HTMLInfo: {} }, 130 | }); 131 | 132 | const deleteChildResult = componentReducer(addChildResult, { 133 | type: types.DELETE_CHILD, 134 | payload: 1, // this is the child ID of the html element 135 | }); 136 | 137 | expect(deleteChildResult.focusComponent.childrenArray.length).toEqual(0); 138 | expect(deleteChildResult.focusChild.childId).toEqual(0); 139 | expect(deleteChildResult.components[1].childrenArray.length).toEqual(0); 140 | }); 141 | 142 | it('should delete instances of a component as a child when the component has been deleted (uses alt form of action payload)', () => { 143 | const addChildResult = componentReducer(aboutToAddChildMock, { 144 | type: types.ADD_CHILD, 145 | payload: { title: 'Child', childType: 'COMP', HTMLInfo: {} }, 146 | }); 147 | 148 | const deleteChildResult = componentReducer(addChildResult, { 149 | type: types.DELETE_CHILD, 150 | payload: { 151 | parentId: 1, 152 | childId: 1, 153 | calledFromDeleteComponent: true, 154 | }, 155 | }); 156 | 157 | expect(deleteChildResult.focusComponent.childrenArray.length).toEqual(0); 158 | expect(deleteChildResult.focusChild.childId).toEqual(0); 159 | expect(deleteChildResult.components[1].childrenArray.length).toEqual(0); 160 | }); 161 | }); 162 | 163 | describe('deleteComponent', () => { 164 | it('should delete a component correctly', () => { 165 | const deleteComponentResult = componentReducer(aboutToDeleteComponentMock, { 166 | type: types.DELETE_COMPONENT, 167 | payload: { 168 | componentId: 2, 169 | }, 170 | }); 171 | expect(deleteComponentResult.totalComponents).toEqual(1); 172 | expect(deleteComponentResult.components.length).toEqual(1); 173 | expect(deleteComponentResult.components[0].title).toEqual('App'); 174 | }); 175 | 176 | it('App component is not deletable', () => { 177 | const deleteComponentResult = componentReducer(aboutToDeleteComponentMock, { 178 | type: types.DELETE_COMPONENT, 179 | payload: { 180 | componentId: 1, 181 | }, 182 | }); 183 | expect(deleteComponentResult.totalComponents).toEqual(2); 184 | expect(deleteComponentResult.components.length).toEqual(2); 185 | expect(deleteComponentResult.components[0].title).toEqual('App'); 186 | }); 187 | }); 188 | -------------------------------------------------------------------------------- /images/actions.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/preducks/1ef20cbca94cfebdc62ef99a86292269a7275052/images/actions.PNG -------------------------------------------------------------------------------- /images/addordeletecomponent.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/preducks/1ef20cbca94cfebdc62ef99a86292269a7275052/images/addordeletecomponent.PNG -------------------------------------------------------------------------------- /images/addreduxconn.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/preducks/1ef20cbca94cfebdc62ef99a86292269a7275052/images/addreduxconn.PNG -------------------------------------------------------------------------------- /images/createcomponent.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/preducks/1ef20cbca94cfebdc62ef99a86292269a7275052/images/createcomponent.PNG -------------------------------------------------------------------------------- /images/createinterface.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/preducks/1ef20cbca94cfebdc62ef99a86292269a7275052/images/createinterface.PNG -------------------------------------------------------------------------------- /images/deletecomponent.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/preducks/1ef20cbca94cfebdc62ef99a86292269a7275052/images/deletecomponent.PNG -------------------------------------------------------------------------------- /images/export.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/preducks/1ef20cbca94cfebdc62ef99a86292269a7275052/images/export.PNG -------------------------------------------------------------------------------- /images/household.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/preducks/1ef20cbca94cfebdc62ef99a86292269a7275052/images/household.PNG -------------------------------------------------------------------------------- /images/interfacefields.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/preducks/1ef20cbca94cfebdc62ef99a86292269a7275052/images/interfacefields.PNG -------------------------------------------------------------------------------- /images/store.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/preducks/1ef20cbca94cfebdc62ef99a86292269a7275052/images/store.PNG -------------------------------------------------------------------------------- /images/storeselections.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/preducks/1ef20cbca94cfebdc62ef99a86292269a7275052/images/storeselections.PNG -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: [''], 3 | globals: { 4 | 'ts-jest': { 5 | diagnostics: false, 6 | }, 7 | }, 8 | transform: { 9 | '^.+\\.jsx?$': 'babel-jest', 10 | '^.+\\.tsx?$': 'ts-jest', 11 | }, 12 | testMatch: ['**/__tests__/**/*.[jt]s?(x)', '**/?(*.)+(spec|test).[jt]s?(x)'], 13 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], 14 | moduleNameMapper: { 15 | '^.+\\.(css|scss|styl|less|sass|scss|png|jpg|ttf|woff|woff2)$': 'identity-obj-proxy', 16 | electron: '/__mocks__/electron.js', 17 | }, 18 | snapshotSerializers: ['enzyme-to-json/serializer'], 19 | }; 20 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const isElectron = require('is-electron'); 3 | 4 | if (isElectron()) { 5 | const { 6 | app, BrowserWindow, Menu, shell, dialog, ipcMain, 7 | } = require('electron'); 8 | 9 | // Uncomment below for hot reloading during development 10 | // require('electron-reload')(__dirname); 11 | 12 | // const isDev = true; 13 | const isDev = process.env.NODE_ENV === 'development'; 14 | 15 | // Keep a global reference of the window object, if you don't, the window will 16 | // be closed automatically when the JavaScript object is garbage collected. 17 | let mainWindow; 18 | 19 | // Open image file 20 | const openFile = () => { 21 | // Opens file dialog looking for markdown 22 | const files = dialog.showOpenDialog(mainWindow, { 23 | properties: ['openFile'], 24 | filters: [ 25 | { 26 | name: 'Images', 27 | extensions: ['jpeg', 'jpg', 'png', 'gif', 'pdf'], 28 | }, 29 | ], 30 | }); 31 | 32 | // if no files 33 | if (!files) return; 34 | const file = files[0]; 35 | 36 | // Send fileContent to renderer 37 | mainWindow.webContents.send('new-file', file); 38 | }; 39 | 40 | // Choose directory 41 | ipcMain.on('choose_app_dir', (event) => { 42 | const directory = dialog.showOpenDialog(mainWindow, { 43 | properties: ['openDirectory'], 44 | buttonLabel: 'Export', 45 | }); 46 | 47 | if (!directory) return; 48 | event.sender.send('app_dir_selected', directory[0]); 49 | }); 50 | 51 | ipcMain.on('view_app_dir', (event, appDir) => { 52 | shell.openItem(appDir); 53 | }); 54 | 55 | // Update file 56 | ipcMain.on('update-file', () => { 57 | openFile(); 58 | }); 59 | 60 | const createWindow = () => { 61 | // Create the browser window. 62 | // eslint-disable-next-line 63 | const { width, height } = require('electron').screen.getPrimaryDisplay().size; 64 | mainWindow = new BrowserWindow({ 65 | width, 66 | height, 67 | minWidth: 790, 68 | minHeight: 420, 69 | webPreferences: { 70 | zoomFactor: 0.7, 71 | nodeIntegration: true, 72 | webSecurity: false, 73 | scrollBounce: true, 74 | }, 75 | show: false, 76 | backgroundColor: '#333', 77 | titleBarStyle: 'hidden', 78 | icon: path.join(__dirname, '/src/public/icons/mac/icon.icns'), 79 | win: { 80 | icon: path.join(__dirname, '/src/public/icons/win/icon.ico'), 81 | target: ['portable'], 82 | }, 83 | }); 84 | 85 | // and load the index.html of the app. 86 | mainWindow.loadURL(`file://${__dirname}/build/index.html`); 87 | // load page once window is loaded 88 | mainWindow.once('ready-to-show', () => { 89 | mainWindow.show(); 90 | }); 91 | 92 | const template = [ 93 | { 94 | label: 'File', 95 | submenu: [ 96 | { 97 | label: 'Open File', 98 | accelerator: process.platform === 'darwin' ? 'Cmd+O' : 'Ctrl+Shift+O', 99 | click() { 100 | openFile(); 101 | }, 102 | }, 103 | ], 104 | }, 105 | { 106 | label: 'Edit', 107 | submenu: [ 108 | // { role: 'undo' }, 109 | // { role: 'redo' }, 110 | // { type: 'separator' }, 111 | { role: 'cut' }, 112 | { role: 'copy' }, 113 | { role: 'paste' }, 114 | { role: 'pasteandmatchstyle' }, 115 | { role: 'delete' }, 116 | { role: 'selectall' }, 117 | ], 118 | }, 119 | { 120 | label: 'View', 121 | submenu: [ 122 | { role: 'reload' }, 123 | { role: 'forcereload' }, 124 | { type: 'separator' }, 125 | { role: 'resetzoom' }, 126 | { role: 'zoomin' }, 127 | { role: 'zoomout' }, 128 | { type: 'separator' }, 129 | { role: 'togglefullscreen' }, 130 | ], 131 | }, 132 | { 133 | role: 'window', 134 | submenu: [{ role: 'minimize' }, { role: 'close' }], 135 | }, 136 | { 137 | role: 'help', 138 | submenu: [ 139 | { 140 | label: 'Learn More', 141 | click() { 142 | shell.openExternal('https://electronjs.org'); 143 | }, 144 | }, 145 | ], 146 | }, 147 | { 148 | label: 'Developer', 149 | submenu: [ 150 | { 151 | label: 'Toggle Developer Tools', 152 | accelerator: process.platform === 'darwin' ? 'Alt+Command+I' : 'Ctrl+Shift+I', 153 | click() { 154 | mainWindow.webContents.toggleDevTools(); 155 | }, 156 | }, 157 | ], 158 | }, 159 | ]; 160 | 161 | if (process.platform === 'darwin') { 162 | template.unshift({ 163 | label: app.getName(), 164 | submenu: [ 165 | { role: 'about' }, 166 | { type: 'separator' }, 167 | { role: 'services', submenu: [] }, 168 | { type: 'separator' }, 169 | { role: 'hide' }, 170 | { role: 'hideothers' }, 171 | { role: 'unhide' }, 172 | { type: 'separator' }, 173 | { role: 'quit' }, 174 | ], 175 | }); 176 | 177 | // Edit menu 178 | // template[2].submenu.push( 179 | // { type: 'separator' }, 180 | // { label: 'Speech', submenu: [{ role: 'startspeaking' }, { role: 'stopspeaking' }] }, 181 | // ); 182 | 183 | // Window menu 184 | template[4].submenu = [ 185 | { role: 'close' }, 186 | { role: 'minimize' }, 187 | { role: 'zoom' }, 188 | { type: 'separator' }, 189 | { role: 'front' }, 190 | ]; 191 | } 192 | 193 | const menu = Menu.buildFromTemplate(template); 194 | Menu.setApplicationMenu(menu); 195 | 196 | // Emitted when the window is closed. 197 | mainWindow.on('closed', () => { 198 | // Dereference the window object, usually you would store windows 199 | // in an array if your app supports multi windows, this is the time 200 | // when you should delete the corresponding element. 201 | mainWindow = null; 202 | }); 203 | }; 204 | 205 | // This method will be called when Electron has finished 206 | // initialization and is ready to create browser windows. 207 | // Some APIs can only be used after this event occurs. 208 | app.on('ready', () => { 209 | if (isDev) { 210 | const { 211 | default: installExtension, 212 | REACT_DEVELOPER_TOOLS, 213 | REDUX_DEVTOOLS, 214 | } = require('electron-devtools-installer'); 215 | 216 | installExtension([REACT_DEVELOPER_TOOLS, REDUX_DEVTOOLS]) 217 | .then(() => { 218 | createWindow(); 219 | }) 220 | .catch(err => err); 221 | } else { 222 | createWindow(); 223 | } 224 | }); 225 | 226 | // Quit when all windows are closed. 227 | app.on('window-all-closed', () => { 228 | // On OS X it is common for applications and their menu bar 229 | // to stay active until the user quits explicitly with Cmd + Q 230 | if (process.platform !== 'darwin') { 231 | app.quit(); 232 | } 233 | }); 234 | 235 | app.on('activate', () => { 236 | // On OS X it's common to re-create a window in the app when the 237 | // dock icon is clicked and there are no other windows open. 238 | if (mainWindow === null) { 239 | createWindow(); 240 | } 241 | }); 242 | } 243 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "preducks", 3 | "version": "", 4 | "description": "prototyping tool for react + redux + typescript applications", 5 | "author": "preducks", 6 | "main": "main.js", 7 | "contributors": [ 8 | "jacob richards", 9 | "max gonzalez", 10 | "will napier" 11 | ], 12 | "engines": { 13 | "node": "12.x" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/oslabs-beta/preducks" 18 | }, 19 | "build": { 20 | "appId": "com.team-preducks.preducks", 21 | "copyright": "Copyright © 2019", 22 | "linux": { 23 | "target": [ 24 | "AppImage", 25 | "deb" 26 | ], 27 | "maintainer": "sjaco" 28 | }, 29 | "mac": { 30 | "category": "public.app-category.developer-tools", 31 | "target": "dmg" 32 | }, 33 | "nsis": { 34 | "createStartMenuShortcut": true, 35 | "createDesktopShortcut": true, 36 | "runAfterFinish": true 37 | }, 38 | "win": { 39 | "target": "nsis-web" 40 | }, 41 | "files": [ 42 | "main.js", 43 | "build/electron" 44 | ], 45 | "productName": "preducks", 46 | "dmg": { 47 | "contents": [ 48 | { 49 | "x": 110, 50 | "y": 150 51 | }, 52 | { 53 | "x": 240, 54 | "y": 150, 55 | "type": "link", 56 | "path": "/Applications" 57 | } 58 | ] 59 | } 60 | }, 61 | "scripts": { 62 | "prestart": "cross-env NODE_ENV=production webpack --config webpack.prod.js", 63 | "start": "cross-env NODE_ENV=production", 64 | "dev-electron": "cross-env NODE_ENV=development webpack --target electron-main --open --config webpack.dev.js --watch & cross-env NODE_ENV=development electron .", 65 | "build-electron": "rm -rf ./build && cross-env NODE_ENV=production webpack --target electron-renderer --config webpack.prod.js", 66 | "build-electron-bin": "electron-builder -mwl", 67 | "dev-web": "cross-env NODE_ENV=development webpack-dev-server --target web --open --config webpack.dev.js --watch", 68 | "build-web": "rm -rf ./build && cross-env NODE_ENV=production webpack --target web --config webpack.prod.js", 69 | "test": "cross-env NODE_ENV=test jest --verbose --runInBand", 70 | "refresh": "rm -rf ./node_modules ./build && npm install" 71 | }, 72 | "bin": { 73 | "preducks": "./src/index.js" 74 | }, 75 | "preferGlobal": true, 76 | "license": "MIT", 77 | "dependencies": { 78 | "@material-ui/core": "^3.9.3", 79 | "@material-ui/icons": "^2.0.0", 80 | "@types/react": "^16.8.14", 81 | "@types/react-dom": "^16.8.4", 82 | "@types/react-redux": "^7.0.8", 83 | "babel-jest": "^24.8.0", 84 | "babel-polyfill": "^6.26.0", 85 | "clean-webpack-plugin": "^3.0.0", 86 | "copy-webpack-plugin": "^5.0.3", 87 | "d3": "^5.9.7", 88 | "extract-text-webpack-plugin": "^4.0.0-beta.0", 89 | "file-saver": "^2.0.2", 90 | "html-webpack-plugin": "^3.1.0", 91 | "is-electron": "^2.2.0", 92 | "jszip": "^3.2.2", 93 | "localforage": "^1.7.2", 94 | "lodash.throttle": "^4.1.1", 95 | "minimist": "^1.2.0", 96 | "prettier": "^1.18.2", 97 | "prop-types": "^15.6.2", 98 | "react": "^16.8.6", 99 | "react-d3-tree": "^1.13.0", 100 | "react-dom": "^16.8.6", 101 | "react-redux": "^7.1.0", 102 | "react-syntax-highlighter": "^11.0.1", 103 | "redux": "^4.0.1", 104 | "redux-devtools-extension": "^2.13.8", 105 | "redux-thunk": "^2.3.0", 106 | "webpack": "^4.35.2", 107 | "webpack-merge": "^4.2.1", 108 | "webpack-target-electron-renderer": "^0.4.0" 109 | }, 110 | "devDependencies": { 111 | "electron": "^5.0.8", 112 | "electron-devtools-installer": "^2.2.4", 113 | "electron-prebuilt": "^1.4.13", 114 | "@babel/preset-typescript": "^7.3.3", 115 | "@types/enzyme": "^3.10.2", 116 | "@types/jest": "^24.0.15", 117 | "@typescript-eslint/parser": "^1.11.0", 118 | "babel-core": "^6.26.3", 119 | "babel-eslint": "^8.2.6", 120 | "babel-loader": "^7.1.4", 121 | "babel-preset-env": "^1.6.1", 122 | "babel-preset-react": "^6.24.1", 123 | "babel-preset-stage-0": "^6.24.1", 124 | "cross-env": "^5.2.0", 125 | "css-loader": "^0.28.11", 126 | "electron-builder": "^21.1.5", 127 | "electron-installer-dmg": "^3.0.0", 128 | "electron-reload": "^1.4.1", 129 | "enzyme": "^3.10.0", 130 | "enzyme-adapter-react-16": "^1.14.0", 131 | "enzyme-to-json": "^3.3.5", 132 | "eslint": "^4.19.1", 133 | "eslint-config-airbnb-base": "^13.0.0", 134 | "eslint-plugin-babel": "^5.1.0", 135 | "eslint-plugin-import": "^2.13.0", 136 | "eslint-plugin-jest": "^21.21.0", 137 | "eslint-plugin-jsx-a11y": "^6.1.1", 138 | "eslint-plugin-react": "^7.10.0", 139 | "identity-obj-proxy": "^3.0.0", 140 | "jest": "^23.6.0", 141 | "ts-jest": "^24.0.2", 142 | "typescript": "^3.4.4", 143 | "webpack-cli": "^3.3.5", 144 | "webpack-dev-server": "^3.7.2" 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /preducks-logo-text.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Artboard 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /preducks-logo-text@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/preducks/1ef20cbca94cfebdc62ef99a86292269a7275052/preducks-logo-text@2x.png -------------------------------------------------------------------------------- /src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/preducks/1ef20cbca94cfebdc62ef99a86292269a7275052/src/.DS_Store -------------------------------------------------------------------------------- /src/actionTypes/index.js: -------------------------------------------------------------------------------- 1 | // add new action types ADD_SELECTOR, DELETE_SELECTOR, ADD_ACTION, DELETE_ACTION 2 | 3 | export const LOAD_INIT_DATA = 'LOAD_INIT_DATA'; 4 | export const ADD_COMPONENT = 'ADD_COMPONENT'; 5 | export const ADD_CHILD = 'ADD_CHILD'; 6 | export const DELETE_CHILD = 'DELETE_CHILD'; 7 | export const UPDATE_COMPONENT = 'UPDATE_COMPONENT'; 8 | export const DELETE_COMPONENT = 'DELETE_COMPONENT'; 9 | export const CHANGE_FOCUS_COMPONENT = 'CHANGE_FOCUS_COMPONENT'; 10 | export const CHANGE_COMPONENT_FOCUS_CHILD = 'CHANGE_COMPONENT_FOCUS_CHILD'; 11 | export const CHANGE_FOCUS_CHILD = 'CHANGE_FOCUS_CHILD'; 12 | export const UPDATE_CHILDREN = 'UPDATE_CHILDREN'; 13 | export const REASSIGN_PARENT = 'REASSIGN_PARENT'; 14 | export const SET_SELECTABLE_PARENTS = 'SET_SELECTABLE_PARENTS'; 15 | export const EXPORT_FILES = 'EXPORT_FILES'; 16 | export const EXPORT_FILES_SUCCESS = 'EXPORT_FILES_SUCCESS'; 17 | export const EXPORT_FILES_ERROR = 'EXPORT_FILES_ERROR'; 18 | export const HANDLE_CLOSE = 'HANDLE_CLOSE'; 19 | export const HANDLE_TRANSFORM = 'HANDLE_TRANSFORM'; 20 | export const CREATE_APPLICATION = 'CREATE_APPLICATION'; 21 | export const CREATE_APPLICATION_SUCCESS = 'CREATE_APPLICATION_SUCCESS'; 22 | export const CREATE_APPLICATION_ERROR = 'CREATE_APPLICATION_ERROR'; 23 | export const MOVE_TO_BOTTOM = 'MOVE_TO_BOTTOM'; 24 | export const MOVE_TO_TOP = 'MOVE_TO_TOP'; 25 | export const OPEN_EXPANSION_PANEL = 'OPEN_EXPANSION_PANEL'; 26 | export const DELETE_PROP = 'DELETE_PROP'; 27 | export const ADD_PROP = 'ADD_PROP'; 28 | export const DELETE_ALL_DATA = 'DELETE_ALL_DATA'; 29 | export const CHANGE_IMAGE_PATH = 'CHANGE_IMAGE_PATH'; 30 | export const UPDATE_HTML_ATTR = 'UPDATE_HTML_ATTR'; 31 | export const UPDATE_CHILDREN_SORT = 'UPDATE_CHILDREN_SORT'; 32 | export const ADD_SELECTOR = 'ADD_SELECTOR'; 33 | export const DELETE_SELECTOR = 'DELETE_SELECTOR'; 34 | export const ADD_ACTION_TO_COMPONENT = 'ADD_ACTION_TO_COMPONENT'; 35 | export const DELETE_ACTION_FROM_COMPONENT = 'DELETE_ACTION_FROM_COMPONENT'; 36 | export const SET_REDUCER = 'SET_REDUCER'; 37 | export const DELETE_REDUCER = 'DELETE_REDUCER'; 38 | // export const RENAME_REDUCER = 'RENAME_REDUCER'; 39 | export const SET_INTERFACE = 'SET_INTERFACE'; 40 | export const DELETE_INTERFACE = 'DELETE_INTERFACE'; 41 | // export const RENAME_INTERFACE = 'RENAME_INTERFACE'; 42 | export const SET_STATE = 'SET_STATE'; 43 | export const DELETE_STATE = 'DELETE_STATE'; 44 | // export const RENAME_STATE = 'RENAME_STATE'; 45 | -------------------------------------------------------------------------------- /src/actions/components.ts: -------------------------------------------------------------------------------- 1 | import JSZip from 'jszip'; 2 | import FileSaver from 'file-saver'; 3 | import { 4 | ComponentInt, 5 | ComponentsInt, 6 | PropInt, 7 | ChildInt, 8 | InterfacesInterface, 9 | ReducersInterface, 10 | StoreConfigInterface, 11 | ComponentStateInterface, 12 | } from '../utils/InterfaceDefinitions'; 13 | import { 14 | LOAD_INIT_DATA, 15 | ADD_COMPONENT, 16 | ADD_CHILD, 17 | DELETE_CHILD, 18 | DELETE_COMPONENT, 19 | CHANGE_FOCUS_COMPONENT, 20 | CHANGE_FOCUS_CHILD, 21 | CHANGE_COMPONENT_FOCUS_CHILD, 22 | EXPORT_FILES, 23 | EXPORT_FILES_SUCCESS, 24 | EXPORT_FILES_ERROR, 25 | HANDLE_CLOSE, 26 | HANDLE_TRANSFORM, 27 | CREATE_APPLICATION, 28 | CREATE_APPLICATION_SUCCESS, 29 | CREATE_APPLICATION_ERROR, 30 | OPEN_EXPANSION_PANEL, 31 | DELETE_PROP, 32 | ADD_PROP, 33 | DELETE_ALL_DATA, 34 | UPDATE_HTML_ATTR, 35 | UPDATE_CHILDREN_SORT, 36 | ADD_SELECTOR, 37 | DELETE_SELECTOR, 38 | ADD_ACTION_TO_COMPONENT, 39 | DELETE_ACTION_FROM_COMPONENT, 40 | SET_REDUCER, 41 | DELETE_REDUCER, 42 | // RENAME_REDUCER, 43 | SET_INTERFACE, 44 | DELETE_INTERFACE, 45 | // RENAME_INTERFACE, 46 | SET_STATE, 47 | DELETE_STATE, 48 | // RENAME_STATE, 49 | } from '../actionTypes/index.js'; 50 | 51 | import { loadState } from '../localStorage'; 52 | import createComponentFiles from '../utils/createComponentFiles.util'; 53 | import createApplicationUtil from '../utils/createApplication.util'; 54 | 55 | const zip = new JSZip(); 56 | 57 | export const loadInitData = () => (dispatch: any) => { 58 | loadState().then((data: any) => dispatch({ 59 | type: LOAD_INIT_DATA, 60 | payload: { 61 | data: data ? data.workspace : {}, 62 | }, 63 | })); 64 | }; 65 | 66 | export const addComponent = ({ title }: { title: string }) => (dispatch: any) => { 67 | dispatch({ type: ADD_COMPONENT, payload: { title } }); 68 | }; 69 | 70 | export const addChild = ({ 71 | title, 72 | childType, 73 | HTMLInfo, 74 | }: { 75 | title: string; 76 | childType: string; 77 | HTMLInfo: object; 78 | }) => (dispatch: any) => { 79 | dispatch({ type: ADD_CHILD, payload: { title, childType, HTMLInfo } }); 80 | }; 81 | 82 | export const deleteChild = (childToDeleteId?: number) => (dispatch: any) => { 83 | // with no payload, it will delete focusd child 84 | dispatch({ type: DELETE_CHILD, payload: childToDeleteId }); 85 | }; 86 | 87 | export const deleteComponent = ({ 88 | componentId, 89 | stateComponents, 90 | }: { 91 | componentId: number; 92 | stateComponents: ComponentsInt; 93 | }) => (dispatch: any) => { 94 | // find all places where the "to be deleted" is a child and do what u gotta do 95 | stateComponents.forEach((parent: ComponentInt) => { 96 | parent.childrenArray 97 | .filter((child: ChildInt) => child.childComponentId === componentId) 98 | .forEach((child: ChildInt) => { 99 | dispatch({ 100 | type: DELETE_CHILD, 101 | payload: { 102 | parentId: parent.id, 103 | childId: child.childId, 104 | calledFromDeleteComponent: true, 105 | }, 106 | }); 107 | }); 108 | }); 109 | 110 | // change focus to app 111 | dispatch({ type: CHANGE_FOCUS_COMPONENT, payload: { title: 'App' } }); 112 | // after taking care of the children delete the component 113 | dispatch({ type: DELETE_COMPONENT, payload: { componentId } }); 114 | }; 115 | 116 | export const changeFocusComponent = ({ title }: { title: string }) => (dispatch: any) => { 117 | dispatch({ type: CHANGE_FOCUS_COMPONENT, payload: { title } }); 118 | }; 119 | 120 | // make sure childId is being sent in 121 | export const changeFocusChild = ({ childId }: { childId: number }) => (dispatch: any) => { 122 | dispatch({ type: CHANGE_FOCUS_CHILD, payload: { childId } }); 123 | }; 124 | 125 | export const changeComponentFocusChild = ({ 126 | componentId, 127 | childId, 128 | }: { 129 | componentId: number; 130 | childId: number; 131 | }) => (dispatch: any) => { 132 | dispatch({ 133 | type: CHANGE_COMPONENT_FOCUS_CHILD, 134 | payload: { componentId, childId }, 135 | }); 136 | }; 137 | 138 | export const exportFiles = ({ 139 | components, 140 | path, 141 | appName, 142 | exportAppBool, 143 | }: { 144 | components: ComponentsInt; 145 | path: string; 146 | appName: string; 147 | exportAppBool: boolean; 148 | }) => (dispatch: any) => { 149 | // this dispatch sets the global state property 'loading' to true until the createComponentFiles call resolves below 150 | // dispatch({ 151 | // type: EXPORT_FILES, 152 | // }); 153 | 154 | const dir = createComponentFiles(components, path, appName, exportAppBool, zip); 155 | dispatch({ 156 | type: EXPORT_FILES_SUCCESS, 157 | payload: { status: true, dir: dir[0] }, 158 | }); 159 | zip.generateAsync({type: "blob"}).then(blob => { 160 | FileSaver.saveAs(blob, "preducksApp.zip"); 161 | }, function (err) { 162 | console.log(err); 163 | }); 164 | 165 | }; 166 | 167 | export const handleClose = () => ({ 168 | type: HANDLE_CLOSE, 169 | payload: false, 170 | }); 171 | 172 | export const handleTransform = ( 173 | componentId: number, 174 | childId: number, 175 | { 176 | x, y, width, height, 177 | }: { x: number; y: number; width: number; height: number }, 178 | ) => ({ 179 | type: HANDLE_TRANSFORM, 180 | payload: { 181 | componentId, 182 | childId, 183 | x, 184 | y, 185 | width, 186 | height, 187 | }, 188 | }); 189 | 190 | export const createApplication = ({ 191 | path, 192 | components = [], 193 | genOption, 194 | appName = 'dope_exported_preducks_app', 195 | exportAppBool, 196 | storeConfig, 197 | }: { 198 | path: string; 199 | components: ComponentsInt; 200 | genOption: number; 201 | appName: string; 202 | exportAppBool: boolean; 203 | storeConfig: StoreConfigInterface; 204 | }) => (dispatch: any) => { 205 | if (genOption) { 206 | exportAppBool = true; 207 | dispatch({ 208 | type: CREATE_APPLICATION, 209 | }); 210 | createApplicationUtil({ 211 | path, 212 | appName, 213 | genOption, 214 | storeConfig, 215 | zip 216 | }) 217 | .then(() => { 218 | dispatch({ 219 | type: CREATE_APPLICATION_SUCCESS, 220 | }); 221 | dispatch( 222 | exportFiles({ 223 | appName, 224 | path, 225 | components, 226 | exportAppBool, 227 | }), 228 | ); 229 | }) 230 | .catch(err => dispatch({ 231 | type: CREATE_APPLICATION_ERROR, 232 | payload: { status: true, err }, 233 | })); 234 | } 235 | }; 236 | 237 | export const openExpansionPanel = (component: ComponentInt) => ({ 238 | type: OPEN_EXPANSION_PANEL, 239 | payload: { component }, 240 | }); 241 | 242 | export const deleteAllData = () => ({ 243 | type: DELETE_ALL_DATA, 244 | }); 245 | 246 | export const deleteProp = (propId: number) => (dispatch: any) => { 247 | dispatch({ type: DELETE_PROP, payload: propId }); 248 | }; 249 | 250 | export const addProp = (prop: PropInt) => ({ 251 | type: ADD_PROP, 252 | payload: { ...prop }, 253 | }); 254 | 255 | export const updateHtmlAttr = ({ attr, value }: { attr: string; value: string }) => ( 256 | dispatch: any, 257 | ) => { 258 | dispatch({ 259 | type: UPDATE_HTML_ATTR, 260 | payload: { attr, value }, 261 | }); 262 | }; 263 | 264 | export const updateChildrenSort = ({ newSortValues }: { newSortValues: any }) => ( 265 | dispatch: any, 266 | ) => { 267 | dispatch({ 268 | type: UPDATE_CHILDREN_SORT, 269 | payload: { newSortValues }, 270 | }); 271 | }; 272 | 273 | export const addSelector = (name: string) => ({ 274 | type: ADD_SELECTOR, 275 | payload: name, 276 | }); 277 | export const deleteSelector = (name: string) => ({ 278 | type: DELETE_SELECTOR, 279 | payload: name, 280 | }); 281 | 282 | export const addActionToComponent = (name: string) => ({ 283 | type: ADD_ACTION_TO_COMPONENT, 284 | payload: name, 285 | }); 286 | 287 | export const deleteActionFromComponent = (name: string) => ({ 288 | type: DELETE_ACTION_FROM_COMPONENT, 289 | payload: name, 290 | }); 291 | 292 | export const setReducer = (reducer: ReducersInterface) => ({ 293 | type: SET_REDUCER, 294 | payload: reducer, 295 | }); 296 | 297 | export const deleteReducer = (name: string) => ({ 298 | type: DELETE_REDUCER, 299 | payload: name, 300 | }); 301 | 302 | export const setInterface = (userInterface: InterfacesInterface) => ({ 303 | type: SET_INTERFACE, 304 | payload: userInterface, 305 | }); 306 | 307 | export const deleteInterface = (name: string) => ({ 308 | type: DELETE_INTERFACE, 309 | payload: name, 310 | }); 311 | 312 | export const setState = (state: ComponentStateInterface) => ({ 313 | type: SET_STATE, 314 | payload: state, 315 | }); 316 | 317 | export const deleteState = (name: string) => ({ 318 | type: DELETE_STATE, 319 | payload: name, 320 | }); 321 | 322 | -------------------------------------------------------------------------------- /src/components/Actions.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import Checkbox from '@material-ui/core/Checkbox'; 3 | import FormControlLabel from '@material-ui/core/FormControlLabel'; 4 | import Icon from '@material-ui/core/Icon'; 5 | import IconButton from '@material-ui/core/IconButton'; 6 | import TextField from '@material-ui/core/TextField'; 7 | import Tooltip from '@material-ui/core/Tooltip'; 8 | import TypeSelect from './TypeSelect'; 9 | import validateInput from '../utils/validateInput.util'; 10 | import ErrorMessage from './ErrorMessage'; 11 | 12 | const Actions = (props: any) => { 13 | const { 14 | reducer, reducers, interfaces, setReducer, 15 | } = props; 16 | const [propertyName, setPropertyName] = useState(''); 17 | const [propertyIsAsync, setPropertyIsAsync] = useState(false); 18 | const [parameterName, setParameterName] = useState(''); 19 | const [parameterType, setParameterType] = useState(''); 20 | const [parameterIsArray, setParameterIsArray] = useState(false); 21 | const [payloadType, setPayloadType] = useState(''); 22 | const [payloadIsArray, setPayloadIsArray] = useState(false); 23 | 24 | const [nameValidation, setNameValidation] = useState(validateInput('')); 25 | const [nameIsVisible, setNameVisibility] = useState(false); 26 | const [parameterNameValidation, setParameterNameValidation] = useState(validateInput('')); 27 | const [parameterNameIsVisible, setParameterNameVisiblility] = useState(false); 28 | 29 | const handleChange = (event: Event, setter: any, setValidation: any = '') => { 30 | const target: any = event.target; 31 | setter(target.type === 'checkbox' ? target.checked : target.value); 32 | if (setValidation !== '') { 33 | const result = validateInput(target.value); 34 | setValidation(result); 35 | } 36 | }; 37 | 38 | const addProperty = () => { 39 | if (nameValidation.isValid && parameterNameValidation.isValid && parameterType && payloadType) { 40 | const updatedReducer = reducers[reducer]; 41 | updatedReducer.actions[nameValidation.input] = { 42 | parameter: { 43 | name: parameterNameValidation.input, 44 | type: parameterType, 45 | array: parameterIsArray, 46 | }, 47 | payload: { 48 | type: payloadType, 49 | array: payloadIsArray, 50 | }, 51 | async: propertyIsAsync, 52 | }; 53 | setReducer({ [reducer]: updatedReducer }); 54 | setPropertyName(''); 55 | setPropertyIsAsync(false); 56 | setParameterName(''); 57 | setParameterType(''); 58 | setParameterIsArray(false); 59 | setPayloadType(''); 60 | setPayloadIsArray(false); 61 | 62 | setNameVisibility(false); 63 | setParameterNameVisiblility(false); 64 | } else { 65 | setNameVisibility(true); 66 | setParameterNameVisiblility(true); 67 | } 68 | }; 69 | 70 | const deleteProperty = (property: string) => { 71 | const updatedReducer = reducers[reducer]; 72 | delete updatedReducer.actions[property]; 73 | setReducer({ [reducer]: updatedReducer }); 74 | }; 75 | 76 | return ( 77 |
78 | 82 |

Actions

83 |
84 |
85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | {reducers[reducer].actions 98 | && Object.keys(reducers[reducer].actions).map(action => ( 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 114 | 115 | ))} 116 | 117 |
nameasyncparameterparameter typeparameter arraypayload typepayload array
{action}{reducers[reducer].actions[action].async ? '✓' : '×'}{reducers[reducer].actions[action].parameter.name}{reducers[reducer].actions[action].parameter.type}{reducers[reducer].actions[action].parameter.array ? '✓' : '×'}{reducers[reducer].actions[action].payload.type}{reducers[reducer].actions[action].payload.array ? '✓' : '×'} 108 | deleteProperty(action)}> 111 | delete 112 | 113 |
118 |
119 |
120 | 121 |
122 | { 126 | handleChange(event, setPropertyName, setNameValidation); 127 | }} 128 | required 129 | /> 130 | { 135 | handleChange(event, setPropertyIsAsync); 136 | }} 137 | /> 138 | } 139 | label="async" 140 | /> 141 |
142 | 143 | Parameter 144 |
145 | { 149 | handleChange(event, setParameterName, setParameterNameValidation); 150 | }} 151 | required 152 | /> 153 | { 159 | handleChange(event, setParameterType); 160 | }} 161 | required 162 | /> 163 | { 168 | handleChange(event, setParameterIsArray); 169 | }} 170 | /> 171 | } 172 | label="array" 173 | /> 174 |
175 | 176 | Payload 177 |
178 | { 184 | handleChange(event, setPayloadType); 185 | }} 186 | /> 187 | { 192 | handleChange(event, setPayloadIsArray); 193 | }} 194 | /> 195 | } 196 | label="array" 197 | /> 198 |
199 | 210 | add 211 | 212 | 213 |
214 | ); 215 | }; 216 | 217 | export default Actions; 218 | -------------------------------------------------------------------------------- /src/components/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import '../public/styles/index.css'; 3 | import AppContainer from '../containers/AppContainer'; 4 | 5 | export const App: React.SFC = () => ; 6 | 7 | export default App; 8 | -------------------------------------------------------------------------------- /src/components/BottomPanel.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { handleClose } from '../actions/components'; 4 | import BottomTabs from './BottomTabs'; 5 | import { ComponentInt, ComponentsInt, ChildInt } from '../utils/InterfaceDefinitions'; 6 | 7 | const mapDispatchToProps = (dispatch: any) => ({ 8 | handleNotificationClose: () => dispatch(handleClose()), 9 | }); 10 | 11 | const mapStateToProps = (store: any) => ({ 12 | focusChild: store.workspace.focusChild, 13 | components: store.workspace.components, 14 | }); 15 | 16 | interface PropsInt { 17 | focusChild: ChildInt; 18 | components: ComponentsInt; 19 | focusComponent: ComponentInt; 20 | } 21 | 22 | class BottomPanel extends Component { 23 | render() { 24 | const { components, focusComponent, focusChild } = this.props; 25 | 26 | return ( 27 |
28 | 33 |
34 | ); 35 | } 36 | } 37 | 38 | export default connect( 39 | mapStateToProps, 40 | mapDispatchToProps, 41 | )(BottomPanel); 42 | -------------------------------------------------------------------------------- /src/components/BottomTabs.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { withStyles } from '@material-ui/core/styles'; 3 | import Tabs from '@material-ui/core/Tabs'; 4 | import Tab from '@material-ui/core/Tab'; 5 | import Tooltip from '@material-ui/core/Tooltip'; 6 | import HtmlAttr from './HtmlAttr'; 7 | import CodePreview from './CodePreview'; 8 | import { ComponentInt, ComponentsInt, ChildInt } from '../utils/InterfaceDefinitions'; 9 | import ComponentReduxSetup from './ComponentReduxSetup'; 10 | 11 | interface PropsInt { 12 | focusChild: ChildInt; 13 | components: ComponentsInt; 14 | focusComponent: ComponentInt; 15 | classes: any; 16 | } 17 | 18 | // interface TreeInt { 19 | // name: string; 20 | // attributes: { [key: string]: { value: string } }; 21 | // children: TreeInt[]; 22 | // } 23 | 24 | const styles = (theme: any): any => ({ 25 | root: { 26 | flexGrow: 1, 27 | backgroundColor: '#333333', 28 | height: '100%', 29 | color: '#fff', 30 | // boxShadow: '0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23)', 31 | }, 32 | tabsRoot: { 33 | borderBottom: '0.5px solid #424242', 34 | }, 35 | tabsIndicator: { 36 | backgroundColor: '#1de9b6', 37 | }, 38 | tabRoot: { 39 | textTransform: 'initial', 40 | minWidth: 72, 41 | fontWeight: theme.typography.fontWeightRegular, 42 | marginRight: theme.spacing.unit * 4, 43 | 44 | fontFamily: [ 45 | '-apple-system', 46 | 'BlinkMacSystemFont', 47 | '"Segoe UI"', 48 | 'Roboto', 49 | '"Helvetica Neue"', 50 | 'Arial', 51 | 'sans-serif', 52 | '"Apple Color Emoji"', 53 | '"Segoe UI Emoji"', 54 | '"Segoe UI Symbol"', 55 | ].join(','), 56 | '&:hover': { 57 | color: '#1de9b6', 58 | opacity: 1, 59 | }, 60 | '&$tabSelected': { 61 | color: '#33eb91', 62 | fontWeight: theme.typography.fontWeightMedium, 63 | }, 64 | '&:focus': { 65 | color: '#4aedc4', 66 | }, 67 | }, 68 | tabSelected: {}, 69 | typography: { 70 | padding: theme.spacing.unit * 3, 71 | }, 72 | padding: { 73 | padding: `0 ${theme.spacing.unit * 2}px`, 74 | }, 75 | }); 76 | 77 | class BottomTabs extends Component { 78 | state = { 79 | value: 0, 80 | }; 81 | 82 | handleChange = (event: any, value: number) => { 83 | this.setState({ value }); 84 | }; 85 | 86 | generateComponentTree(componentId: number, components: ComponentsInt) { 87 | const component = components.find(comp => comp.id === componentId); 88 | const tree = { name: component.title, attributes: {}, children: [] }; 89 | 90 | component.childrenArray.forEach((child) => { 91 | if (child.childType === 'COMP') { 92 | tree.children.push(this.generateComponentTree(child.childComponentId, components)); 93 | } else { 94 | tree.children.push({ 95 | name: child.componentName, 96 | attributes: {}, 97 | children: [], 98 | }); 99 | } 100 | }); 101 | return tree; 102 | } 103 | 104 | render() { 105 | const { 106 | classes, components, focusComponent, focusChild, 107 | } = this.props; 108 | const { value } = this.state; 109 | 110 | // display count on the tab. user can see without clicking into tab 111 | const propCount = focusComponent.props.length; 112 | const htmlAttribCount = focusComponent.childrenArray.filter(child => child.childType === 'HTML') 113 | .length; 114 | 115 | return ( 116 |
117 | 121 | 126 | 130 | 135 | 136 | 140 | 145 | 146 | 147 |
148 | {value === 0 && } 149 | {value === 1 && } 150 | {value === 2 && focusChild.childType === 'HTML' && } 151 | {value === 2 && focusChild.childType !== 'HTML' && ( 152 |

select an HTML element to view attributes

153 | )} 154 |
155 |
156 | ); 157 | } 158 | } 159 | 160 | export default withStyles(styles)(BottomTabs); 161 | -------------------------------------------------------------------------------- /src/components/CodePreview.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { PrismAsyncLight as SyntaxHighlighter } from 'react-syntax-highlighter'; 3 | import jsx from 'react-syntax-highlighter/dist/esm/languages/prism/jsx'; 4 | import { dark as style } from 'react-syntax-highlighter/dist/esm/styles/prism'; 5 | import { formatter } from '../utils/formatter.util'; 6 | import componentRender from '../utils/componentRender.util'; 7 | import { ComponentInt, ComponentsInt } from '../utils/InterfaceDefinitions'; 8 | 9 | SyntaxHighlighter.registerLanguage('jsx', jsx); 10 | type Props = { 11 | focusComponent: ComponentInt; 12 | components: ComponentsInt; 13 | }; 14 | 15 | class CodePreview extends Component { 16 | render(): JSX.Element { 17 | const focusComponent: ComponentInt = this.props.focusComponent; 18 | const components: ComponentsInt = this.props.components; 19 | return ( 20 |
21 | 34 | {formatter(componentRender(focusComponent, components))} 35 | 36 |
37 | ); 38 | } 39 | } 40 | 41 | export default CodePreview; 42 | -------------------------------------------------------------------------------- /src/components/ComponentReduxSetup.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { useSelector, useDispatch } from 'react-redux'; 3 | import { withStyles, withTheme } from '@material-ui/core/styles'; 4 | import FormControl from '@material-ui/core/FormControl'; 5 | import Grid from '@material-ui/core/Grid'; 6 | import Button from '@material-ui/core/Button'; 7 | import Select from '@material-ui/core/Select'; 8 | import InputLabel from '@material-ui/core/InputLabel'; 9 | import Input from '@material-ui/core/Input'; 10 | import { 11 | addSelector, 12 | deleteSelector, 13 | addActionToComponent, 14 | deleteActionFromComponent, 15 | setState, 16 | deleteState, 17 | } from '../actions/components'; 18 | import DataTable from './DataTable'; 19 | import { StoreInterface } from '../utils/InterfaceDefinitions'; 20 | 21 | const numbersAsStrings = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']; 22 | 23 | const reservedWords = [ 24 | 'break', 25 | 'case', 26 | 'catch', 27 | 'class', 28 | 'const', 29 | 'continue', 30 | 'debugger', 31 | 'default', 32 | 'delete', 33 | 'do', 34 | 'else', 35 | 'export', 36 | 'extends', 37 | 'finally', 38 | 'for', 39 | 'function', 40 | 'if', 41 | 'import', 42 | 'in', 43 | 'instanceof', 44 | 'new', 45 | 'return', 46 | 'super', 47 | 'switch', 48 | 'this', 49 | 'throw', 50 | 'try', 51 | 'typeof', 52 | 'var', 53 | 'void', 54 | 'while', 55 | 'with', 56 | 'yield', 57 | 'enum', 58 | ]; 59 | 60 | const convertToOptions = choices => [ 61 | 66 | )), 67 | ]; 68 | 69 | const ComponentReduxSetup: React.FC = (props: any): JSX.Element => { 70 | const [chosenAction, setChosenAction] = useState(''); 71 | const [chosenSelector, setChosenSelector] = useState(''); 72 | const [enteredName, setEnteredName] = useState(''); 73 | const [enteredType, setEnteredType] = useState(''); 74 | const [enteredValue, setEnteredValue] = useState(''); 75 | const storeConfig = useSelector((store: StoreInterface) => store.workspace.storeConfig); 76 | const { focusComponent, classes } = props; 77 | const dispatch = useDispatch(); 78 | const rowHeader = ['Actions', 'Store Selections']; 79 | let selectorOptions = []; 80 | let actionOptions = []; 81 | Object.keys(storeConfig.reducers).forEach((reducerName) => { 82 | Object.keys(storeConfig.reducers[reducerName].store).forEach((storePieceName) => { 83 | selectorOptions.push(`${reducerName}.${storePieceName}`); 84 | }); 85 | Object.keys(storeConfig.reducers[reducerName].actions).forEach((actionName) => { 86 | actionOptions.push(`${reducerName}.${actionName}`); 87 | }); 88 | }); 89 | 90 | selectorOptions = convertToOptions(selectorOptions); 91 | actionOptions = convertToOptions(actionOptions); 92 | 93 | const handleChange = cb => e => cb(e.target.value); 94 | 95 | const handleStoreSubmit = (cb, value) => { 96 | const callback = cb; 97 | return (e) => { 98 | e.preventDefault(); 99 | return dispatch(callback(value)); 100 | }; 101 | }; 102 | 103 | const transformIntoVariableName = (string: string): string => string 104 | .replace(/[^ _$A-Za-z0-9]/g, '') 105 | .replace(/\s+(\w)/g, (match, $1) => $1.toUpperCase()) 106 | .replace(/\s/g, ''); 107 | 108 | const handleLocalStateSubmit = (e) => { 109 | e.preventDefault(); 110 | if (numbersAsStrings.includes(enteredName[0])) { 111 | return; 112 | } 113 | if (reservedWords.includes(transformIntoVariableName(enteredName))) { 114 | return; 115 | } 116 | return dispatch( 117 | setState({ 118 | name: transformIntoVariableName(enteredName), 119 | type: enteredType, 120 | initialValue: enteredValue, 121 | }), 122 | ); 123 | }; 124 | 125 | const editHandler = (row) => { 126 | const name = row.match(/Name: \w+/)[0].slice(6); 127 | const type = row.match(/Type: \w+/)[0].slice(6); 128 | const initialValue = row.match(/Initial Value: \w+/)[0].slice(15); 129 | dispatch(deleteState(name)); 130 | setEnteredName(name); 131 | setEnteredType(type); 132 | setEnteredValue(initialValue); 133 | }; 134 | 135 | const submitValueUsingAction = (title, value, onChange, onSubmit, choices) => ( 136 | 137 |
138 | 139 | 140 | 141 | 142 | {`select ${title}`} 143 | 144 | 154 | 155 | 156 | 157 | 160 | 161 | 162 |
163 |
164 | ); 165 | return ( 166 |
167 | {' '} 168 | {Object.keys(focusComponent).length < 1 ? ( 169 |
170 | select a component to view its state & actions 171 |
172 | ) : ( 173 |
174 | {/* */} 175 |
176 |

add redux connections

177 | 178 |
179 | {submitValueUsingAction( 180 | 'redux state', 181 | chosenSelector, 182 | setChosenSelector, 183 | addSelector, 184 | selectorOptions, 185 | )} 186 | dispatch(deleteSelector(name))} 190 | /> 191 |
192 |
193 | 194 |
195 | {submitValueUsingAction( 196 | 'redux action', 197 | chosenAction, 198 | setChosenAction, 199 | addActionToComponent, 200 | actionOptions, 201 | )} 202 | dispatch(deleteActionFromComponent(name))} 206 | /> 207 |
208 |
209 |
210 |
211 |
{ 214 | handleLocalStateSubmit(e); 215 | setEnteredName(''); 216 | setEnteredType(''); 217 | setEnteredValue(''); 218 | }}> 219 |

add local state

220 | 221 | 222 | Name: 223 | 224 | 230 | 231 | 232 | 233 | Type: 234 | 235 | 251 | 252 | 253 | 254 | Value: 255 | 256 | 262 | 263 | 271 |
272 | `Name: ${state.name}. Type: ${state.type}. Initial Value: ${state.initialValue}`, 276 | )} 277 | deletePropHandler={name => dispatch(deleteState(name.match(/Name: \w+/)[0].slice(6)))} 278 | editHandler={row => editHandler(row)} 279 | /> 280 |
281 | {/*
*/} 282 |
283 | )} 284 |
285 | ); 286 | }; 287 | 288 | const styles = theme => ({ 289 | root: { 290 | display: 'flex', 291 | justifyContent: 'center', 292 | flexWrap: 'wrap', 293 | }, 294 | chip: { 295 | color: '#eee', 296 | backgroundColor: '#333333', 297 | }, 298 | column: { 299 | display: 'inline-flex', 300 | alignItems: 'baseline', 301 | }, 302 | icon: { 303 | fontSize: '20px', 304 | color: '#eee', 305 | opacity: '0.7', 306 | transition: 'all .2s ease', 307 | 308 | '&:hover': { 309 | color: 'red', 310 | }, 311 | }, 312 | cssLabel: { 313 | color: 'white', 314 | 315 | '&$cssFocused': { 316 | color: 'green', 317 | }, 318 | }, 319 | cssFocused: {}, 320 | input: { 321 | color: '#eee', 322 | marginBottom: '30px', 323 | width: '50%', 324 | textAlign: 'center', 325 | }, 326 | light: { 327 | color: '#eee', 328 | fontSize: '14px', 329 | padding: '12px', 330 | }, 331 | avatar: { 332 | color: '#eee', 333 | fontSize: '11px', 334 | }, 335 | }); 336 | 337 | // const Placeholder: React.FC = (props): JSX.Element =>
HI
; 338 | export default withStyles(styles)(ComponentReduxSetup); 339 | -------------------------------------------------------------------------------- /src/components/DataTable.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withStyles } from '@material-ui/core/styles'; 3 | import Table from '@material-ui/core/Table'; 4 | import TableBody from '@material-ui/core/TableBody'; 5 | import TableCell from '@material-ui/core/TableCell'; 6 | import TableHead from '@material-ui/core/TableHead'; 7 | import TableRow from '@material-ui/core/TableRow'; 8 | import Paper from '@material-ui/core/Paper'; 9 | import DeleteIcon from '@material-ui/icons/Delete'; 10 | import CreateIcon from '@material-ui/icons/Create'; 11 | import IconButton from '@material-ui/core/IconButton'; 12 | import uuid from 'uuid'; 13 | 14 | const styles = (theme: any) => ({ 15 | root: { 16 | marginTop: theme.spacing.unit * 3, 17 | margin: '10px', 18 | fontSize: '20px', 19 | borderRadius: '10px', 20 | backgroundColor: 'white', 21 | // overflowX: "auto" 22 | }, 23 | table: { 24 | minWidth: 500, 25 | marginRight: '100px', 26 | fontSize: '20px', 27 | borderRadius: '10px', 28 | backgroundColor: 'white', 29 | }, 30 | }); 31 | 32 | /** ************************** 33 | * cannot have a row header or a key in the data called "key" 34 | * ,ust have unique id 35 | * ****************************** */ 36 | 37 | function dataTable(props: any) { 38 | const { 39 | classes, rowData, rowHeader, deletePropHandler, editHandler, 40 | } = props; 41 | 42 | const renderHeader = rowHeader.map((col: any, idx: number) => ( 43 | {col} 44 | )); 45 | 46 | function renderRowCells(row: any) { 47 | if (!row) return; 48 | return rowHeader.map((header: string, idx: number) => ( 49 | 50 | {row.toString()} 51 | 52 | )); 53 | } 54 | // style={{height: 30}} 55 | const renderRows = rowData.map((row: any) => ( 56 | 57 | {renderRowCells(row)} 58 | 59 | deletePropHandler(row)}> 60 | 61 | 62 | editHandler(row)}> 63 | 64 | 65 | 66 | 67 | )); 68 | 69 | return ( 70 | 71 | 72 | 73 | {renderHeader} 74 | 75 | {renderRows} 76 |
77 |
78 | ); 79 | } 80 | 81 | export default withStyles(styles)(dataTable); 82 | -------------------------------------------------------------------------------- /src/components/ErrorMessage.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | const ErrorMessage = (props: any) => { 4 | 5 | const { validation, visible } = props; 6 | 7 | return ( 8 |
12 | {validation.error || ''}  13 |
14 | ); 15 | 16 | }; 17 | 18 | export default ErrorMessage; -------------------------------------------------------------------------------- /src/components/HTMLComponentPanel.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Image from '@material-ui/icons/Image'; 3 | import Description from '@material-ui/icons/Description'; 4 | import EditAttributes from '@material-ui/icons/EditAttributes'; 5 | import Link from '@material-ui/icons/Link'; 6 | import List from '@material-ui/icons/List'; 7 | import LocalParking from '@material-ui/icons/LocalParking'; 8 | import Chip from '@material-ui/core/Chip'; 9 | 10 | interface PropsInterface { 11 | addChild: any; 12 | focusComponent: any; 13 | } 14 | 15 | const HTMLComponentPanel = (props: PropsInterface) => { 16 | const { addChild } = props; 17 | 18 | const handleCreateHTMLChild = (type: string) => { 19 | addChild({ title: type, childType: type, HTMLInfo: {} }); 20 | }; 21 | 22 | return ( 23 |
24 | add HTML element as child 25 | } 29 | onClick={() => { 30 | handleCreateHTMLChild('image'); 31 | }} 32 | variant="outlined" 33 | /> 34 | } 38 | onClick={() => { 39 | handleCreateHTMLChild('form'); 40 | }} 41 | variant="outlined" 42 | /> 43 | } 47 | onClick={() => { 48 | handleCreateHTMLChild('button'); 49 | }} 50 | variant="outlined" 51 | /> 52 | } 56 | onClick={() => { 57 | handleCreateHTMLChild('link'); 58 | }} 59 | variant="outlined" 60 | /> 61 | } 65 | onClick={() => { 66 | handleCreateHTMLChild('list'); 67 | }} 68 | variant="outlined" 69 | /> 70 | } 74 | onClick={() => { 75 | handleCreateHTMLChild('paragraph'); 76 | }} 77 | variant="outlined" 78 | /> 79 |
80 | ); 81 | }; 82 | 83 | export default HTMLComponentPanel; 84 | -------------------------------------------------------------------------------- /src/components/HtmlAttr.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { withStyles } from '@material-ui/core/styles'; 4 | import Grid from '@material-ui/core/Grid'; 5 | import TextField from '@material-ui/core/TextField'; 6 | import Paper from '@material-ui/core/Paper'; 7 | import Fab from '@material-ui/core/Fab'; 8 | import { updateHtmlAttr } from '../actions/components'; 9 | import { HTMLelements } from '../utils/htmlElements.util'; 10 | import { ComponentInt, ChildInt } from '../utils/InterfaceDefinitions'; 11 | 12 | interface PropsInt { 13 | updateHtmlAttr?: any; 14 | focusComponent?: ComponentInt; 15 | classes?: any; 16 | deleteProp?: any; 17 | addProp?: any; 18 | focusChild?: ChildInt; 19 | } 20 | 21 | interface StateInt {} 22 | 23 | const styles = (theme: any): any => ({ 24 | root: { 25 | display: 'flex', 26 | justifyContent: 'center', 27 | flexWrap: 'wrap', 28 | background: '#007BFF', 29 | color: 'white', 30 | borderRadius: '15px', 31 | border: '2px solid white', 32 | }, 33 | cssLabel: { 34 | color: 'white', 35 | }, 36 | cssFocused: { 37 | color: '#5CDB95', 38 | }, 39 | input: { 40 | color: 'white', 41 | opacity: '0.7', 42 | marginBottom: '15px', 43 | }, 44 | }); 45 | 46 | const mapDispatchToProps = (dispatch: any) => ({ 47 | updateHtmlAttr: ({ attr, value }: { attr: string; value: string }) => dispatch(updateHtmlAttr({ attr, value })), 48 | }); 49 | 50 | const mapStateToProps = (store: any) => ({ 51 | focusComponent: store.workspace.focusComponent, 52 | focusChild: store.workspace.focusChild, 53 | }); 54 | 55 | class HtmlAttr extends Component { 56 | constructor(props) { 57 | super(props); 58 | this.state = HTMLelements[this.props.focusChild.htmlElement].attributes.reduce((acc, attr) => { 59 | acc[attr] = ''; 60 | return acc; 61 | }, {}); 62 | } 63 | 64 | handleSave = (attr: string) => { 65 | this.props.updateHtmlAttr({ attr, value: this.state[attr] }); 66 | this.setState({ 67 | [attr]: '', 68 | }); 69 | }; 70 | 71 | handleChange = (event: any) => { 72 | this.setState({ 73 | [event.target.id]: event.target.value.trim(), 74 | }); 75 | }; 76 | 77 | render() { 78 | const { classes, focusChild, focusComponent } = this.props; 79 | const focusChildType = focusChild.htmlElement; 80 | 81 | const HtmlForm = HTMLelements[focusChildType].attributes.map((attr: string, i: number) => ( 82 | 83 | 84 | 105 | 106 | 107 | this.handleSave(attr)}> 118 | {'save'} 119 | 120 | 121 | 122 | 123 |

124 | {focusChild.HTMLInfo[attr] ? focusChild.HTMLInfo[attr] : ' no attribute assigned'} 125 |

126 |
127 |
128 |
129 | )); 130 | 131 | return ( 132 |
133 |
{`${focusChildType.toLowerCase()}`}
134 | {HtmlForm} 135 |
136 | ); 137 | } 138 | } 139 | 140 | export default connect( 141 | mapStateToProps, 142 | mapDispatchToProps, 143 | )(withStyles(styles)(HtmlAttr)); 144 | -------------------------------------------------------------------------------- /src/components/HtmlChild.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | import { withStyles } from '@material-ui/core/styles'; 4 | import ListItem from '@material-ui/core/ListItem'; 5 | import Button from '@material-ui/core/Button'; 6 | import DeleteIcon from '@material-ui/icons/Delete'; 7 | import { deleteChild, changeFocusChild } from '../actions/components'; 8 | import { StoreInterface } from '../utils/InterfaceDefinitions'; 9 | 10 | const HtmlChild: React.FC = (props: any): JSX.Element => { 11 | const { classes, childId, color } = props; 12 | const dispatch = useDispatch(); 13 | const focusChildId = useSelector((store: StoreInterface) => store.workspace.focusChild).childId; 14 | const deleteButton = ( 15 | 16 | {/* shows the delete button */} 17 | 28 | 29 | ); 30 | 31 | return ( 32 | dispatch(changeFocusChild({ childId }))} 35 | className="node-html-child"> 36 |
39 | {props.componentName.toLowerCase()} 40 |
41 | {deleteButton} 42 |
43 | ); 44 | }; 45 | 46 | export default withStyles({})(HtmlChild); 47 | -------------------------------------------------------------------------------- /src/components/Interface.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import TextField from '@material-ui/core/TextField'; 3 | import IconButton from '@material-ui/core/IconButton'; 4 | import Icon from '@material-ui/core/Icon'; 5 | import Checkbox from '@material-ui/core/Checkbox'; 6 | import FormControlLabel from '@material-ui/core/FormControlLabel'; 7 | import TypeSelect from './TypeSelect'; 8 | import validateInput from '../utils/validateInput.util'; 9 | import ErrorMessage from './ErrorMessage'; 10 | import StoreItemHeader from './StoreItemHeader'; 11 | 12 | const Interface = (props: any) => { 13 | const { 14 | thisInterface, interfaces, setInterface, deleteInterface, 15 | } = props; 16 | const [newPropertyName, setNewPropertyName] = useState(''); 17 | const [newPropertyType, setNewPropertyType] = useState(''); 18 | const [newPropertyIsArray, setNewPropertyIsArray] = useState(false); 19 | const [newPropertyValidation, setNewPropertyValidation] = useState(validateInput('')); 20 | const [isVisible, setVisibility] = useState(false); 21 | 22 | const handleChange = (event: Event, setter: any) => { 23 | const target: HTMLInputElement = event.target; 24 | setter(target.value); 25 | const result = validateInput(target.value); 26 | setNewPropertyValidation(result); 27 | }; 28 | 29 | const addProperty = () => { 30 | if (newPropertyValidation.isValid && newPropertyType) { 31 | const updatedInterface = interfaces[thisInterface]; 32 | updatedInterface[newPropertyValidation.input] = newPropertyType; 33 | setInterface({ [thisInterface]: updatedInterface }); 34 | setNewPropertyName(''); 35 | setNewPropertyType(''); 36 | setNewPropertyIsArray(false); 37 | setVisibility(false); 38 | } else { 39 | setVisibility(true); 40 | } 41 | }; 42 | 43 | const deleteProperty = (property) => { 44 | const updatedInterface = interfaces[thisInterface]; 45 | delete updatedInterface[property]; 46 | setInterface({ [thisInterface]: updatedInterface }); 47 | }; 48 | 49 | return ( 50 |
51 | 52 |
53 | {interfaces[thisInterface] 54 | && Object.keys(interfaces[thisInterface]).map(property => ( 55 |
56 |
    57 |
  • 58 |
    name
    59 |
    {property}
    60 |
  • 61 |
  • 62 |
    type
    63 |
    {interfaces[thisInterface][property]}
    64 |
  • 65 |
66 |
67 | deleteProperty(property)}> 70 | delete 71 | 72 |
73 |
74 | ))} 75 |
76 | 77 |
78 | { 83 | handleChange(event, setNewPropertyName); 84 | }} 85 | onKeyPress={(event) => { 86 | if (event.key === 'Enter') event.preventDefault(); 87 | }} 88 | required 89 | /> 90 | { 96 | setNewPropertyType(event.target.value); 97 | }} 98 | /> 99 | { 106 | if (newPropertyIsArray) { 107 | // remove [] 108 | setNewPropertyType((type) => { 109 | if (type.indexOf('[') !== -1) { 110 | return type.slice(0, type.length - 2); 111 | } 112 | return type; 113 | }); 114 | } else { 115 | setNewPropertyType(type => `${type}[]`); 116 | } 117 | setNewPropertyIsArray(isArray => !isArray); 118 | }} 119 | /> 120 | } 121 | label="array?" 122 | labelPlacement="top" 123 | /> 124 | 128 | add 129 | 130 | 131 |
132 | ); 133 | }; 134 | 135 | export default Interface; 136 | -------------------------------------------------------------------------------- /src/components/Interfaces.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import TextField from '@material-ui/core/TextField'; 4 | import IconButton from '@material-ui/core/IconButton'; 5 | import Icon from '@material-ui/core/Icon'; 6 | import Tooltip from '@material-ui/core/Tooltip'; 7 | import * as actions from '../actions/components'; 8 | import { InterfacesInterface, InputValidation } from '../utils/InterfaceDefinitions'; 9 | import Interface from './Interface'; 10 | import validateInput from '../utils/validateInput.util'; 11 | import ErrorMessage from './ErrorMessage'; 12 | 13 | const mapDispatchToProps = (dispatch: any) => ({ 14 | setInterface: (myInterface: InterfacesInterface) => dispatch(actions.setInterface(myInterface)), 15 | deleteInterface: (interfaceName: string) => dispatch(actions.deleteInterface(interfaceName)), 16 | }); 17 | 18 | const mapStateToProps = (store: any) => ({ 19 | interfaces: store.workspace.storeConfig.interfaces, 20 | }); 21 | 22 | interface PropsInterface { 23 | setInterface?: any; 24 | deleteInterface?: any; 25 | interfaces?: any; 26 | classes?: any; 27 | validateInput?: any; 28 | } 29 | 30 | interface StateInterface { 31 | newInterfaceValidation: InputValidation; 32 | newInterfaceNameInput: string; 33 | isVisible: boolean; 34 | } 35 | 36 | class Interfaces extends Component { 37 | constructor(props: PropsInterface) { 38 | super(props); 39 | this.state = { 40 | newInterfaceValidation: { isValid: false, input: '', error: '' }, 41 | newInterfaceNameInput: '', 42 | isVisible: false, 43 | }; 44 | } 45 | 46 | createInterface = () => { 47 | if (this.state.newInterfaceValidation.isValid) { 48 | const interfaceName = this.state.newInterfaceValidation.input; 49 | this.props.setInterface({ [interfaceName]: {} }); 50 | this.setState({ newInterfaceNameInput: '' }); 51 | this.setState({ isVisible: false }); 52 | } else { 53 | this.setState({ isVisible: true }); 54 | } 55 | }; 56 | 57 | handleChange = (event: Event) => { 58 | if (event.target.value.length < 18) { 59 | const target: HTMLInputElement = event.target; 60 | const result: InputValidation = validateInput(target.value); 61 | this.setState({ newInterfaceNameInput: target.value, newInterfaceValidation: result }); 62 | } 63 | }; 64 | 65 | render() { 66 | return ( 67 |
68 | 72 |

Interfaces

73 |
74 |
75 | {this.props.interfaces 76 | && Object.keys(this.props.interfaces).map(thisInterface => ( 77 | 84 | ))} 85 |
86 | 90 |
91 | { 96 | if (event.key === 'Enter') { 97 | this.createInterface(); 98 | event.preventDefault(); 99 | } 100 | }} 101 | required 102 | /> 103 | 107 | add 108 | 109 | 110 |
111 | ); 112 | } 113 | } 114 | 115 | export default connect( 116 | mapStateToProps, 117 | mapDispatchToProps, 118 | )(Interfaces); 119 | -------------------------------------------------------------------------------- /src/components/LeftColExpansionPanel.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import { withStyles } from '@material-ui/core/styles'; 3 | import Typography from '@material-ui/core/Typography'; 4 | import List from '@material-ui/core/List'; 5 | import ListItem from '@material-ui/core/ListItem'; 6 | import ListItemText from '@material-ui/core/ListItemText'; 7 | import IconButton from '@material-ui/core/IconButton'; 8 | import Grid from '@material-ui/core/Grid'; 9 | import AddIcon from '@material-ui/icons/Add'; 10 | import RemoveIcon from '@material-ui/icons/Remove'; 11 | import Button from '@material-ui/core/Button'; 12 | import Tooltip from '@material-ui/core/Tooltip'; 13 | import uuid from 'uuid'; 14 | import Collapse from '@material-ui/core/Collapse'; 15 | import HtmlChild from './HtmlChild'; 16 | 17 | export const LeftColExpansionPanel = (props: any) => { 18 | const { 19 | classes, 20 | focusComponent, 21 | component, 22 | addChild, 23 | deleteChild, 24 | changeFocusComponent, 25 | selectableChildren, 26 | components, 27 | deleteComponent, 28 | } = props; 29 | const { title, id, color } = component; 30 | 31 | function isFocused() { 32 | return focusComponent.id === id ? 'focused' : ''; 33 | } 34 | 35 | const focusedStyle = { 36 | boxShadow: 'rgba(150, 150, 150, 0.2) 0 0 4px 2px', 37 | background: color, 38 | }; 39 | 40 | const componentTitleDisplay = ( 41 | 42 | 43 | { 47 | changeFocusComponent({ title }); 48 | }}> 49 | 54 | {title} 55 | 56 | } 57 | /> 58 | 59 | 60 | 61 | ); 62 | 63 | const deleteComponentButton = ( 64 | 65 | 88 | 89 | ); 90 | 91 | const addAsChildButton = ( 92 | 93 | { 96 | addChild({ title, childType: 'COMP' }); 97 | }}> 98 | 99 | 100 | 101 | ); 102 | 103 | const deleteChildButton = ( 104 | 105 | { 108 | deleteChild(id); 109 | }}> 110 | 111 | 112 | 113 | ); 114 | const HtmlChildrenOfFocusComponent = []; 115 | let thisComponentIsAChildOfFocusComponent = false; 116 | focusComponent.childrenArray.forEach((child) => { 117 | if (child.childType === 'HTML') { 118 | HtmlChildrenOfFocusComponent.push( 119 | , 128 | ); 129 | } else if (child.childComponentId === id) { 130 | thisComponentIsAChildOfFocusComponent = true; 131 | } 132 | }); 133 | 134 | return ( 135 | 136 | 137 |
138 | {componentTitleDisplay} 139 | 140 | 141 | {isFocused() ? HtmlChildrenOfFocusComponent :
} 142 | 143 | 144 | {id !== 1 && isFocused() ? deleteComponentButton :
} 145 |
146 | 147 | 148 | {id !== 1 && !isFocused() && selectableChildren.includes(id) ? addAsChildButton :
} 149 | 150 | {id !== 1 151 | && !isFocused() 152 | && selectableChildren.includes(id) 153 | && thisComponentIsAChildOfFocusComponent ? ( 154 | 155 | {deleteChildButton} 156 | 157 | ) : ( 158 |
159 | )} 160 | 161 | ); 162 | }; 163 | 164 | function styles(): any { 165 | return { 166 | root: { 167 | width: '100%', 168 | height: '100%', 169 | borderRadius: '10px', 170 | marginTop: 10, 171 | backgroundColor: '#4e4e4e', 172 | }, 173 | light: { 174 | color: '#eee', 175 | '&:hover': { 176 | color: '#1de9b6', 177 | }, 178 | }, 179 | }; 180 | } 181 | 182 | export default withStyles(styles)(LeftColExpansionPanel); 183 | -------------------------------------------------------------------------------- /src/components/NewTreeDisplay.tsx: -------------------------------------------------------------------------------- 1 | import Tree from 'react-d3-tree'; 2 | import React, { useEffect, useState } from 'react'; 3 | import { withStyles } from '@material-ui/core/styles'; 4 | import { ComponentInt, ComponentsInt, ChildInt } from '../utils/InterfaceDefinitions'; 5 | 6 | interface TreeInt { 7 | name: string; 8 | attributes: { [key: string]: { value: string } }; 9 | children: TreeInt[]; 10 | } 11 | 12 | interface PropsInt { 13 | focusChild: ChildInt; 14 | components: ComponentsInt; 15 | focusComponent: ComponentInt; 16 | classes: any; 17 | } 18 | 19 | const styles = (theme: any): any => ({ 20 | root: { 21 | flexGrow: 1, 22 | backgroundColor: '#333333', 23 | height: '100%', 24 | color: '#fff', 25 | boxShadow: '0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23)', 26 | }, 27 | tabsRoot: { 28 | borderBottom: '0.5px solid #424242', 29 | }, 30 | tabsIndicator: { 31 | backgroundColor: '#1de9b6', 32 | }, 33 | tabRoot: { 34 | textTransform: 'initial', 35 | minWidth: 72, 36 | fontWeight: theme.typography.fontWeightRegular, 37 | marginRight: theme.spacing.unit * 4, 38 | 39 | fontFamily: [ 40 | '-apple-system', 41 | 'BlinkMacSystemFont', 42 | '"Segoe UI"', 43 | 'Roboto', 44 | '"Helvetica Neue"', 45 | 'Arial', 46 | 'sans-serif', 47 | '"Apple Color Emoji"', 48 | '"Segoe UI Emoji"', 49 | '"Segoe UI Symbol"', 50 | ].join(','), 51 | '&:hover': { 52 | color: '#1de9b6', 53 | opacity: 1, 54 | }, 55 | '&$tabSelected': { 56 | color: '#33eb91', 57 | fontWeight: theme.typography.fontWeightMedium, 58 | }, 59 | '&:focus': { 60 | color: '#4aedc4', 61 | }, 62 | }, 63 | tabSelected: {}, 64 | typography: { 65 | padding: theme.spacing.unit * 3, 66 | }, 67 | padding: { 68 | padding: `0 ${theme.spacing.unit * 2}px`, 69 | }, 70 | }); 71 | 72 | const TreeDisplay: React.FC = (props): JSX.Element => { 73 | const [translation, setTranslation] = useState({ x: 0, y: 0 }); 74 | const [zoomLevel, setZoomLevel] = useState(1); 75 | 76 | let treeWrapper; 77 | 78 | const handleResize = () => { 79 | if (treeWrapper) { 80 | const treeSize = document.querySelector('g').getBBox(); 81 | const container = treeWrapper.getBoundingClientRect(); 82 | if (container.width < 600) { 83 | setTranslation({ x: container.width / 2, y: container.height / 2.3 }); 84 | const containerToTreeHeightRatio = container.height / treeSize.height; 85 | setZoomLevel(containerToTreeHeightRatio); 86 | } else { 87 | setTranslation({ x: container.width / 2, y: container.height / 2.9 }); 88 | } 89 | const containerToTreeWidthRatio = container.width / treeSize.width; 90 | setZoomLevel(containerToTreeWidthRatio); 91 | } 92 | }; 93 | 94 | useEffect(() => { 95 | window.addEventListener('resize', handleResize); 96 | return () => { 97 | window.removeEventListener('resize', handleResize); 98 | }; 99 | }); 100 | 101 | useEffect(() => { 102 | handleResize(); 103 | }, []); 104 | 105 | useEffect(() => { 106 | handleResize(); 107 | }, [props.components, props.focusComponent]); 108 | 109 | return ( 110 |
(treeWrapper = node)}> 111 | 155 |
156 | ); 157 | }; 158 | 159 | export default withStyles(styles)(TreeDisplay); 160 | 161 | function generateComponentTree(componentId: number, components: ComponentsInt) { 162 | const component = components.find(comp => comp.id === componentId); 163 | const tree = { 164 | name: component.title, 165 | attributes: {}, 166 | children: [], 167 | nodeSvgShape: createShape(65, component.color), 168 | }; 169 | component.childrenArray.forEach((child) => { 170 | if (child.childType === 'COMP') { 171 | tree.children.push(generateComponentTree(child.childComponentId, components)); 172 | } else { 173 | tree.children.push({ 174 | name: child.componentName, 175 | attributes: {}, 176 | children: [], 177 | nodeSvgShape: createShape(50, '#007BFF'), 178 | }); 179 | } 180 | }); 181 | return tree; 182 | } 183 | 184 | function createShape(size, color) { 185 | return { 186 | shape: 'circle', 187 | shapeProps: { 188 | r: size, 189 | fill: color, 190 | stroke: hexToHSL(color), 191 | }, 192 | }; 193 | } 194 | 195 | const randomColor = `rgb(${Math.floor(255 * Math.random())},${Math.floor( 196 | 255 * Math.random(), 197 | )},${Math.floor(255 * Math.random())})`; 198 | 199 | function hexToHSL(hex) { 200 | const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); 201 | let r = parseInt(result[1], 16); 202 | let g = parseInt(result[2], 16); 203 | let b = parseInt(result[3], 16); 204 | (r /= 255), (g /= 255), (b /= 255); 205 | let max = Math.max(r, g, b), 206 | min = Math.min(r, g, b); 207 | let h = (max + min) / 2; 208 | let s = (max + min) / 2; 209 | const l = (max + min) / 2; 210 | 211 | if (max == min) { 212 | h = s = 0; // achromatic 213 | } else { 214 | const d = max - min; 215 | s = l > 0.5 ? d / (2 - max - min) : d / (max + min); 216 | switch (max) { 217 | case r: 218 | h = (g - b) / d + (g < b ? 6 : 0); 219 | break; 220 | case g: 221 | h = (b - r) / d + 2; 222 | break; 223 | case b: 224 | h = (r - g) / d + 4; 225 | break; 226 | } 227 | h /= 6; 228 | } 229 | return `hsl(${h * 360},50%,50%)`; 230 | } 231 | -------------------------------------------------------------------------------- /src/components/Reducer.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import Store from './Store'; 3 | import Actions from './Actions'; 4 | import StoreItemHeader from './StoreItemHeader'; 5 | 6 | const Reducer = (props: any) => { 7 | 8 | const { 9 | reducer, 10 | reducers, 11 | interfaces, 12 | setReducer, 13 | deleteReducer, 14 | } = props; 15 | 16 | return ( 17 |
18 | 19 | 24 | 29 |
30 | ); 31 | 32 | }; 33 | 34 | export default Reducer; -------------------------------------------------------------------------------- /src/components/Reducers.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import TextField from '@material-ui/core/TextField'; 4 | import IconButton from '@material-ui/core/IconButton'; 5 | import Icon from '@material-ui/core/Icon'; 6 | import Tooltip from '@material-ui/core/Tooltip'; 7 | import * as actions from '../actions/components'; 8 | import { ReducersInterface, InputValidation } from '../utils/InterfaceDefinitions'; 9 | import Reducer from './Reducer'; 10 | import validateInput from '../utils/validateInput.util'; 11 | import ErrorMessage from './ErrorMessage'; 12 | 13 | const mapDispatchToProps = (dispatch: any) => ({ 14 | setReducer: (reducer: ReducersInterface) => dispatch(actions.setReducer(reducer)), 15 | deleteReducer: (reducer: string) => dispatch(actions.deleteReducer(reducer)), 16 | }); 17 | 18 | const mapStateToProps = (store: any) => ({ 19 | interfaces: store.workspace.storeConfig.interfaces, 20 | reducers: store.workspace.storeConfig.reducers, 21 | }); 22 | 23 | interface PropsInterface { 24 | setReducer: any; 25 | deleteReducer: any; 26 | interfaces: any; 27 | reducers: any; 28 | } 29 | 30 | interface StateInterface { 31 | nameInput: string; 32 | validation: InputValidation; 33 | isVisible: boolean; 34 | } 35 | 36 | class Reducers extends Component { 37 | constructor(props: PropsInterface) { 38 | super(props); 39 | this.state = { 40 | nameInput: '', 41 | validation: validateInput(''), 42 | isVisible: false, 43 | }; 44 | } 45 | 46 | handleChange = (event: Event) => { 47 | if (event.target.value.length < 18) { 48 | const target: HTMLInputElement = event.target; 49 | const result: InputValidation = validateInput(target.value); 50 | this.setState({ nameInput: target.value, validation: result }); 51 | } 52 | }; 53 | 54 | createReducer = () => { 55 | if (this.state.validation.isValid) { 56 | const reducerName = this.state.validation.input; 57 | this.props.setReducer({ [reducerName]: { store: {}, actions: {} } }); 58 | this.setState({ nameInput: '' }); 59 | this.setState({ isVisible: false }); 60 | } else { 61 | this.setState({ isVisible: true }); 62 | } 63 | }; 64 | 65 | render() { 66 | return ( 67 |
68 | 69 |

Reducers

70 |
71 |
72 | {this.props.reducers 73 | && Object.keys(this.props.reducers).map(reducer => ( 74 | 82 | ))} 83 |
84 | 85 |
86 | { 91 | if (event.key === 'Enter') { 92 | this.createReducer(); 93 | event.preventDefault(); 94 | } 95 | }} 96 | required 97 | /> 98 | 102 | add 103 | 104 | 105 |
106 | ); 107 | } 108 | } 109 | 110 | export default connect( 111 | mapStateToProps, 112 | mapDispatchToProps, 113 | )(Reducers); 114 | -------------------------------------------------------------------------------- /src/components/RightPanel.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Interfaces from './Interfaces'; 3 | import Reducers from './Reducers'; 4 | import validateInput from '../utils/validateInput.util'; 5 | 6 | const RightPanel = (props: any) => { 7 | const handleChange = (event: Event, setter: any, setValidation: any = '') => { 8 | const target: any = event.target; 9 | setter(target.type === 'checkbox' ? target.checked : target.value); 10 | if (setValidation !== '') { 11 | const result = validateInput(target.value); 12 | setValidation(result); 13 | } 14 | }; 15 | 16 | return ( 17 |
18 | 19 | 20 |
21 | ); 22 | }; 23 | 24 | export default RightPanel; 25 | -------------------------------------------------------------------------------- /src/components/SimpleModal.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import Modal from '@material-ui/core/Modal'; 3 | import { withStyles } from '@material-ui/core/styles'; 4 | import Typography from '@material-ui/core/Typography'; 5 | import Button from '@material-ui/core/Button'; 6 | import IconButton from '@material-ui/core/IconButton'; 7 | import CloseIcon from '@material-ui/icons/Close'; 8 | 9 | const styles = (theme: any): any => ({ 10 | paper: { 11 | position: 'absolute', 12 | width: 'auto', 13 | maxWidth: '500px', 14 | height: 'auto', 15 | maxHeight: '300px', 16 | backgroundColor: '#FFCB9A', 17 | boxShadow: theme.shadows[5], 18 | padding: '4%', 19 | minWidth: '500px', 20 | minHeight: '300px', 21 | borderRadius: '10px', 22 | }, 23 | button: { 24 | marginTop: '0%', 25 | height: 'auto', 26 | marginLeft: '3%', 27 | borderRadius: '4px', 28 | float: 'right', 29 | }, 30 | }); 31 | 32 | const SimpleModal = (props: any) => { 33 | const { 34 | classes, 35 | open, 36 | message, 37 | primBtnLabel, 38 | secBtnLabel, 39 | primBtnAction, 40 | secBtnAction, 41 | closeModal, 42 | children = null, 43 | } = props; 44 | 45 | return ( 46 | 47 | 52 |
60 | 71 | 72 | 73 | 74 | {message} 75 | 76 |
{children}
77 |
78 | {secBtnLabel ? ( 79 | 84 | ) : null} 85 | {primBtnLabel ? ( 86 | 91 | ) : null} 92 |
93 |
94 |
95 |
96 | ); 97 | }; 98 | 99 | export default withStyles(styles)(SimpleModal); 100 | -------------------------------------------------------------------------------- /src/components/Store.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import Checkbox from '@material-ui/core/Checkbox'; 3 | import FormControlLabel from '@material-ui/core/FormControlLabel'; 4 | import Icon from '@material-ui/core/Icon'; 5 | import IconButton from '@material-ui/core/IconButton'; 6 | import TextField from '@material-ui/core/TextField'; 7 | import Tooltip from '@material-ui/core/Tooltip'; 8 | import TypeSelect from './TypeSelect'; 9 | import validateInput from '../utils/validateInput.util'; 10 | import ErrorMessage from './ErrorMessage'; 11 | 12 | const Store = (props: any) => { 13 | const { 14 | reducer, reducers, interfaces, setReducer, 15 | } = props; 16 | const [validation, setValidation] = useState(validateInput('')); 17 | const [propertyName, setPropertyName] = useState(''); 18 | const [propertyType, setPropertyType] = useState(''); 19 | const [propertyIsArray, setPropertyIsArray] = useState(false); 20 | const [propertyInitialValue, setPropertyInitialValue] = useState(''); 21 | const [isVisible, setVisibility] = useState(false); 22 | 23 | const handleChange = (event: Event, setter: any) => { 24 | const target: HTMLInputElement = event.target; 25 | setter(target.type === 'checkbox' ? target.checked : target.value); 26 | if (target.type === 'text') { 27 | const result = validateInput(target.value); 28 | setValidation(result); 29 | } 30 | }; 31 | 32 | const addProperty = () => { 33 | if (validation.isValid && propertyType && propertyInitialValue) { 34 | const updatedReducer = reducers[reducer]; 35 | updatedReducer.store[validation.input] = { 36 | type: propertyType, 37 | array: propertyIsArray, 38 | initialValue: propertyInitialValue, 39 | }; 40 | setReducer({ [reducer]: updatedReducer }); 41 | setPropertyName(''); 42 | setPropertyType(''); 43 | setPropertyIsArray(false); 44 | setPropertyInitialValue(''); 45 | setVisibility(false); 46 | } else { 47 | setVisibility(true); 48 | } 49 | }; 50 | 51 | const deleteProperty = (property: string) => { 52 | const updatedReducer = reducers[reducer]; 53 | delete updatedReducer.store[property]; 54 | setReducer({ [reducer]: updatedReducer }); 55 | }; 56 | 57 | return ( 58 |
59 | 63 |

Store

64 |
65 |
66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | {reducers[reducer].store 76 | && Object.keys(reducers[reducer].store).map(store => ( 77 | 78 | 79 | 80 | 81 | 82 | 89 | 90 | ))} 91 | 92 |
nametypearrayinitial
{store}{reducers[reducer].store[store].type}{reducers[reducer].store[store].array ? '✓' : '×'}{reducers[reducer].store[store].initialValue} 83 | deleteProperty(store)}> 86 | delete 87 | 88 |
93 |
94 | 95 |
96 | { 100 | handleChange(event, setPropertyName); 101 | }} 102 | required 103 | /> 104 | { 110 | handleChange(event, setPropertyType); 111 | }} 112 | /> 113 | { 118 | handleChange(event, setPropertyIsArray); 119 | }} 120 | /> 121 | } 122 | label="array" 123 | /> 124 | { 128 | setPropertyInitialValue(event.target.value); 129 | }} 130 | className="code" 131 | onKeyPress={(event) => { 132 | if (event.key === 'Enter') { 133 | addProperty(); 134 | event.preventDefault(); 135 | } 136 | }} 137 | required 138 | /> 139 | 143 | add 144 | 145 | 146 |
147 | ); 148 | }; 149 | 150 | export default Store; 151 | -------------------------------------------------------------------------------- /src/components/StoreItemHeader.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import Button from '@material-ui/core/Button'; 3 | import IconButton from '@material-ui/core/IconButton'; 4 | import Icon from '@material-ui/core/Icon'; 5 | 6 | const StoreItemHeader = (props: any) => { 7 | const { storeItem, deleter } = props; 8 | const [dialogIsVisible, setDialogVisibility] = useState(false); 9 | 10 | const toggleVisibility = () => { 11 | setDialogVisibility(!dialogIsVisible); 12 | }; 13 | 14 | return ( 15 | 16 |
17 |
18 | 21 | 24 |
25 |
26 |
27 |

{storeItem}

28 | toggleVisibility()} 31 | className="delete-store-item"> 32 | delete 33 | 34 |
35 |
36 | ); 37 | }; 38 | 39 | export default StoreItemHeader; 40 | -------------------------------------------------------------------------------- /src/components/TypeSelect.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import FormControl from '@material-ui/core/FormControl'; 3 | import InputLabel from '@material-ui/core/InputLabel'; 4 | import MenuItem from '@material-ui/core/MenuItem'; 5 | import Select from '@material-ui/core/Select'; 6 | import Input from '@material-ui/core/Input'; 7 | 8 | const TypeSelect = (props: any) => { 9 | const { 10 | selectName, outer, interfaces, labelName, handleChange, value, 11 | } = props; 12 | const defaultItems = ['boolean', 'number', 'string']; 13 | const items = [...defaultItems, ...Object.keys(interfaces), 'any']; 14 | 15 | return ( 16 | 17 | {labelName || 'type'} 18 | 34 | 35 | ); 36 | }; 37 | 38 | export default TypeSelect; 39 | -------------------------------------------------------------------------------- /src/components/theme.ts: -------------------------------------------------------------------------------- 1 | import { createMuiTheme } from '@material-ui/core/styles'; 2 | import red from '@material-ui/core/colors/indigo'; 3 | 4 | const theme = createMuiTheme({ 5 | typography: { 6 | useNextVariants: true, 7 | }, 8 | palette: { 9 | primary: { 10 | light: '#00e676', 11 | // main: '#33eb91', 12 | main: '#01d46d', // less blinding green 13 | dark: '#14a37f', 14 | contrastText: '#fff', 15 | }, 16 | secondary: red, 17 | }, 18 | }); 19 | 20 | export default theme; 21 | -------------------------------------------------------------------------------- /src/containers/AppContainer.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { MuiThemeProvider } from '@material-ui/core/styles'; 4 | import LinearProgress from '@material-ui/core/LinearProgress'; 5 | import LeftContainer from './LeftContainer'; 6 | import MainContainer from './MainContainer'; 7 | import theme from '../components/theme'; 8 | import { loadInitData } from '../actions/components'; 9 | import { ComponentInt, ComponentsInt } from '../utils/InterfaceDefinitions'; 10 | 11 | type Props = { 12 | components: ComponentsInt; 13 | focusComponent: ComponentInt; 14 | totalComponents: number; 15 | loading: boolean; 16 | selectableChildren: Array; 17 | loadInitData: any; 18 | }; 19 | 20 | const mapStateToProps = (store: any) => ({ 21 | components: store.workspace.components, 22 | totalComponents: store.workspace.totalComponents, 23 | focusComponent: store.workspace.focusComponent, 24 | loading: store.workspace.loading, 25 | selectableChildren: store.workspace.selectableChildren, 26 | }); 27 | 28 | const mapDispatchToProps = { loadInitData }; 29 | 30 | class AppContainer extends Component { 31 | state = { 32 | width: 25, 33 | rightColumnOpen: true, 34 | }; 35 | 36 | componentDidMount() { 37 | this.props.loadInitData(); 38 | } 39 | 40 | render(): JSX.Element { 41 | const { 42 | components, focusComponent, loading, selectableChildren, totalComponents, 43 | } = this.props; 44 | // const { width, rightColumnOpen } = this.state; 45 | 46 | // uses component childIds and parentIds arrays (numbers) to build component-filled children and parents arrays 47 | return ( 48 | 49 | 55 | 56 | {loading ? ( 57 |
63 | 64 |
65 | ) : null} 66 |
67 | ); 68 | } 69 | } 70 | 71 | export default connect( 72 | mapStateToProps, 73 | mapDispatchToProps, 74 | )(AppContainer); 75 | -------------------------------------------------------------------------------- /src/containers/LeftContainer.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { compose } from 'redux'; 4 | import TextField from '@material-ui/core/TextField'; 5 | import Button from '@material-ui/core/Button'; 6 | import AddIcon from '@material-ui/icons/Add'; 7 | import Grid from '@material-ui/core/Grid'; 8 | import { withStyles } from '@material-ui/core/styles'; 9 | import GetAppIcon from '@material-ui/icons/GetApp'; 10 | // import List from '@material-ui/core/List'; 11 | // import ListItem from '@material-ui/core/ListItem'; 12 | // import ListItemText from '@material-ui/core/ListItemText'; 13 | import Fab from '@material-ui/core/Fab'; 14 | import LeftColExpansionPanel from '../components/LeftColExpansionPanel'; 15 | import HTMLComponentPanel from '../components/HTMLComponentPanel'; 16 | import * as actions from '../actions/components'; 17 | import { 18 | ComponentInt, 19 | ComponentsInt, 20 | StoreConfigInterface, 21 | StoreInterface, 22 | } from '../utils/InterfaceDefinitions'; 23 | import createModal from '../utils/createModal.util'; 24 | import cloneDeep from '../utils/cloneDeep'; 25 | 26 | 27 | // /////// FOR TESTING ONLY////////////////////////// 28 | 29 | const dummyFilePath = '/Users/jacobrichards/Desktop/'; 30 | // /////////////////////////////////////////////////// 31 | 32 | interface PropsInt { 33 | components: ComponentsInt; 34 | focusComponent: ComponentInt; 35 | selectableChildren: Array; 36 | storeConfig: StoreConfigInterface; 37 | classes: any; 38 | addComponent: any; 39 | addChild: any; 40 | deleteChild: any; 41 | changeFocusComponent: any; 42 | changeFocusChild: any; 43 | deleteComponent: any; 44 | createApp: any; 45 | deleteAllData: any; 46 | } 47 | 48 | interface StateInt { 49 | componentName: string; 50 | modal: any; 51 | genOptions: Array; 52 | genOption: number; 53 | } 54 | 55 | const mapStateToProps = (store: StoreInterface) => ({ 56 | components: store.workspace.components, 57 | storeConfig: store.workspace.storeConfig, 58 | }); 59 | 60 | const mapDispatchToProps = (dispatch: any) => ({ 61 | addComponent: ({ title }: { title: string }) => dispatch(actions.addComponent({ title })), 62 | addChild: ({ 63 | title, 64 | childType, 65 | HTMLInfo, 66 | }: { 67 | title: string; 68 | childType: string; 69 | HTMLInfo: object; 70 | }) => dispatch(actions.addChild({ title, childType, HTMLInfo })), 71 | deleteChild: (childId: number) => dispatch(actions.deleteChild(childId)), 72 | changeFocusComponent: ({ title }: { title: string }) => dispatch(actions.changeFocusComponent({ title })), 73 | changeFocusChild: ({ childId }: { childId: number }) => dispatch(actions.changeFocusChild({ childId })), 74 | deleteComponent: ({ 75 | componentId, 76 | stateComponents, 77 | }: { 78 | componentId: number; 79 | stateComponents: ComponentsInt; 80 | }) => dispatch(actions.deleteComponent({ componentId, stateComponents })), 81 | deleteAllData: () => dispatch(actions.deleteAllData()), 82 | createApp: ({ 83 | path, 84 | components, 85 | genOption, 86 | appName, 87 | exportAppBool, 88 | storeConfig, 89 | }: { 90 | path: string; 91 | components: ComponentsInt; 92 | genOption: number; 93 | appName: string; 94 | exportAppBool: boolean; 95 | storeConfig: StoreConfigInterface; 96 | }) => dispatch( 97 | actions.createApplication({ 98 | path, 99 | components, 100 | genOption, 101 | appName, 102 | exportAppBool, 103 | storeConfig, 104 | }), 105 | ), 106 | }); 107 | 108 | export class LeftContainer extends Component { 109 | state: StateInt; 110 | 111 | constructor(props: PropsInt) { 112 | super(props); 113 | 114 | this.state = { 115 | componentName: '', 116 | modal: null, 117 | genOptions: ['export components', 'export app files & components'], 118 | genOption: 0, 119 | }; 120 | } 121 | 122 | chooseGenOptions = (genOption: number) => { 123 | // set option 124 | this.setState({ genOption }); 125 | // closeModal 126 | this.closeModal(); 127 | // Choose app dir 128 | const { components, storeConfig } = this.props; 129 | // const { genOption } = this.state; 130 | const appName = 'preducksApp'; 131 | const exportAppBool = true; 132 | this.props.createApp({ 133 | path: '', 134 | components, 135 | genOption, 136 | appName, 137 | exportAppBool, 138 | storeConfig, 139 | }); 140 | 141 | }; 142 | 143 | handleChange = (event: any) => { 144 | if (event.target.value.length < 18) { 145 | const newValue: string = event.target.value; 146 | this.setState({ componentName: newValue }); 147 | } 148 | }; 149 | 150 | handleAddComponent = () => { 151 | this.props.addComponent({ title: this.state.componentName }); 152 | this.setState({ 153 | componentName: '', 154 | }); 155 | }; 156 | 157 | closeModal = () => this.setState({ modal: null }); 158 | 159 | clearWorkspace = () => { 160 | this.setState({ 161 | modal: createModal({ 162 | message: 'delete all data?', 163 | closeModal: this.closeModal, 164 | secBtnLabel: 'clear workspace', 165 | open: true, 166 | children: null, 167 | primBtnAction: null, 168 | primBtnLabel: null, 169 | secBtnAction: () => { 170 | this.props.deleteAllData(); 171 | this.closeModal(); 172 | }, 173 | }), 174 | }); 175 | }; 176 | 177 | showGenerateAppModal = () => { 178 | const { chooseGenOptions } = this; 179 | chooseGenOptions(1); 180 | }; 181 | 182 | render(): JSX.Element { 183 | const { 184 | components, 185 | deleteComponent, 186 | focusComponent, 187 | classes, 188 | addChild, 189 | deleteChild, 190 | changeFocusComponent, 191 | changeFocusChild, 192 | selectableChildren, 193 | } = this.props; 194 | const { componentName, modal } = this.state; 195 | 196 | const leftColExpansionPanels = cloneDeep(components) 197 | .sort((b: ComponentInt, a: ComponentInt) => b.id - a.id) // sort by id value of comp 198 | .map((component: ComponentInt, i: number) => ( 199 | 213 | )); 214 | 215 | const addComponent = ( 216 | 217 | 218 | { 226 | if (ev.key === 'Enter') { 227 | this.handleAddComponent(); 228 | ev.preventDefault(); 229 | } 230 | }} 231 | value={componentName} 232 | name="componentName" 233 | className={classes.light} 234 | InputProps={{ 235 | className: classes.input, 236 | }} 237 | InputLabelProps={{ 238 | className: classes.input, 239 | }} 240 | /> 241 | 242 | 243 | 250 | 251 | 252 | 253 | 254 | ); 255 | 256 | const clearAndExportButtons = ( 257 |
262 |
268 | 284 |
285 |
291 | 305 |
306 |
307 | ); 308 | 309 | return ( 310 |
311 | {addComponent} 312 |
{leftColExpansionPanels}
313 | 314 | {clearAndExportButtons} 315 | {modal} 316 |
317 | ); 318 | } 319 | } 320 | 321 | function styles(): any { 322 | return { 323 | cssLabel: { 324 | color: 'white', 325 | 326 | '&$cssFocused': { 327 | color: 'green', 328 | }, 329 | }, 330 | cssFocused: {}, 331 | input: { 332 | color: '#fff', 333 | opacity: '0.7', 334 | marginBottom: '10px', 335 | }, 336 | underline: { 337 | color: 'white', 338 | '&::before': { 339 | color: 'white', 340 | }, 341 | }, 342 | button: { 343 | color: '#fff', 344 | 345 | '&:disabled': { 346 | color: 'grey', 347 | }, 348 | }, 349 | clearButton: { 350 | top: '96%', 351 | position: 'sticky!important', 352 | zIndex: '1', 353 | 354 | '&:disabled': { 355 | color: 'grey', 356 | backgroundColor: '#424242', 357 | }, 358 | }, 359 | }; 360 | } 361 | 362 | export default compose( 363 | withStyles(styles), 364 | connect( 365 | mapStateToProps, 366 | mapDispatchToProps, 367 | ), 368 | )(LeftContainer); 369 | -------------------------------------------------------------------------------- /src/containers/MainContainer.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { MuiThemeProvider } from '@material-ui/core/styles'; 4 | import RightPanel from '../components/RightPanel'; 5 | import BottomPanel from '../components/BottomPanel'; 6 | import theme from '../components/theme'; 7 | import { 8 | handleTransform, 9 | changeFocusChild, 10 | changeComponentFocusChild, 11 | } from '../actions/components'; 12 | import { ComponentInt, ComponentsInt } from '../utils/InterfaceDefinitions'; 13 | import TreeDisplay from '../components/NewTreeDisplay'; 14 | 15 | interface PropsInt { 16 | components?: ComponentsInt; 17 | focusComponent?: ComponentInt; 18 | classes?: any; 19 | focusChild?: any; 20 | } 21 | 22 | interface StateInt { 23 | draggable: boolean; 24 | toggleClass: boolean; 25 | scaleX: number; 26 | scaleY: number; 27 | x: number; 28 | y: number; 29 | modal: any; 30 | windowWidth: 0; 31 | windowHeight: 0; 32 | } 33 | 34 | const mapDispatchToProps = (dispatch: any) => ({ 35 | handleTransformation: ( 36 | componentId: number, 37 | childId: number, 38 | { 39 | x, y, width, height, 40 | }: { x: number; y: number; width: number; height: number }, 41 | ) => dispatch( 42 | handleTransform(componentId, childId, { 43 | x, 44 | y, 45 | width, 46 | height, 47 | }), 48 | ), 49 | // openPanel: component => dispatch(openExpansionPanel(component)), 50 | changeFocusChild: ({ childId }: { childId: number }) => dispatch(changeFocusChild({ childId })), 51 | changeComponentFocusChild: ({ componentId, childId }: { componentId: number; childId: number }) => dispatch(changeComponentFocusChild({ componentId, childId })), // if u send no prms, function will delete focus child. 52 | }); 53 | 54 | const mapStateToProps = (store: any) => ({ 55 | focusComponent: store.workspace.focusComponent, 56 | focusChild: store.workspace.focusChild, 57 | components: store.workspace.components, 58 | }); 59 | 60 | class MainContainer extends Component { 61 | constructor(props) { 62 | super(props); 63 | this.state = { 64 | draggable: false, 65 | toggleClass: true, 66 | scaleX: 1, 67 | scaleY: 1, 68 | x: 0, 69 | y: 0, 70 | modal: '', 71 | windowWidth: 0, 72 | windowHeight: 0, 73 | }; 74 | this.updateWindowDimensions = this.updateWindowDimensions.bind(this); 75 | } 76 | 77 | componentDidMount() { 78 | this.updateWindowDimensions(); 79 | window.addEventListener('resize', this.updateWindowDimensions); 80 | } 81 | 82 | componentWillUnmount() { 83 | window.removeEventListener('resize', this.updateWindowDimensions); 84 | } 85 | 86 | updateWindowDimensions() { 87 | this.setState({ windowWidth: window.innerWidth, windowHeight: window.innerHeight }); 88 | } 89 | 90 | render() { 91 | const { modal } = this.state; 92 | const { 93 | components, focusComponent, focusChild, classes, 94 | } = this.props; 95 | const { main }: any = this; 96 | 97 | return ( 98 | 99 |
100 | {modal} 101 |
102 |
103 | 108 |
109 | {this.state.windowWidth >= 800 ? : ''} 110 |
111 | {this.state.windowWidth >= 800 ? : ''} 112 |
113 |
114 | ); 115 | } 116 | } 117 | 118 | export default connect( 119 | mapStateToProps, 120 | mapDispatchToProps, 121 | )(MainContainer); 122 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill'; 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | import { Provider } from 'react-redux'; 5 | import App from './components/App.tsx'; 6 | import store from './store'; 7 | 8 | ReactDOM.render( 9 | 10 | 11 | , 12 | document.getElementById('app'), 13 | ); 14 | -------------------------------------------------------------------------------- /src/localStorage.js: -------------------------------------------------------------------------------- 1 | import localforage from 'localforage'; 2 | 3 | const resetLocalForage = () => { 4 | // use this in place of saveState to fix any writing errors by refreshing localForage. 5 | localforage.setItem('state-v1.0.1', {}); 6 | }; 7 | 8 | export const saveState = (state) => { 9 | localforage.setItem('state-v1.0.1', state, (err) => { 10 | if (err) { 11 | console.log(`error saving state: ${err}`, state); 12 | } 13 | }); 14 | }; 15 | export const loadState = () => localforage.getItem('state-v1.0.1', (err) => { 16 | if (err) { 17 | console.log(`error loading state: ${err}`); 18 | } 19 | }); 20 | -------------------------------------------------------------------------------- /src/public/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Will Napier 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/public/README.md: -------------------------------------------------------------------------------- 1 | # preducks-website 2 | Website for preducks 3 | -------------------------------------------------------------------------------- /src/public/_headers: -------------------------------------------------------------------------------- 1 | [[headers]] 2 | for = "*.js" 3 | [headers.values] 4 | Cache-Control = "public, max-age=604800" -------------------------------------------------------------------------------- /src/public/icons/mac/preducks.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/preducks/1ef20cbca94cfebdc62ef99a86292269a7275052/src/public/icons/mac/preducks.icns -------------------------------------------------------------------------------- /src/public/icons/png/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/preducks/1ef20cbca94cfebdc62ef99a86292269a7275052/src/public/icons/png/128x128.png -------------------------------------------------------------------------------- /src/public/icons/png/16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/preducks/1ef20cbca94cfebdc62ef99a86292269a7275052/src/public/icons/png/16x16.png -------------------------------------------------------------------------------- /src/public/icons/png/24x24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/preducks/1ef20cbca94cfebdc62ef99a86292269a7275052/src/public/icons/png/24x24.png -------------------------------------------------------------------------------- /src/public/icons/png/256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/preducks/1ef20cbca94cfebdc62ef99a86292269a7275052/src/public/icons/png/256x256.png -------------------------------------------------------------------------------- /src/public/icons/png/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/preducks/1ef20cbca94cfebdc62ef99a86292269a7275052/src/public/icons/png/32x32.png -------------------------------------------------------------------------------- /src/public/icons/png/48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/preducks/1ef20cbca94cfebdc62ef99a86292269a7275052/src/public/icons/png/48x48.png -------------------------------------------------------------------------------- /src/public/icons/png/512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/preducks/1ef20cbca94cfebdc62ef99a86292269a7275052/src/public/icons/png/512x512.png -------------------------------------------------------------------------------- /src/public/icons/png/64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/preducks/1ef20cbca94cfebdc62ef99a86292269a7275052/src/public/icons/png/64x64.png -------------------------------------------------------------------------------- /src/public/icons/png/96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/preducks/1ef20cbca94cfebdc62ef99a86292269a7275052/src/public/icons/png/96x96.png -------------------------------------------------------------------------------- /src/public/icons/win/preducks.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/preducks/1ef20cbca94cfebdc62ef99a86292269a7275052/src/public/icons/win/preducks.ico -------------------------------------------------------------------------------- /src/public/images/TreeImageComponent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/preducks/1ef20cbca94cfebdc62ef99a86292269a7275052/src/public/images/TreeImageComponent.png -------------------------------------------------------------------------------- /src/public/images/file structure photo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/preducks/1ef20cbca94cfebdc62ef99a86292269a7275052/src/public/images/file structure photo.png -------------------------------------------------------------------------------- /src/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | preducks 12 | 13 | 14 | 15 | 19 | 20 | 21 |
 
22 |
23 | 24 | 25 | -------------------------------------------------------------------------------- /src/reducers/componentReducer.ts: -------------------------------------------------------------------------------- 1 | import { ComponentInt, ChildInt, ApplicationStateInt } from '../utils/InterfaceDefinitions'; 2 | 3 | import * as types from '../actionTypes'; 4 | 5 | import { 6 | addComponent, 7 | addChild, 8 | deleteChild, 9 | deleteComponent, 10 | changeFocusComponent, 11 | changeComponentFocusChild, 12 | changeFocusChild, 13 | exportFilesSuccess, 14 | exportFilesError, 15 | handleClose, 16 | handleTransform, 17 | openExpansionPanel, 18 | addProp, 19 | deleteProp, 20 | updateHtmlAttr, 21 | updateChildrenSort, 22 | addSelector, 23 | deleteSelector, 24 | addActionToComponent, 25 | deleteActionFromComponent, 26 | setReducer, 27 | deleteReducer, 28 | // renameReducer, 29 | setInterface, 30 | deleteInterface, 31 | // renameInterface, 32 | setState, 33 | deleteState, 34 | // renameState, 35 | } from '../utils/componentReducer.util'; 36 | import cloneDeep from '../utils/cloneDeep'; 37 | 38 | const appComponent: ComponentInt = { 39 | id: 1, 40 | stateful: false, 41 | componentState: [], 42 | title: 'App', 43 | color: '#FF6D00', 44 | props: [], 45 | nextPropId: 1, 46 | childrenArray: [], 47 | nextChildId: 1, 48 | focusChildId: 0, 49 | selectors: [], 50 | actions: [], 51 | }; 52 | 53 | const initialApplicationFocusChild: ChildInt = { 54 | childId: 0, 55 | componentName: null, 56 | childType: null, 57 | childSort: 0, 58 | childComponentId: 0, 59 | color: null, 60 | htmlElement: null, 61 | HTMLInfo: null, 62 | }; 63 | 64 | const initialApplicationState: ApplicationStateInt = { 65 | totalComponents: 1, 66 | nextId: 2, 67 | successOpen: false, 68 | errorOpen: false, 69 | focusComponent: appComponent, 70 | selectableChildren: [], 71 | ancestors: [], 72 | initialApplicationFocusChild, 73 | focusChild: cloneDeep(initialApplicationFocusChild), 74 | components: [appComponent], 75 | appDir: '', 76 | loading: false, 77 | storeConfig: { interfaces: {}, reducers: {} }, 78 | }; 79 | 80 | const componentReducer = (state = initialApplicationState, action: any) => { 81 | // console.log(action.type); 82 | switch (action.type) { 83 | case types.LOAD_INIT_DATA: 84 | // return { ...state }; 85 | return { 86 | ...state, 87 | ...action.payload.data, 88 | loading: false, 89 | appDir: '', 90 | successOpen: false, 91 | errorOpen: false, 92 | }; 93 | case types.ADD_COMPONENT: 94 | return addComponent(state, action.payload); 95 | case types.ADD_CHILD: 96 | return addChild(state, action.payload); 97 | case types.DELETE_CHILD: 98 | return deleteChild(state, action.payload); 99 | case types.DELETE_COMPONENT: 100 | return deleteComponent(state, action.payload); 101 | case types.CHANGE_FOCUS_COMPONENT: 102 | return changeFocusComponent(state, action.payload); 103 | case types.CHANGE_FOCUS_CHILD: 104 | return changeFocusChild(state, action.payload); 105 | case types.CHANGE_COMPONENT_FOCUS_CHILD: 106 | return changeComponentFocusChild(state, action.payload); 107 | case types.CREATE_APPLICATION: 108 | case types.EXPORT_FILES: 109 | return { ...state, loading: true }; 110 | case types.EXPORT_FILES_SUCCESS: 111 | return exportFilesSuccess(state, action.payload); 112 | case types.CREATE_APPLICATION_ERROR: 113 | case types.EXPORT_FILES_ERROR: 114 | return exportFilesError(state, action.payload); 115 | case types.HANDLE_CLOSE: 116 | return handleClose(state, action.payload); 117 | case types.HANDLE_TRANSFORM: 118 | return handleTransform(state, action.payload); 119 | case types.OPEN_EXPANSION_PANEL: 120 | return openExpansionPanel(state, action.payload); 121 | case types.DELETE_ALL_DATA: 122 | return initialApplicationState; 123 | case types.ADD_PROP: 124 | return addProp(state, action.payload); 125 | case types.DELETE_PROP: 126 | return deleteProp(state, action.payload); 127 | case types.UPDATE_HTML_ATTR: 128 | return updateHtmlAttr(state, action.payload); 129 | case types.UPDATE_CHILDREN_SORT: 130 | return updateChildrenSort(state, action.payload); 131 | case types.ADD_SELECTOR: 132 | return addSelector(state, action.payload); 133 | case types.DELETE_SELECTOR: 134 | return deleteSelector(state, action.payload); 135 | case types.ADD_ACTION_TO_COMPONENT: 136 | return addActionToComponent(state, action.payload); 137 | case types.DELETE_ACTION_FROM_COMPONENT: 138 | return deleteActionFromComponent(state, action.payload); 139 | case types.SET_REDUCER: 140 | return setReducer(state, action.payload); 141 | case types.DELETE_REDUCER: 142 | return deleteReducer(state, action.payload); 143 | case types.SET_INTERFACE: 144 | return setInterface(state, action.payload); 145 | case types.DELETE_INTERFACE: 146 | return deleteInterface(state, action.payload); 147 | // case RENAME_INTERFACE: 148 | // return renameInterface(state, action.payload); 149 | case types.SET_STATE: 150 | return setState(state, action.payload); 151 | case types.DELETE_STATE: 152 | return deleteState(state, action.payload); 153 | // case RENAME_STATE: 154 | // return renameState(state, action.payload); 155 | default: 156 | return state; 157 | } 158 | }; 159 | 160 | export default componentReducer; 161 | -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | 3 | import componentReducer from './componentReducer.ts'; 4 | 5 | const reducers = combineReducers({ 6 | workspace: componentReducer, 7 | }); 8 | 9 | export default reducers; 10 | -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | import throttle from 'lodash.throttle'; 2 | import { createStore, applyMiddleware } from 'redux'; 3 | import thunk from 'redux-thunk'; 4 | import reducers from './reducers/index'; 5 | import { saveState } from './localStorage'; 6 | 7 | const store = createStore(reducers, applyMiddleware(thunk)); 8 | 9 | store.subscribe(throttle(() => saveState(store.getState()), 1000)); 10 | 11 | export default store; 12 | -------------------------------------------------------------------------------- /src/utils/InterfaceDefinitions.ts: -------------------------------------------------------------------------------- 1 | export interface PropInt { 2 | id: number; 3 | key: string; 4 | value: string; 5 | required: boolean; 6 | type: string; 7 | } 8 | 9 | export interface ChildInt { 10 | childId: number; 11 | childSort: number; 12 | childType: string; 13 | childComponentId: number; 14 | componentName: string; 15 | color: string | null; // maybe optional instead, look up null vs undefined 16 | htmlElement: string | null; // maybe should be optional instead 17 | HTMLInfo: { [index: string]: string }; // replace with HTMLinfo specifics 18 | } 19 | 20 | export interface ChildrenInt extends Array {} 21 | 22 | export interface ComponentStateInterface { 23 | name: string; 24 | type: string; 25 | initialValue: any; 26 | } 27 | 28 | export interface ComponentInt { 29 | id: number; 30 | stateful: boolean; 31 | componentState: ComponentStateInterface[]; 32 | title: string; 33 | color: string; 34 | props: PropInt[]; 35 | nextPropId: number; 36 | childrenArray: ChildInt[]; 37 | nextChildId: number; 38 | focusChildId: number; 39 | selectors: string[]; 40 | actions: string[]; 41 | } 42 | 43 | export interface ComponentsInt extends Array {} 44 | 45 | export interface ApplicationStateInt { 46 | totalComponents: number; 47 | nextId: number; 48 | successOpen: boolean; 49 | errorOpen: boolean; 50 | focusComponent: ComponentInt; 51 | selectableChildren: number[]; 52 | ancestors: number[]; 53 | initialApplicationFocusChild: ChildInt; 54 | focusChild: ChildInt | ChildInt[] | { [key: string]: ChildInt }; 55 | components: ComponentsInt; 56 | appDir: string; 57 | loading: boolean; 58 | storeConfig: StoreConfigInterface; 59 | } 60 | 61 | export interface StoreInterface { 62 | workspace: ApplicationStateInt; 63 | } 64 | 65 | export interface ActionConfigInterface { 66 | parameter: { name: string; type: string; array: boolean }; 67 | payload: { type: string; array: boolean }; 68 | async: boolean; 69 | } 70 | 71 | export interface StateConfigInterface { 72 | type: string; 73 | array: boolean; 74 | initialValue: any; 75 | } 76 | 77 | export interface InterfacesInterface { 78 | [key: string]: { [key: string]: string }; 79 | } 80 | 81 | export interface ReducersInterface { 82 | [key: string]: { 83 | store: { [key: string]: StateConfigInterface }; 84 | actions: { [key: string]: ActionConfigInterface }; 85 | }; 86 | } 87 | 88 | export interface StoreConfigInterface { 89 | interfaces: InterfacesInterface; 90 | reducers: ReducersInterface; 91 | } 92 | 93 | export interface InputValidation { 94 | isValid: boolean, 95 | input: string, 96 | error?: string, 97 | } -------------------------------------------------------------------------------- /src/utils/cloneDeep.ts: -------------------------------------------------------------------------------- 1 | // index signatures 2 | function cloneDeep(value: { [key: string]: T } | T[] | T): { [key: string]: T } | T[] | T { 3 | if (Array.isArray(value)) { 4 | const result: any[] = []; 5 | value.forEach((el) => { 6 | if (typeof el === 'object') { 7 | result.push(cloneDeep(el)); 8 | } else { 9 | result.push(el); 10 | } 11 | }); 12 | return result; 13 | } 14 | if (typeof value === 'object' && value !== null) { 15 | const result: { [key: string]: any } = {}; 16 | Object.keys(value).forEach((key) => { 17 | if (typeof value[key] === 'object') { 18 | result[key] = cloneDeep(value[key]); 19 | } else { 20 | result[key] = value[key]; 21 | } 22 | }); 23 | return result; 24 | } 25 | return value; 26 | } 27 | 28 | export default cloneDeep; 29 | -------------------------------------------------------------------------------- /src/utils/colors.util.ts: -------------------------------------------------------------------------------- 1 | const colors: Array = [ 2 | '#E27D60', 3 | '#E3AFBC', 4 | '#E8A87C', 5 | '#C38D9E', 6 | '#41B3A3', 7 | '#D12FA2', 8 | '#F64C72', 9 | '#DAAD86', 10 | '#8EE4AF', 11 | '#5CDB95', 12 | '#7395AE', // light grey 13 | '#b90061', 14 | '#AFD275', 15 | '#45A29E', 16 | '#D79922', 17 | '#C5CBE3', // lightish grey 18 | '#FFCB9A', 19 | '#E98074', 20 | '#8860D0', 21 | '#5AB9EA', 22 | '#5860E9', 23 | '#84CEEB', 24 | '#61892F', 25 | ]; 26 | const getColor = (portion: number): string => colors[Math.floor(Math.random() * portion * colors.length)]; 27 | 28 | export default getColor; 29 | -------------------------------------------------------------------------------- /src/utils/componentRender.util.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ComponentInt, 3 | ComponentsInt, 4 | ChildInt, 5 | ChildrenInt, 6 | PropInt, 7 | ComponentStateInterface, 8 | } from './InterfaceDefinitions'; 9 | import cloneDeep from './cloneDeep'; 10 | import store from '../store'; 11 | 12 | const componentRender = (component: ComponentInt, components: ComponentsInt) => { 13 | const { 14 | childrenArray, 15 | title, 16 | props, 17 | selectors, 18 | actions, 19 | componentState, 20 | }: { 21 | childrenArray: ChildrenInt; 22 | title: string; 23 | props: PropInt[]; 24 | selectors: string[]; 25 | actions: string[]; 26 | componentState: ComponentStateInterface[]; 27 | } = component; 28 | function typeSwitcher(type: string) { 29 | switch (type) { 30 | case 'string': 31 | return 'string'; 32 | case 'number': 33 | return 'number'; 34 | case 'object': 35 | return 'object'; 36 | case 'array': 37 | return 'any[]'; 38 | case 'boolean': 39 | return 'boolean'; 40 | case 'function': 41 | return '() => any'; 42 | case 'node': 43 | return 'string'; 44 | case 'element': 45 | return 'string'; 46 | case 'tuple': 47 | return '[any]'; 48 | case 'enum': 49 | return '{}'; 50 | case 'any': 51 | return 'any'; 52 | default: 53 | return 'any'; 54 | } 55 | } 56 | 57 | function propDrillTextGenerator(child: ChildInt) { 58 | // probably don't need this 59 | if (child.childType === 'COMP') { 60 | return components 61 | .find((c: any) => c.id === child.childComponentId) 62 | .props.map((prop: PropInt) => `${prop.key}={${prop.value}}`) 63 | .join(' '); 64 | } 65 | if (child.childType === 'HTML') { 66 | const keys: string[] = Object.keys(child.HTMLInfo); 67 | return keys.map(key => `${key}={${htmlAttrSanitizer(child.HTMLInfo[key])}}`).join(' '); 68 | } 69 | return ''; 70 | } 71 | 72 | function htmlAttrSanitizer(element: string) { 73 | return `'${element}'`; 74 | } 75 | function componentNameGenerator(child: ChildInt) { 76 | if (child.childType === 'HTML') { 77 | switch (child.componentName) { 78 | case 'Image': 79 | return 'img'; 80 | case 'Form': 81 | return 'form'; 82 | case 'Button': 83 | return 'button'; 84 | case 'Link': 85 | return 'a'; 86 | case 'List': 87 | return 'ul'; 88 | case 'Paragraph': 89 | return 'p'; 90 | default: 91 | return 'div'; 92 | } 93 | } else { 94 | return child.componentName; 95 | } 96 | } 97 | 98 | const toImport = []; 99 | if (selectors.length) { 100 | toImport.push('useSelector'); 101 | } 102 | if (actions.length) { 103 | toImport.push('useDispatch'); 104 | } 105 | 106 | const importFromReactReduxText = toImport.length 107 | ? `import {${toImport.join(',')}} from 'react-redux'` 108 | : ''; 109 | 110 | const actionsToImport = {}; 111 | actions.forEach((action) => { 112 | const [reducer, actionName] = action.split('.'); 113 | if (!actionsToImport[reducer]) { 114 | actionsToImport[reducer] = [actionName]; 115 | } else { 116 | actionsToImport[reducer].push(actionName); 117 | } 118 | }); 119 | 120 | const actionsText = Object.keys(actionsToImport).map( 121 | reducer => `import {${actionsToImport[reducer].join(',')}} from '../actions/${reducer}Actions';`, 122 | ); 123 | 124 | const reservedTypeScriptTypes = [ 125 | 'string', 126 | 'boolean', 127 | 'number', 128 | 'any', 129 | 'string[]', 130 | 'boolean[]', 131 | 'number[]', 132 | 'any[]', 133 | ]; 134 | 135 | const listOfInterfaces = componentState.reduce((interfaces, current) => { 136 | if (!reservedTypeScriptTypes.includes(current.type) && !interfaces.includes(current.type)) { 137 | interfaces.push(current.type); 138 | } 139 | return interfaces; 140 | }, []); 141 | 142 | const state = store.getState().workspace.storeConfig.reducers; 143 | const useSelectorCalls = selectors.length 144 | ? selectors 145 | .map((selector) => { 146 | const selectorStrings = selector.split('.'); 147 | const variableName = selectorStrings[0] + selectorStrings[1][0].toUpperCase() + selectorStrings[1].slice(1); 148 | const properties = selector.split('.'); 149 | let returnType; 150 | if (properties.length === 2) { 151 | returnType = state[properties[0]].store[properties[1]].type; 152 | if (state[properties[0]].store[properties[1]].array) { 153 | returnType += '[]'; 154 | } 155 | } else { 156 | returnType = 'any'; 157 | } 158 | if ( 159 | !reservedTypeScriptTypes.includes(returnType) 160 | && !listOfInterfaces.includes(returnType) 161 | ) { 162 | listOfInterfaces.push( 163 | returnType.indexOf('[') !== -1 164 | ? returnType.slice(0, returnType.length - 2) 165 | : returnType, 166 | ); 167 | } 168 | return `const ${variableName} = useSelector(state => state.${selector});`; 169 | }) 170 | .join('\n') 171 | : ''; 172 | 173 | const interfacesToImport = listOfInterfaces.length 174 | ? `import {${listOfInterfaces.join(', ')}} from '../Interfaces'` 175 | : ''; 176 | 177 | const importsText = `${ 178 | componentState.length ? "import React, {useState} from 'react'" : "import React from 'react'" 179 | }; 180 | ${[ 181 | ...new Set( 182 | childrenArray 183 | .filter(child => child.childType !== 'HTML') 184 | .map(child => `import ${child.componentName} from './${child.componentName}';`), 185 | ), 186 | ].join('\n')} 187 | ${importFromReactReduxText} 188 | ${interfacesToImport} 189 | ${toImport.includes('useSelector') ? "import {StoreInterface} from '../reducers/index'" : ''} 190 | ${actions.length ? actionsText.join('\n') : ''} 191 | \n\n`; 192 | 193 | const childrenToRender = `
194 | ${cloneDeep(childrenArray) 195 | .sort((a: ChildInt, b: ChildInt) => a.childSort - b.childSort) 196 | .map( 197 | (child: ChildInt) => `<${componentNameGenerator(child)} ${propDrillTextGenerator(child)}/>`, 198 | ) 199 | .join('\n')}` + '
'; 200 | 201 | const useStateCalls = componentState.length 202 | ? componentState 203 | .map((pieceOfState: ComponentStateInterface) => { 204 | const initialValue = pieceOfState.type === 'string' 205 | ? `'${pieceOfState.initialValue}'` 206 | : pieceOfState.initialValue; 207 | return `const [${ 208 | pieceOfState.name 209 | }, set${pieceOfState.name[0].toUpperCase()}${pieceOfState.name.slice(1)}] = useState<${ 210 | pieceOfState.type 211 | }>(${initialValue});`; 212 | }) 213 | .join('\n') 214 | : ''; 215 | 216 | const functionalComponentBody = ` 217 | const ${title}:React.FC = (props: any):JSX.Element => { 218 | ${useStateCalls} 219 | ${useSelectorCalls} 220 | ${actions.length ? 'const dispatch = useDispatch();' : ''} 221 | return (${childrenToRender}); 222 | } 223 | export default ${title};`; 224 | return importsText + functionalComponentBody; 225 | }; 226 | 227 | export default componentRender; 228 | -------------------------------------------------------------------------------- /src/utils/createComponentFiles.util.ts: -------------------------------------------------------------------------------- 1 | import { formatter } from './formatter.util'; 2 | import componentRender from './componentRender.util'; 3 | 4 | const createComponentFiles = ( 5 | components: any, 6 | path: string, 7 | appName: string, 8 | exportAppBool: boolean, 9 | zip: any, 10 | ) => { 11 | components.forEach((component: any) => { 12 | zip.file( 13 | `src/components/${component.title}.tsx`, 14 | formatter(componentRender(component, components)), 15 | ); 16 | }); 17 | return path; 18 | }; 19 | 20 | export default createComponentFiles; 21 | -------------------------------------------------------------------------------- /src/utils/createModal.util.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import SimpleModal from '../components/SimpleModal'; 3 | 4 | type Props = { 5 | closeModal: any; 6 | primBtnAction: any; 7 | secBtnAction: any; 8 | open: boolean; 9 | children: any; 10 | message: string; 11 | primBtnLabel: any; 12 | secBtnLabel: any; 13 | }; 14 | 15 | const createModal = ({ 16 | message, 17 | closeModal, 18 | primBtnLabel, 19 | primBtnAction, 20 | secBtnAction = null, 21 | secBtnLabel = null, 22 | children = null, 23 | open = true, 24 | }: Props) => ( 25 | 33 | {children} 34 | 35 | ); 36 | 37 | export default createModal; 38 | -------------------------------------------------------------------------------- /src/utils/createReduxFiles.util.ts: -------------------------------------------------------------------------------- 1 | import { formatter } from './formatter.util'; 2 | import { StoreConfigInterface } from './InterfaceDefinitions'; 3 | 4 | const createSharedInterfaces = ( 5 | path: string, 6 | appName: string, 7 | storeConfig: StoreConfigInterface, 8 | zip: any, 9 | ): any => { 10 | const filePath: string = 'src/Interfaces.ts'; 11 | const interfaceObj = storeConfig.interfaces; 12 | const interfaceObjKeys = Object.keys(interfaceObj); 13 | if (interfaceObjKeys.length === 0) return; 14 | let data = ''; 15 | interfaceObjKeys.forEach((interfaceName) => { 16 | let curInterface = `export interface ${interfaceName} {\n`; 17 | Object.keys(interfaceObj[interfaceName]).forEach((property) => { 18 | const curType = interfaceObj[interfaceName][property]; 19 | curInterface += `${property}: ${curType};\n`; 20 | }); 21 | curInterface += '}\n\n'; 22 | data += curInterface; 23 | }); 24 | 25 | zip.file(filePath, formatter(data)); 26 | }; 27 | 28 | function createActionFiles(path, appName, storeConfig, reducerName, zip) { 29 | // CREATE ACTION TYPES STUFF ////////////// 30 | const reducerObj = storeConfig.reducers[reducerName]; 31 | const actionTypesFile: string = `src/actions/${reducerName}ActionTypes.ts`; 32 | const actions = Object.keys(reducerObj.actions); // action names in a list 33 | 34 | let actionInterfacesText = ''; 35 | let actionCreatorsText = ''; 36 | const actionInterfaceNames = []; 37 | if (actions.length === 0) return; 38 | actions.forEach((actionName, i) => { 39 | // loop through all actions and generate all text in actionTypes 40 | // and actions that depends on it. 41 | const curActionObj = reducerObj.actions[actionName]; 42 | const actionInterfaceName = `${actionName}ActionInterface`; 43 | actionInterfaceNames.push(actionInterfaceName); 44 | 45 | // add an action interface 46 | const bracketsIfPayloadIsAnArray = curActionObj.payload.array ? '[]' : ''; 47 | const payloadType = curActionObj.payload.type; 48 | const curActionInterface = `export interface ${actionInterfaceName}{\n 49 | type: ${reducerName}ActionTypes.${actionName}; 50 | payload?: ${payloadType}${bracketsIfPayloadIsAnArray}; 51 | }\n\n`; 52 | actionInterfacesText += curActionInterface; 53 | 54 | // add boilerplate for this action creator function 55 | let curActionCreatorText; 56 | const bracketsIfParamIsAnArray = curActionObj.parameter.array ? '[]' : ''; 57 | const paramWithColonIfParamExists = curActionObj.parameter.name 58 | ? `${curActionObj.parameter.name} : ` 59 | : ''; 60 | if (curActionObj.async) { 61 | curActionCreatorText = `export const ${actionName} = (${paramWithColonIfParamExists}${curActionObj.parameter.type}${bracketsIfParamIsAnArray}) => { 62 | return async (dispatch: Dispatch) => { 63 | // your code here ! add your own payload to the dispatched action. 64 | dispatch<${actionInterfaceName}>({ 65 | type: ${reducerName}ActionTypes.${actionName} 66 | }); 67 | }; 68 | };`; 69 | } else { 70 | curActionCreatorText = `export const ${actionName} = 71 | (${paramWithColonIfParamExists}${curActionObj.parameter.type}${bracketsIfParamIsAnArray}) : 72 | ${actionInterfaceName} => { 73 | // your code here ! add your own payload to the dispatched action. 74 | return { 75 | type: ${reducerName}ActionTypes.${actionName} 76 | }; 77 | };`; 78 | } 79 | 80 | actionCreatorsText += curActionCreatorText; 81 | }); 82 | 83 | // import all shared interfaces 84 | const interfaceNameArray = Object.keys(storeConfig.interfaces); 85 | const interfacesImportText = interfaceNameArray.length 86 | ? `import {${interfaceNameArray.toString()}} from '../Interfaces';\n\n` 87 | : ''; 88 | // exoirt an enum with all actions 89 | const actionTypesEnumText = `export enum ${reducerName}ActionTypes{${actions.toString()}};\n\n`; 90 | const typeGuardText = `export type ${reducerName}ActionInterfaceUnion = ${actionInterfaceNames.join( 91 | '|', 92 | )};\n\n`; 93 | 94 | const firstFormat = formatter(interfacesImportText + actionInterfacesText); 95 | 96 | const secondFormat = formatter(typeGuardText); 97 | 98 | zip.file(actionTypesFile, firstFormat + actionTypesEnumText + secondFormat); 99 | 100 | // // ///// ACTIONS STUFF ///////////////////////////// 101 | 102 | const actionsFile: string = `src/actions/${reducerName}Actions.ts`; 103 | // import dispatch, import the action types enum, and import ALL action interfaces 104 | const actionsImportText = `import {Dispatch} from 'redux'; 105 | import {${reducerName}ActionTypes, ${actionInterfaceNames.join(',')}} 106 | from './${reducerName}ActionTypes'\n`; 107 | zip.file(actionsFile, formatter(actionsImportText + interfacesImportText + actionCreatorsText)); 108 | } 109 | 110 | function createReducerFiles(path, appName, storeConfig, reducerName, zip) { 111 | const reducerFile: string = `src/reducers/${reducerName}Reducer.ts`; 112 | const currentReducerStoreSlice = storeConfig.reducers[reducerName].store; 113 | const currentReducerStoreSliceKeys = Object.keys(currentReducerStoreSlice); 114 | const numberOfActions = Object.keys(storeConfig.reducers[reducerName].actions).length; 115 | let importText = numberOfActions 116 | ? `import {${reducerName}ActionInterfaceUnion, ${reducerName}ActionTypes} from '../actions/${reducerName}ActionTypes'\n` 117 | : ''; 118 | const interfaceNameArray = Object.keys(storeConfig.interfaces); 119 | importText += interfaceNameArray.length 120 | ? `import {${interfaceNameArray.toString()}} from '../Interfaces';\n\n` 121 | : ''; 122 | 123 | let storeSliceInterfaceText = `export interface ${reducerName}StoreSliceInterface {`; 124 | const initialState = {}; 125 | currentReducerStoreSliceKeys.forEach((storeSlicePropertyName) => { 126 | // build out interfaces for this reducer's store slices first! 127 | const storeSlicePropObj = currentReducerStoreSlice[storeSlicePropertyName]; 128 | const bracketsIfPropIsAnArray = storeSlicePropObj.array ? '[]' : ''; 129 | storeSliceInterfaceText += `${storeSlicePropertyName}: ${storeSlicePropObj.type}${bracketsIfPropIsAnArray};\n`; 130 | try { 131 | const replacedStr = storeSlicePropObj.initialValue 132 | .replace(/(\w+) ?(:)/g, (wholeMatch, groupOne, groupTwo) => `"${groupOne}"${groupTwo}`) 133 | .replace(/'/g, '"'); 134 | initialState[storeSlicePropertyName] = JSON.parse(replacedStr); 135 | } catch (e) { 136 | initialState[storeSlicePropertyName] = storeSlicePropObj.initialValue; 137 | } 138 | }); 139 | storeSliceInterfaceText += '};\n\n'; 140 | const initialStateText = `const initialState: ${reducerName}StoreSliceInterface = ${JSON.stringify( 141 | initialState, 142 | )}\n\n`; 143 | 144 | let reducerText = `export const ${reducerName}Reducer = 145 | (state: ${reducerName}StoreSliceInterface = initialState, action: ${ 146 | numberOfActions ? `${reducerName}ActionInterfaceUnion` : 'any' 147 | }) => { 148 | switch(action.type){\n`; 149 | 150 | Object.keys(storeConfig.reducers[reducerName].actions).forEach((actionTypeName) => { 151 | reducerText += `case ${reducerName}ActionTypes.${actionTypeName}: 152 | // your logic here! 153 | return state;\n`; 154 | }); 155 | reducerText += `default: 156 | return state; 157 | } 158 | };`; 159 | 160 | zip.file( 161 | reducerFile, 162 | formatter(importText + storeSliceInterfaceText + initialStateText + reducerText), 163 | ); 164 | } 165 | 166 | const createActionsAndStoresForEachReducer = ( 167 | // creates an Interfaces.tsx that all other redux files will import from. 168 | path: string, 169 | appName: string, 170 | storeConfig: StoreConfigInterface, 171 | zip, 172 | ): void => { 173 | const rootReducerFile: string = 'src/reducers/index.ts'; 174 | let rootReducerImportsText = "import {combineReducers} from 'redux';\n"; 175 | let storeInterfaceText = 'export interface StoreInterface {\n'; 176 | let combineReducersText = 'export const reducers = combineReducers({\n'; 177 | 178 | const reducerNamesArray = Object.keys(storeConfig.reducers); 179 | if (reducerNamesArray.length === 0) return; 180 | reducerNamesArray.forEach((reducerName) => { 181 | rootReducerImportsText += `import {${reducerName}Reducer, ${reducerName}StoreSliceInterface } from './${reducerName}Reducer';\n`; 182 | storeInterfaceText += `${reducerName}: ${reducerName}StoreSliceInterface;\n`; 183 | combineReducersText += `${reducerName}: ${reducerName}Reducer,\n`; 184 | 185 | createActionFiles(path, appName, storeConfig, reducerName, zip); 186 | createReducerFiles(path, appName, storeConfig, reducerName, zip); 187 | }); 188 | rootReducerImportsText += '\n'; 189 | storeInterfaceText += '}\n\n'; 190 | combineReducersText += '});\n'; 191 | 192 | zip.file( 193 | rootReducerFile, 194 | formatter(rootReducerImportsText + storeInterfaceText + combineReducersText), 195 | ); 196 | }; 197 | 198 | export const createReduxFiles = async ( 199 | // CALL THIS FUNCTION FROM OUTSIDE AND IT WILL DO THE WHOLE REDUX SETUP. 200 | // creates an Interfaces.tsx that all other redux files will import from. 201 | path: string, 202 | appName: string, 203 | storeConfig: StoreConfigInterface, 204 | zip: any, 205 | ): Promise => { 206 | createSharedInterfaces(path, appName, storeConfig, zip); 207 | createActionsAndStoresForEachReducer(path, appName, storeConfig, zip); 208 | return null; 209 | }; 210 | -------------------------------------------------------------------------------- /src/utils/dummyData.ts: -------------------------------------------------------------------------------- 1 | import { StoreConfigInterface } from './InterfaceDefinitions'; 2 | 3 | export const dummyComponent = { 4 | id: 1, 5 | stateful: false, 6 | title: 'App', 7 | color: '#FF6D00', 8 | props: [], 9 | nextPropId: 2, 10 | componentState: [{ name: 'hej', type: 'string', initialValue: 'hej' }], 11 | childrenArray: [ 12 | { 13 | childId: 4, 14 | childSort: 4, 15 | childType: 'COMP', 16 | childComponentId: 7, 17 | componentName: 'Board', 18 | color: null, 19 | htmlElement: null, 20 | HTMLInfo: {}, 21 | }, 22 | { 23 | childId: 7, 24 | childSort: 7, 25 | childType: 'HTML', 26 | childComponentId: null, 27 | componentName: 'Image', 28 | color: null, 29 | htmlElement: 'Image', 30 | HTMLInfo: {}, 31 | }, 32 | ], 33 | nextChildId: 10, 34 | focusChildId: 7, 35 | focusChild: { 36 | childId: 7, 37 | childSort: 7, 38 | childType: 'HTML', 39 | childComponentId: null, 40 | componentName: 'Image', 41 | color: null, 42 | htmlElement: 'Image', 43 | HTMLInfo: {}, 44 | }, 45 | selectors: ['reducer1.property1', 'reducer2.property2'], 46 | actions: ['action1, action2'], 47 | }; 48 | 49 | export const dummyAllComponents = [ 50 | { 51 | id: 1, 52 | stateful: false, 53 | title: 'App', 54 | color: '#FF6D00', 55 | props: [], 56 | nextPropId: 2, 57 | childrenArray: [ 58 | { 59 | childId: 4, 60 | childSort: 4, 61 | childType: 'COMP', 62 | childComponentId: 7, 63 | componentName: 'Board', 64 | color: null, 65 | htmlElement: null, 66 | HTMLInfo: {}, 67 | }, 68 | { 69 | childId: 7, 70 | childSort: 7, 71 | childType: 'HTML', 72 | childComponentId: null, 73 | componentName: 'Image', 74 | color: null, 75 | htmlElement: 'Image', 76 | HTMLInfo: {}, 77 | }, 78 | ], 79 | nextChildId: 10, 80 | focusChildId: 7, 81 | focusChild: { 82 | childId: 7, 83 | childSort: 7, 84 | childType: 'HTML', 85 | childComponentId: null, 86 | componentName: 'Image', 87 | color: null, 88 | htmlElement: 'Image', 89 | HTMLInfo: {}, 90 | }, 91 | selectors: [], 92 | actions: [], 93 | }, 94 | { 95 | id: 7, 96 | stateful: false, 97 | title: 'Board', 98 | color: '#E3AFBC', 99 | props: [ 100 | { 101 | id: 1, 102 | key: 'isGameOver', 103 | value: 'false', 104 | required: true, 105 | type: 'boolean', 106 | }, 107 | ], 108 | nextPropId: 2, 109 | childrenArray: [ 110 | { 111 | childId: 2, 112 | childSort: 2, 113 | childType: 'COMP', 114 | childComponentId: 8, 115 | componentName: 'Box', 116 | color: null, 117 | htmlElement: null, 118 | HTMLInfo: {}, 119 | }, 120 | { 121 | childId: 6, 122 | childSort: 6, 123 | childType: 'COMP', 124 | childComponentId: 8, 125 | componentName: 'Box', 126 | color: null, 127 | htmlElement: null, 128 | HTMLInfo: {}, 129 | }, 130 | { 131 | childId: 5, 132 | childSort: 5, 133 | childType: 'COMP', 134 | childComponentId: 8, 135 | componentName: 'Box', 136 | color: null, 137 | htmlElement: null, 138 | HTMLInfo: {}, 139 | }, 140 | ], 141 | nextChildId: 8, 142 | focusChildId: 0, 143 | focusChild: { 144 | childId: 5, 145 | childSort: 5, 146 | childType: 'COMP', 147 | childComponentId: 8, 148 | componentName: 'Box', 149 | color: null, 150 | htmlElement: null, 151 | HTMLInfo: {}, 152 | }, 153 | selectors: [], 154 | actions: [], 155 | }, 156 | { 157 | id: 8, 158 | stateful: false, 159 | title: 'Box', 160 | color: '#8860D0', 161 | props: [ 162 | { 163 | id: 1, 164 | key: 'gameState', 165 | value: '["","","","","","","","",""]', 166 | required: true, 167 | type: 'array', 168 | }, 169 | ], 170 | nextPropId: 2, 171 | childrenArray: [], 172 | nextChildId: 1, 173 | focusChildId: -1, 174 | selectors: ['reducer1.property1', 'reducer2.property2', 'reducer3.property3'], 175 | actions: ['delet', 'add', 'chaeng'], 176 | }, 177 | ]; 178 | 179 | const storeConfigTicTacToe: StoreConfigInterface = { 180 | interfaces: {}, 181 | reducers: { 182 | game: { 183 | store: { 184 | boxVals: { type: 'string', array: true, initialValue: ['', '', '', '', '', '', '', ''] }, 185 | isGameOver: { type: 'boolean', array: false, initialValue: false }, 186 | }, 187 | actions: { 188 | toggle: { 189 | parameter: { name: 'boxId', type: 'number', array: false }, 190 | payload: { type: 'number', array: false }, 191 | async: false, 192 | }, 193 | }, 194 | }, 195 | }, 196 | }; 197 | 198 | const storeConfigTTTMultiReducer: StoreConfigInterface = { 199 | interfaces: { 200 | user: { name: 'string', password: 'string' }, 201 | userAndHiScore: { user: 'user', hiScore: 'number' }, 202 | }, 203 | reducers: { 204 | game: { 205 | store: { 206 | boxVals: { type: 'string', array: true, initialValue: ['', '', '', '', '', '', '', ''] }, 207 | isGameOver: { type: 'boolean', array: false, initialValue: false }, 208 | }, 209 | actions: { 210 | toggleBox: { 211 | parameter: { name: 'boxId', type: 'number', array: false }, 212 | payload: { type: 'number', array: false }, 213 | async: false, 214 | }, 215 | }, 216 | }, 217 | user: { 218 | store: { 219 | otherUsers: { 220 | type: 'user', 221 | array: true, 222 | initialValue: [{ name: 'bob', password: 'dole' }], 223 | }, 224 | name: { type: 'string', array: false, initialValue: 'dan' }, 225 | score: { type: 'number', array: false, initialValue: 12 }, 226 | }, 227 | actions: { 228 | addPoint: { 229 | parameter: { name: '', type: '', array: false }, 230 | payload: { type: 'number', array: false }, 231 | async: false, 232 | }, 233 | fetchHighScores: { 234 | parameter: { name: '', type: '', array: false }, 235 | payload: { type: 'boolean', array: false }, 236 | async: true, 237 | }, 238 | }, 239 | }, 240 | }, 241 | }; 242 | 243 | // example for todo app 244 | const storeConfigTodo: StoreConfigInterface = { 245 | // config at the global level for redux store/actions 246 | interfaces: { 247 | todo: { id: 'number', title: 'string', completed: 'boolean' }, 248 | }, 249 | reducers: { 250 | todos: { 251 | store: { 252 | todoArray: { type: 'todo', array: true, initialValue: [] }, 253 | allCompleted: { type: 'boolean', array: false, initialValue: false }, 254 | }, 255 | actions: { 256 | fetchTodos: { 257 | parameter: { name: '', type: '', array: false }, 258 | payload: { type: 'todo', array: true }, 259 | async: true, 260 | }, 261 | deleteTodo: { 262 | parameter: { name: 'id', type: 'number', array: false }, 263 | payload: { type: 'number', array: false }, 264 | async: false, 265 | }, 266 | }, 267 | }, 268 | }, 269 | }; 270 | 271 | // module.exports = { 272 | // dummyComponent, 273 | // dummyAllComponents, 274 | // storeConfigTTTMultiReducer, 275 | // storeConfigTicTacToe, 276 | // storeConfigTodo, 277 | // }; 278 | -------------------------------------------------------------------------------- /src/utils/formatter.util.ts: -------------------------------------------------------------------------------- 1 | import { format } from 'prettier/standalone.js'; 2 | import parserTypescript from 'prettier/parser-typescript'; 3 | 4 | export const formatter = text => format( 5 | text, 6 | { 7 | singleQuote: true, 8 | trailingComma: 'es5', 9 | bracketSpacing: true, 10 | jsxBracketSameLine: true, 11 | parser: 'typescript', 12 | plugins: [parserTypescript], 13 | }, 14 | (err) => { 15 | if (err) { 16 | console.log('error formatting code !', err.message); 17 | } 18 | }, 19 | ); 20 | -------------------------------------------------------------------------------- /src/utils/getSelectable.util.ts: -------------------------------------------------------------------------------- 1 | import { ComponentInt, ComponentsInt, ChildInt } from './InterfaceDefinitions'; 2 | 3 | interface getSelectableInt { 4 | [key: string]: Array; 5 | } 6 | 7 | function getSelectable(newFocusComponent: ComponentInt, components: ComponentsInt) { 8 | const focusComponentId = newFocusComponent.id; 9 | const componentsToCheck = components 10 | .map((comp: ComponentInt) => comp.id) 11 | .filter((id: number) => id !== focusComponentId); 12 | return findAncestors(components, [focusComponentId], componentsToCheck); 13 | } 14 | 15 | function findAncestors( 16 | components: ComponentsInt, 17 | currentCompArr: number[], 18 | componentsToCheck: number[], 19 | ancestors: Array = [], 20 | ): getSelectableInt { 21 | if (!currentCompArr.length) { 22 | return { 23 | ancestors, 24 | selectableChildren: componentsToCheck, 25 | }; 26 | } 27 | 28 | const newAncestors: Array = []; 29 | 30 | for (let i = 0; i < components.length; i++) { 31 | if (componentsToCheck.includes(components[i].id)) { 32 | const myChildren = components[i].childrenArray.map( 33 | (child: ChildInt) => child.childComponentId, 34 | ); 35 | 36 | const found = currentCompArr.filter((comp: any) => myChildren.includes(comp)); 37 | 38 | if (found.length) { 39 | ancestors.push(components[i].id); 40 | newAncestors.push(components[i].id); 41 | 42 | const indexToDelete = componentsToCheck.findIndex((c: number) => c === components[i].id); 43 | 44 | componentsToCheck.splice(indexToDelete, 1); 45 | } 46 | } 47 | } 48 | return findAncestors(components, newAncestors, componentsToCheck, ancestors); 49 | } 50 | 51 | export default getSelectable; 52 | -------------------------------------------------------------------------------- /src/utils/htmlElements.util.ts: -------------------------------------------------------------------------------- 1 | // HTML elements that the user can choose from 2 | interface htmlElementInt { 3 | width: number; 4 | height: number; 5 | attributes: Array; 6 | } 7 | 8 | interface htmlElementsInt { 9 | [key: string]: htmlElementInt; 10 | } 11 | 12 | const HTMLelements: htmlElementsInt = { 13 | image: { 14 | width: 100, 15 | height: 100, 16 | attributes: ['className', 'id', 'src'], 17 | }, 18 | button: { 19 | width: 75, 20 | height: 28, 21 | attributes: ['className', 'id', 'text'], 22 | }, 23 | form: { 24 | width: 150, 25 | height: 150, 26 | attributes: ['className', 'id', 'text'], 27 | }, 28 | paragraph: { 29 | width: 250, 30 | height: 75, 31 | attributes: ['className', 'id', 'text'], 32 | }, 33 | list: { 34 | width: 75, 35 | height: 75, 36 | attributes: ['className', 'id', 'text'], 37 | }, 38 | link: { 39 | width: 50, 40 | height: 50, 41 | attributes: ['className', 'id', 'href', 'text'], 42 | }, 43 | }; 44 | 45 | export { HTMLelements }; 46 | -------------------------------------------------------------------------------- /src/utils/validateInput.util.ts: -------------------------------------------------------------------------------- 1 | const validateInput = (input: string = '') => { 2 | let inputValue = input 3 | .replace(/\s([a-z])/gi, el => el.toUpperCase()) 4 | .replace(/\s/g, ''); 5 | let error; 6 | const regex = /^([a-z$_]{1})[a-z0-9$_]+$/i; 7 | if (inputValue === '') { 8 | return { 9 | isValid: false, 10 | input: inputValue, 11 | } 12 | } 13 | if (regex.test(inputValue)) { 14 | if (reservedWords.indexOf(inputValue) === -1) { 15 | return { 16 | isValid: true, 17 | input: inputValue, 18 | }; 19 | } 20 | error = `“${inputValue}” is a reserved word`; 21 | } 22 | return { 23 | isValid: false, 24 | input: inputValue, 25 | error: error || `Invalid input “${inputValue}”`, 26 | }; 27 | } 28 | 29 | const reservedWords = [ 30 | 'abstract', 31 | 'arguments', 32 | 'await', 33 | 'boolean', 34 | 'break', 35 | 'byte', 36 | 'case', 37 | 'catch', 38 | 'char', 39 | 'class', 40 | 'const', 41 | 'continue', 42 | 'debugger', 43 | 'default', 44 | 'delete', 45 | 'do', 46 | 'double', 47 | 'else', 48 | 'enum', 49 | 'eval', 50 | 'export', 51 | 'extends', 52 | 'false', 53 | 'final', 54 | 'finally', 55 | 'float', 56 | 'for', 57 | 'function', 58 | 'goto', 59 | 'if', 60 | 'implements', 61 | 'import', 62 | 'in', 63 | 'instanceof', 64 | 'int', 65 | 'interface', 66 | 'let', 67 | 'long', 68 | 'native', 69 | 'new', 70 | 'null', 71 | 'package', 72 | 'private', 73 | 'protected', 74 | 'public', 75 | 'return', 76 | 'short', 77 | 'static', 78 | 'super', 79 | 'switch', 80 | 'synchronized', 81 | 'this', 82 | 'throw', 83 | 'throws', 84 | 'transient', 85 | 'true', 86 | 'try', 87 | 'typeof', 88 | 'var', 89 | 'void', 90 | 'volatile', 91 | 'while', 92 | 'with', 93 | 'yield', 94 | // Not reserved, but not suggested: 95 | 'Array', 96 | 'Date', 97 | 'eval', 98 | 'function', 99 | 'hasOwnProperty', 100 | 'Infinity', 101 | 'isFinite', 102 | 'isNaN', 103 | 'isPrototypeOf', 104 | 'length', 105 | 'Math', 106 | 'NaN', 107 | 'name', 108 | 'Number', 109 | 'Object', 110 | 'prototype', 111 | 'String', 112 | 'toString', 113 | 'undefined', 114 | 'valueOf', 115 | ]; 116 | 117 | export default validateInput; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./build/", 4 | "sourceMap": true, 5 | "noImplicitAny": false, 6 | "module": "commonjs", 7 | "target": "es6", 8 | "jsx": "react", 9 | "lib": ["es6", "dom", "es2017"], 10 | "allowSyntheticDefaultImports": true, 11 | "moduleResolution": "node", 12 | "baseUrl": "./src" 13 | }, 14 | "include": [ 15 | "./src/**/*", 16 | "__tests__/reducer.test.js", 17 | "__tests__/puppeteer.test.js" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint:recommended", "tslint-react", "tslint-config-prettier"], 3 | "tslint.autoFixOnSave": true, 4 | "linterOptions": { 5 | "exclude": ["config/**/*.js", "node_modules/**/*.ts"] 6 | }, 7 | "rules": { 8 | "quotemark": [true, "single", "avoid-escape", "avoid-template", "jsx-double"], 9 | "jsx-boolean-value": false, 10 | "jsx-no-lambda": false, 11 | "jsx-no-multiline-js": false, 12 | "object-literal-sort-keys": false, 13 | "member-ordering": false, 14 | "no-console": false, 15 | "ordered-imports": false, 16 | "comment-format": false 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /webpack.common.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 4 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 5 | 6 | const SRC_DIR = path.join(__dirname, 'src'); 7 | 8 | const indexCss = new ExtractTextPlugin('index.css'); 9 | 10 | const argv = require('minimist')(process.argv.slice(2)); 11 | 12 | const targetOption = argv.target; 13 | // const output = targetOption === 'web' ? 'build/web' : 'build/electron'; 14 | const BUILD_DIR = path.join(__dirname, 'build'); 15 | 16 | const options = { 17 | entry: { index: ['babel-polyfill', './index.js'] }, 18 | target: targetOption, 19 | output: { 20 | path: BUILD_DIR, 21 | filename: 'js/[name].js', 22 | pathinfo: false, 23 | }, 24 | node: { 25 | fs: 'empty', 26 | __dirname: false, 27 | __filename: false, 28 | }, 29 | context: SRC_DIR, 30 | resolve: { 31 | extensions: ['.ts', '.tsx', '.js', '.jsx'], 32 | }, 33 | plugins: [ 34 | indexCss, 35 | new HtmlWebpackPlugin({ 36 | title: 'app', 37 | filename: 'index.html', 38 | template: 'public/index.html', 39 | chunks: ['index'], 40 | }), 41 | new CopyWebpackPlugin([{ from: './public/', to: '' }]), 42 | ], 43 | module: { 44 | rules: [ 45 | { test: /\.ts$/, exclude: /node-modules/, loader: 'babel-loader' }, 46 | { test: /\.tsx$/, exclude: /node-modules/, loader: 'babel-loader' }, 47 | { 48 | test: /\.(js|jsx)$/, 49 | exclude: /node_modules/, 50 | use: ['babel-loader'], 51 | }, 52 | { 53 | test: /index.css$/, 54 | use: indexCss.extract({ 55 | use: 'css-loader', 56 | }), 57 | }, 58 | ], 59 | }, 60 | }; 61 | module.exports = options; 62 | -------------------------------------------------------------------------------- /webpack.dev.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable linebreak-style */ 2 | const path = require('path'); 3 | const webpack = require('webpack'); 4 | const merge = require('webpack-merge'); 5 | 6 | const argv = require('minimist')(process.argv.slice(2)); 7 | 8 | const common = require('./webpack.common.js'); 9 | 10 | const targetOption = argv.target; 11 | const output = targetOption === 'web' ? 'build/web' : 'build/electron'; 12 | const BUILD_DIR = path.join(__dirname, output); 13 | 14 | module.exports = merge(common, { 15 | devServer: { 16 | contentBase: BUILD_DIR, 17 | hot: true, 18 | compress: true, 19 | }, 20 | mode: 'development', 21 | devtool: 'eval-source-map', 22 | plugins: [new webpack.HotModuleReplacementPlugin()], 23 | watch: true, 24 | }); 25 | -------------------------------------------------------------------------------- /webpack.prod.js: -------------------------------------------------------------------------------- 1 | const merge = require('webpack-merge'); 2 | const common = require('./webpack.common.js'); 3 | 4 | module.exports = merge(common, { 5 | mode: 'production', 6 | optimization: { 7 | usedExports: true, 8 | }, 9 | }); 10 | --------------------------------------------------------------------------------