├── .env ├── .eslintrc ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── app ├── docs ├── technical-introduction.md └── user-documentation.md ├── jsconfig.json ├── package-lock.json ├── package.json ├── public ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon-96x96.png ├── favicon.ico ├── index.html └── social.png └── src ├── app.js ├── components ├── AddComponentTooltip │ ├── ContentWrapper.js │ └── index.js ├── Backtick.js ├── Buttons │ ├── AddButton.js │ ├── GithubButton.js │ ├── IssueOnGithub.js │ ├── StarOnGithub.js │ └── index.js ├── ClassComponent │ ├── components │ │ ├── ClassMethod.js │ │ ├── ClassProperty.js │ │ ├── ClassPropertyDeclarationFactory.js │ │ └── index.js │ └── index.js ├── ExportAppButton.js ├── Flex.js ├── Icons │ ├── FolderIcon.js │ ├── HumanEditRedoIcon.js │ ├── HumanEditUndoIcon.js │ ├── JSONIcon.js │ ├── PlusIcon.js │ ├── ReactIcon.js │ ├── SCIcon.js │ └── index.js ├── Input.js ├── JSX │ └── index.js ├── Json.js ├── Keyword.js ├── Logo │ ├── index.js │ └── logo.png ├── MapInvocation │ └── index.js ├── ObjectLiteral │ └── index.js ├── ParamInvocation.js ├── ProjectIndexDeclaration │ └── index.js ├── PropTypes │ └── index.js ├── Props │ ├── components │ │ ├── Prop.js │ │ ├── SpreadProps.js │ │ └── index.js │ ├── containers │ │ ├── PropDragContainer.js │ │ ├── SpreadPropsContainer.js │ │ └── index.js │ ├── helpers.js │ └── index.js ├── Semi.js ├── Standard.js ├── StatelessFunctionComponent │ └── index.js ├── StyledComponent │ ├── Pre.js │ ├── TemplateStringTextArea.js │ ├── TextAreaActivator.js │ └── index.js ├── VarInvocation │ └── index.js ├── Workspace │ ├── Divider.js │ ├── Embed.js │ └── index.js └── index.js ├── configureStore.js ├── constantz.js ├── containers ├── AperitifPostContainer │ ├── AperitifPost.js │ ├── apiArrayResponse.json │ ├── apiObjectResponse.json │ ├── examples.js │ ├── getJSON.js │ ├── gh-issues.png │ ├── index.js │ ├── nothingtodo.json │ ├── nothingtodo.png │ ├── twitter.json │ └── twitter.png ├── App │ └── index.js ├── ComponentInvocationContainer │ ├── components │ │ ├── CIDropzones.js │ │ ├── CallParam.js │ │ ├── CloseTag.js │ │ ├── ComponentInvocation.js │ │ ├── InvocationChildren.js │ │ ├── OpenTag.js │ │ ├── PropDropzone.js │ │ ├── ReorderDropzone.js │ │ └── index.js │ ├── containers │ │ ├── AddInvocationFromFileDropzone.js │ │ ├── CallParamDragContainer.js │ │ ├── IntermediaryDropzonesContainer.js │ │ ├── OpenTagContainer.js │ │ ├── PropDropzoneContainer.js │ │ ├── ReorderDropzoneContainer.js │ │ └── index.js │ ├── getCIDimensionsInjector.js │ ├── helpers.js │ ├── index.js │ └── makeSelectInvocation.js ├── ComponentTypeToggle.js ├── DeclParams │ ├── index.js │ └── makeSelectDeclParams.js ├── DeclarationContainer.js ├── DraggableDeclaration.js ├── EditorContainer │ ├── components │ │ ├── DefaultExport.js │ │ ├── Editor.js │ │ ├── Imports.js │ │ └── index.js │ ├── index.js │ └── selectors │ │ ├── getCurrentFileDefaultExport.js │ │ ├── getCurrentFileImports.js │ │ ├── index.js │ │ ├── selectCurrentFileDeclarations.js │ │ └── selectors.js ├── FileExplorerContainer │ ├── components │ │ ├── File.js │ │ ├── FileExplorer.js │ │ ├── FileIcon.js │ │ └── index.js │ ├── containers │ │ ├── AddComponentButton.js │ │ ├── AddContainerButton.js │ │ ├── FileContainer.js │ │ └── index.js │ └── index.js ├── InvocationContainer.js ├── KeyPressListeners │ └── index.js ├── Name.js ├── NameInput.js ├── NotFoundPage │ ├── index.js │ └── messages.js ├── ParamInvocationContainer │ ├── index.js │ └── makeSelectParamInvocation.js ├── PropTypesContainer.js ├── PropsContainer.js ├── SelectedThemeProvider │ ├── index.js │ └── themes │ │ ├── default.js │ │ └── index.js ├── StopDropTarget │ └── index.js ├── ToTextContainer │ ├── ToText.js │ ├── copyNodeText.js │ ├── index.js │ ├── indexHtml.js │ └── postProcessFileTree.js ├── WorkspaceContainer │ ├── download.js │ ├── embedStackBlitz.js │ ├── embedUpdate.js │ ├── getStackBlitzProjectDef.js │ ├── index.js │ ├── makeReduxStackblitzUpdateReconciler.js │ └── toStackBlitz.js └── index.js ├── duck ├── duck.js ├── editor.js ├── index.js ├── preferences.js └── tasks │ ├── addNewContainer.js │ ├── createComponentBundle.js │ ├── getInitialState.js │ ├── getRecursiveInvocationAndMaybeRelatedEntitiesRemover.js │ ├── getRecursivePropRemover.js │ ├── index.js │ └── initializeFromData.js ├── global-styles.js ├── i18n.js ├── index.js ├── middleware ├── index.js └── spyMiddleware.js ├── model-prop-types.js ├── orm ├── Model.js ├── index.js ├── models │ ├── CallParam.js │ ├── DeclParam.js │ ├── Declaration.js │ ├── File.js │ ├── Invocation.js │ ├── Name.js │ └── index.js ├── orm.js └── orm.test.js ├── registerServiceWorker.js ├── selectors.js ├── styleUtils.js ├── theme-proxy.js ├── translations ├── de.json ├── en.json └── fr.json └── utils ├── camelcase.js └── index.js /.env: -------------------------------------------------------------------------------- 1 | PORT=3002 2 | NODE_ENV=development 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "react-app", 4 | "airbnb" 5 | ], 6 | "parserOptions": { 7 | "ecmaVersion": 7, 8 | "sourceType": "module" 9 | }, 10 | "rules": { 11 | "comma-dangle": ["error", "always-multiline"], 12 | "consistent-return": "off", 13 | "object-curly-newline": "off", 14 | "function-paren-newline": "off", 15 | "jsx-a11y/href-no-hash": "off", 16 | "import/prefer-default-export": "off", 17 | "import/no-unresolved": "off", 18 | "import/extensions": "off", 19 | "no-plusplus": "off", 20 | "no-shadow": "off", 21 | "no-confusing-arrow": "off", 22 | "no-use-before-define": ["error", { "variables": false, "functions": false } ], 23 | "arrow-parens": "off", 24 | "react/jsx-filename-extension": "off", 25 | "react/prefer-stateless-function": "off", 26 | "react/prop-types": "off", 27 | "react/no-unescaped-entities": "off", 28 | "react/no-find-dom-node": "off", 29 | "react/sort-comp": "off", 30 | "react/no-unused-prop-types": "off", 31 | "semi": ["error", "never"], 32 | "jsx-a11y/click-events-have-key-events": "off", 33 | "jsx-a11y/no-static-element-interactions": "off", 34 | "jsx-a11y/mouse-events-have-key-events": "off", 35 | "jsx-a11y/iframe-has-title": "off" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | .env 3 | 4 | # dependencies 5 | /node_modules 6 | 7 | # testing 8 | /coverage 9 | 10 | # production 11 | /build 12 | 13 | # misc 14 | .DS_Store 15 | .env.local 16 | .env.development.local 17 | .env.test.local 18 | .env.production.local 19 | 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "stable" 5 | 6 | script: 7 | - npm run test 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Daniel Robinson 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |

Aperitif Editor

3 | 4 | 5 |

6 | 7 |

8 | 🍸🍹 Start React apps and features based on API data. 9 |
10 |
11 | Join the community on Spectrum 12 | Build Status 13 | 14 | tweet button 16 | 17 |
18 |
19 |

20 | 21 | 22 | ## What does it do? 23 | 24 | Aperitif uses JSON you provide and a drag and drop interface to help you quickly flesh out an app or feature intelligently. It allows you to interact with React code by dragging things like props, files, and components to quickly create a component tree, as well as adding styles via styled-components. 25 | 26 | For a longer introduction, [see the medium post announcing Aperitif](https://medium.com/@danielrob/introducing-aperitif-the-react-app-or-feature-starter-editor-4d7e392bd887). 27 | 28 | ### How can I use it? 29 | 30 | - It's deployed at [https://aperitif.netlify.com/](https://aperitif.netlify.com/) 31 | - or clone this repo npm install and run `npm start` 32 | 33 | ### How do I use it? 34 | 35 | The major editor interactions are to drag, rename, toggle (click), and style. It will help you if you understand React, but Aperitif editor can also be a fun way to learn about React. In either case you can see the [full user documentation](./docs/user-documentation.md) for further details. Some less obvious interactions are: 36 | - Remove props/components by dragging them onto a whitespace area of the editor. 37 | - Use standard keyboard shortcuts to undo and redo. 38 | - Drag files to component children to add another instance of that component. 39 | - Toggle semicolons by clicking at the end of any statement. 40 | - Toggle components to class syntax by clicking on `const` and back (if poss) by clicking on `class` 41 | - Drag a styled component file onto the editor of an open index.js that's in the same parent folder to combine files. 42 | 43 | #### Not all JSON is equal 44 | Aperitif currently works better when the provided JSON maps reasonably cleanly to the desired UI. There are some improvements in the pipeline for this, for example destructuring props more deeply. The workaround is to create uneeded layers of components and delete them after exporting. 45 | 46 | ## How does it work? 47 | Under the hood Aperitif manages a normalized redux store using a custom ORM inspired by [redux-orm](https://github.com/tommikaikkonen/redux-orm). The models are rendered as selectable text with all the associated (drag and drop) functionality. For text export, each file is rendered to an invisible dom element, and the browsers text selection api is used to copy the text for that file, adding it to the export. For a fuller introduction see the [technical introduction](./docs/technical-introduction.md) in the docs. 48 | 49 | ## Where too from here? 50 | React code is highly structured. Pure business logic generally lives inside pure functions, in component class methods, or outside of the component tree e.g. reducers. This represents a lot of oppurtunity to strongly assist developers with managing the React code itself and to treat components at a higher level of abstraction than raw code. Aperitif offers a feeling that components are entities that can be thrown together like lego. This is great. It could be extended considerably. Is Aperitif (in meta naming fashion) a taster for it's future self, or possibly for a class of tools like this in general? 51 | 52 | ## Contributing 53 | This is your project now! You are absolutely welcomed to get involved, even if that means asking questions, editing one character in the docs, or reporting issues. If you have ideas, browse the issues to see that it isn't already there, and add them in. Swing by the [spectrum community](https://spectrum.chat/aperitif-editor) for all of the above if you prefer. 54 | 55 | 56 | ### License 57 | MIT license 58 | -------------------------------------------------------------------------------- /app: -------------------------------------------------------------------------------- 1 | ./src/ -------------------------------------------------------------------------------- /docs/technical-introduction.md: -------------------------------------------------------------------------------- 1 | # Technical Introduction 2 | 3 | ## Overview 4 | Under the hood Aperitif manages a normalized redux store using a custom ORM inspired by [redux-orm](https://github.com/tommikaikkonen/redux-orm). The models are rendered as selectable text with all the associated (drag and drop) functionality. For text export, each file is rendered to an invisible dom element, and the browsers text selection api is used to copy the text for that file, adding it to the export. This export is created and sent to the StackBlitz embed in a debounced fashion on most redux-actions. 5 | 6 | ## Models 7 | The model enties are `Declaration`, `DeclParam`, `Invocation`, `CallParam`, `Name` and `File`. The model relationships when applied to React code are such that a `DeclParam` (e.g. prop) on a component `Declaration` should only exist if one of the `Invocation`s of that component has a `CallParam` with the same `nameId`. Similarly, a component `Declaration` and it's related `File`(s) will generally only exist if there is an `Invocation` of that component somewhere. If the last invocation is removed for instance, the related `Declaration` and it's `File`s should also be removed. 8 | 9 | So far these six models have been enough to represent all of the code Aperitif can render in a suitable fashion. 10 | 11 | ## ORM 12 | When it was considered, redux-orm lacked support for create-react-app without ejecting, and didn't particularly handle bidirectional relationships very well. Much of the api was copied in Aperitif's reimplementation, though this has drifted with time. 13 | 14 | Conceptually, you can think of Aperitif's implementation more as an immutablity helper that also manages relationships. When kicking off a session for instance, you simply provide the orm with the current session data, and extract the models which will act as helpers on their designated keys in the provided data. 15 | 16 | ``` 17 | const state = { files: {...}, declarations: {...}, ... } 18 | const session = orm.session(state) 19 | const { File, Declaration } = session // now have helpers to manage `files` and `declarations`. 20 | ``` 21 | 22 | One of the eccentric aspects of the implementation is that the `Model`s made available by the orm are for the most part singletons _and they can only have one 'result' in context at any given time_. This means that you may occasionaly see e.g. 23 | 24 | ``` 25 | Name.withId(2) 26 | Name.value // name with id 2 is in context, so this will return it's value 27 | ``` 28 | 29 | Links: 30 | - Almost all of the ORM implementation is the Model class itself - see `src/orm/Model.js`. 31 | - View `src/orm/models` to see the specific model type declarations. 32 | 33 | ## To be continued... -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6", 4 | "moduleResolution": "node", 5 | "baseUrl": "./src" 6 | }, 7 | "include": [ 8 | "src/**/*" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aperitif", 3 | "description": "🍸🍹 An editor for starting React apps and features based on API data", 4 | "version": "0.1.0", 5 | "private": true, 6 | "dependencies": { 7 | "@stackblitz/sdk": "^1.2.0", 8 | "airbnb-prop-types": "^2.10.0", 9 | "browser-locale": "^1.0.3", 10 | "check-types": "^7.3.0", 11 | "classnames": "^2.2.5", 12 | "file-saver": "^1.3.8", 13 | "flat": "^4.0.0", 14 | "history": "^4.7.2", 15 | "invariant": "^2.2.4", 16 | "javascript-stringify": "^1.6.0", 17 | "jszip": "^3.1.5", 18 | "lodash": "^4.17.10", 19 | "pluralize": "^7.0.0", 20 | "prop-types": "^15.6.1", 21 | "react": "^16.4.0", 22 | "react-autosize-textarea": "^3.0.3", 23 | "react-dnd": "^2.6.0", 24 | "react-dnd-html5-backend": "^2.6.0", 25 | "react-dom": "^16.4.0", 26 | "react-helmet": "^5.2.0", 27 | "react-input-autosize": "^2.2.1", 28 | "react-intl": "^2.4.0", 29 | "react-json-syntax-highlighter": "^0.2.0", 30 | "react-redux": "^5.0.7", 31 | "react-router-dom": "^4.2.2", 32 | "react-router-redux": "^5.0.0-alpha.9", 33 | "react-scripts": "1.1.4", 34 | "react-tooltip": "^3.6.1", 35 | "redux": "^4.0.0", 36 | "redux-actions": "^2.4.0", 37 | "redux-localstorage-simple": "^2.1.1", 38 | "redux-undo": "^1.0.0-beta9-9-7", 39 | "reselect": "^3.0.1", 40 | "sanitize.css": "^5.0.0", 41 | "styled-as-components": "*", 42 | "styled-components": "^3.3.2", 43 | "typeface-oxygen-mono": "0.0.54" 44 | }, 45 | "scripts": { 46 | "start": "NODE_PATH=src/ react-scripts start", 47 | "build": "NODE_PATH=src/ react-scripts build", 48 | "test": "NODE_PATH=src/ react-scripts test --env=jsdom", 49 | "eject": "NODE_PATH=src/ react-scripts eject" 50 | }, 51 | "devDependencies": { 52 | "eslint": "^4.19.1", 53 | "eslint-config-airbnb": "^16.1.0", 54 | "eslint-plugin-import": "^2.12.0", 55 | "eslint-plugin-jsx-a11y": "^6.0.3", 56 | "eslint-plugin-react": "^7.9.1" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielrob/aperitif-editor/3b1f2b71f2dae208b6f3f4e04b7bf0c552395c52/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielrob/aperitif-editor/3b1f2b71f2dae208b6f3f4e04b7bf0c552395c52/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielrob/aperitif-editor/3b1f2b71f2dae208b6f3f4e04b7bf0c552395c52/public/favicon-96x96.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielrob/aperitif-editor/3b1f2b71f2dae208b6f3f4e04b7bf0c552395c52/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 28 | 29 | 30 | 31 | 32 | 33 | 42 | 43 | 44 | 47 |
48 | 49 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /public/social.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielrob/aperitif-editor/3b1f2b71f2dae208b6f3f4e04b7bf0c552395c52/public/social.png -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import { Provider } from 'react-redux' 4 | import { ConnectedRouter } from 'react-router-redux' 5 | import { IntlProvider } from 'react-intl' 6 | import createHistory from 'history/createBrowserHistory' 7 | import 'sanitize.css/sanitize.css' 8 | import getBrowserLocale from 'browser-locale' 9 | import { DragDropContext } from 'react-dnd' 10 | import HTML5Backend from 'react-dnd-html5-backend' 11 | import 'typeface-oxygen-mono' 12 | 13 | import { App, SelectedThemeProvider } from 'containers' 14 | 15 | import registerServiceWorker from './registerServiceWorker' 16 | import configureStore from './configureStore' 17 | import { translationMessages } from './i18n' 18 | 19 | import './global-styles' 20 | 21 | const history = createHistory() 22 | const MOUNT_NODE = document.getElementById('root') 23 | const store = configureStore(history) 24 | 25 | const locale = getBrowserLocale().substring(0, 2) 26 | 27 | const AppWithDnDContext = DragDropContext(HTML5Backend)(App) 28 | 29 | const render = (messages) => { 30 | ReactDOM.render( 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | , 40 | MOUNT_NODE 41 | ) 42 | } 43 | 44 | if (module.hot) { 45 | // Hot reloadable React components and translation json files 46 | // modules.hot.accept does not accept dynamic dependencies, 47 | // have to be constants at compile-time 48 | module.hot.accept(['./i18n', 'containers/App'], () => { 49 | ReactDOM.unmountComponentAtNode(MOUNT_NODE) 50 | render(translationMessages) 51 | }) 52 | } 53 | 54 | render(translationMessages) 55 | registerServiceWorker() 56 | -------------------------------------------------------------------------------- /src/components/AddComponentTooltip/ContentWrapper.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | const ContentWrapper = styled.div` 4 | > * { 5 | cursor: pointer; 6 | &:hover { 7 | background-color: #0000001f; 8 | border-radius: 4px; 9 | } 10 | padding: 5px; 11 | box-sizing: content-box; 12 | } 13 | margin: -3px -10px -5px -10px; 14 | ` 15 | 16 | 17 | export default ContentWrapper 18 | -------------------------------------------------------------------------------- /src/components/AddComponentTooltip/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactTooltip from 'react-tooltip' 3 | import styled from 'styled-components' 4 | import { connect } from 'react-redux' 5 | import { pD } from 'utils' 6 | 7 | import { FolderIcon, SCIcon } from 'components' 8 | import { 9 | newComponentBundlePlease, 10 | newStyledComponentPlease, 11 | } from 'duck' 12 | 13 | import ContentWrapper from './ContentWrapper' 14 | 15 | const AddComponentTooltip = ({ 16 | newComponentBundlePlease, 17 | newStyledComponentPlease, 18 | ...props 19 | }) => ( 20 | ( 27 | 28 | 29 | 30 | 31 | )} 32 | {...props} 33 | /> 34 | ) 35 | 36 | const AddComponentTooltipStyled = styled(AddComponentTooltip)` 37 | pointer-events: auto; 38 | user-select: none; 39 | &:hover { 40 | visibility: visible; 41 | opacity: 1; 42 | } 43 | ` 44 | 45 | const mapDispatchToProps = { 46 | newComponentBundlePlease, 47 | newStyledComponentPlease, 48 | } 49 | 50 | export default connect(null, mapDispatchToProps)(AddComponentTooltipStyled) 51 | -------------------------------------------------------------------------------- /src/components/Backtick.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-as-components' 2 | import theme from 'theme-proxy' 3 | 4 | const Backtick = styled.span.attrs({ 5 | children: '`', 6 | })` 7 | color: ${theme.colors.orange}; 8 | ` 9 | 10 | export default Backtick 11 | -------------------------------------------------------------------------------- /src/components/Buttons/AddButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-as-components' 3 | import { PlusIcon } from 'components' 4 | 5 | export default styled(() => ).as.span` 6 | color: white; 7 | cursor: pointer; 8 | background-color: #2d291852; 9 | &:hover { 10 | background-color: #2d2918a3; 11 | } 12 | display: inline-block; 13 | border: 1px solid #4e141421; 14 | border-radius: 8px; 15 | padding: 2px 4px; 16 | line-height: 0; 17 | transform: translateY(-1.5px); 18 | margin-bottom: 5px; 19 | margin-left: ${props => props.left || 0}px; 20 | ` 21 | -------------------------------------------------------------------------------- /src/components/Buttons/GithubButton.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | const GithubButton = styled.div` 4 | ${props => props.hide && 'display:none;'} 5 | position: fixed; 6 | display: inline-block; 7 | left: ${props => props.left}; 8 | ${props => props.leftCalc && `left: ${props.leftCalc}`}; 9 | bottom: 14px; 10 | z-index: 1; 11 | a { 12 | color: #24292e; 13 | text-decoration: none; 14 | outline: 0; 15 | background-color: #eff3f6; 16 | background-image: linear-gradient(to bottom, #fafbfc, #e4ebf0); 17 | background-repeat: repeat-x; 18 | background-size: 110% 110%; 19 | display: inline-block; 20 | font-weight: 600; 21 | cursor: pointer; 22 | border: 1px solid #d1d2d3; 23 | border-radius: 0.25em; 24 | text-decoration: none; 25 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; 26 | font-size: 12px; 27 | white-space: nowrap; 28 | user-select: none; 29 | padding: 0 10px; 30 | height: 28px; 31 | line-height: 28px; 32 | font-size: 12px; 33 | vertical-align: middle; 34 | &:hover { 35 | background-color:#e6ebf1; 36 | background-image:linear-gradient(to bottom, #f0f3f6, #dce3ec); 37 | border-color:#afb1b2; 38 | } 39 | } 40 | ` 41 | 42 | export default GithubButton 43 | -------------------------------------------------------------------------------- /src/components/Buttons/IssueOnGithub.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import GithubButton from './GithubButton' 4 | 5 | const IssueOnGithub = ({ hide }) => !hide && ( 6 | 7 | 8 | 9 | Get involved{' '} 10 | 11 | 🚀 12 | 13 | 14 | 15 | ) 16 | 17 | const IssueOpenIcon = () => ( 18 | 36 | ) 37 | 38 | export default IssueOnGithub 39 | -------------------------------------------------------------------------------- /src/components/Buttons/StarOnGithub.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import GithubButton from './GithubButton' 4 | 5 | const StarOnGithub = ({ hide }) => !hide && ( 6 | 7 | 8 | 9 | Star on Github{' '} 10 | 11 | 🎀 12 | 13 | 14 | 15 | ) 16 | const Star = () => ( 17 | 35 | ) 36 | 37 | export default StarOnGithub 38 | -------------------------------------------------------------------------------- /src/components/Buttons/index.js: -------------------------------------------------------------------------------- 1 | export { default as AddButton } from './AddButton' 2 | export { default as StarOnGithub } from './StarOnGithub' 3 | export { default as IssueOnGithub } from './IssueOnGithub' 4 | -------------------------------------------------------------------------------- /src/components/ClassComponent/components/ClassMethod.js: -------------------------------------------------------------------------------- 1 | import T from 'prop-types' 2 | import React from 'react' 3 | 4 | import { indent } from 'utils' 5 | import { JSX, Keyword, Semi } from 'components' 6 | import { DeclarationContainer, NameInput, PropsContainer } from 'containers' 7 | import { declarationPropTypes } from 'model-prop-types' 8 | 9 | export default class ClassMethod extends React.PureComponent { 10 | render() { 11 | const { 12 | declaration: { 13 | declarationId, 14 | declarationIds, 15 | invocationIds, 16 | }, 17 | thiz: { 18 | declParamIds, 19 | }, 20 | } = this.props 21 | return ( 22 |
23 | {indent(1)}render() {'{'} 24 |
25 | {!!declarationIds.length && indent(2)} 26 | {declarationIds.map(id => ( 27 | ( 31 | 32 | const 33 | {declParamIds.length ? ( 34 | 35 | 40 | 41 | {' '} 42 | = this.props 43 | 44 | 45 | ) : ( 46 | 47 | {'{ '} 48 | 49 | {' } '} 50 | = this.state 51 | 52 | )} 53 |
54 |
55 | )} 56 | /> 57 | ))} 58 | {indent(2)} 59 | return ( 60 | {invocationIds.map(id => )} 61 | {indent(2)}) 62 | {indent(1)} 63 | {'}'} 64 |
65 |
66 | ) 67 | } 68 | } 69 | 70 | ClassMethod.propTypes = { 71 | thiz: T.shape({ props: T.array }).isRequired, 72 | declaration: declarationPropTypes.isRequired, 73 | } 74 | -------------------------------------------------------------------------------- /src/components/ClassComponent/components/ClassProperty.js: -------------------------------------------------------------------------------- 1 | import T from 'prop-types' 2 | import React from 'react' 3 | import { indent } from 'utils' 4 | import { declarationPropTypes } from 'model-prop-types' 5 | 6 | import { ObjectLiteral } from 'components' 7 | 8 | export default class ClassProperty extends React.PureComponent { 9 | render() { 10 | const { declaration: { declarationIds, invocationIds } } = this.props 11 | return ( 12 |
13 | {indent(1)}state{' = '} 14 | 18 |
19 |
20 |
21 | ) 22 | } 23 | } 24 | 25 | ClassProperty.propTypes = { 26 | thiz: T.shape({ props: T.array }).isRequired, 27 | declaration: declarationPropTypes.isRequired, 28 | } 29 | -------------------------------------------------------------------------------- /src/components/ClassComponent/components/ClassPropertyDeclarationFactory.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { CLASS_PROP, CLASS_METHOD } from 'constantz' 3 | import { ClassProperty, ClassMethod } from './' 4 | 5 | const ClassPropertyDeclarationFactory = props => { 6 | switch (props.declaration.type) { 7 | case CLASS_PROP: return 8 | case CLASS_METHOD: return 9 | default: return
unknown type
10 | } 11 | } 12 | 13 | export default ClassPropertyDeclarationFactory 14 | -------------------------------------------------------------------------------- /src/components/ClassComponent/components/index.js: -------------------------------------------------------------------------------- 1 | export { default as ClassPropertyDeclarationFactory } from './ClassPropertyDeclarationFactory' 2 | export { default as ClassMethod } from './ClassMethod' 3 | export { default as ClassProperty } from './ClassProperty' 4 | -------------------------------------------------------------------------------- /src/components/ClassComponent/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Keyword } from 'components' 3 | import { DeclarationContainer, NameInput, ComponentTypeToggle, PropTypesContainer } from 'containers' 4 | import { INLINE, STATELESS_FUNCTION_COMPONENT } from 'constantz' 5 | import styled from 'styled-as-components' 6 | 7 | import { ClassPropertyDeclarationFactory } from './components' 8 | 9 | class ClassComponent extends React.PureComponent { 10 | render() { 11 | const { declarationId, exportType, nameId, declParamIds, declarationIds } = this.props 12 | return ( 13 | 14 | {/* open */} 15 | {exportType === INLINE && export}{' '} 16 | 21 | extends React.Component {'{'} 22 |
23 | {declarationIds.map(id => ( 24 | ( 28 | 29 | )} 30 | /> 31 | ))} 32 | {'}'} 33 |
34 | 35 |
36 | ) 37 | } 38 | } 39 | 40 | export default styled(ClassComponent).as.div` 41 | 42 | ` 43 | -------------------------------------------------------------------------------- /src/components/ExportAppButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | 4 | const ExportAppButton = styled.button.attrs({ 5 | children: props => props.text, 6 | })` 7 | z-index: 1; 8 | user-select: none; 9 | position: absolute; 10 | ${props => props.fixed && 'position: fixed;'} 11 | ${props => (props.top === 0 || props.top) && `top: ${props.top}px;`} 12 | ${props => (props.bottom === 0 || props.bottom) && `bottom: ${props.bottom}px;`} 13 | ${props => (props.left === 0 || props.left) && `left: ${props.left}px;`} 14 | ${props => (props.right === 0 || props.right) && `right: ${props.right}px;`} 15 | border: 1px solid black; 16 | padding: 3px; 17 | font-size: 10px; 18 | border-radius: 3px; 19 | background-color: white; 20 | &:hover { 21 | color: white; 22 | background-color: #010431bb; 23 | } 24 | ` 25 | 26 | export default props => props.onClick ? : null 27 | -------------------------------------------------------------------------------- /src/components/Flex.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-as-components' 2 | 3 | const Flex = styled.div` 4 | width: 100%; 5 | display: flex; 6 | justify-content: space-between; 7 | ${props => props.aiend && 'align-items: flex-end;'} 8 | ` 9 | 10 | export default Flex 11 | -------------------------------------------------------------------------------- /src/components/Icons/FolderIcon.js: -------------------------------------------------------------------------------- 1 | /* 2 | Icons: https://material.io/tools/icons/?icon=folder_open&style=baseline 3 | Apache license version 2.0 4 | */ 5 | 6 | import React from 'react' 7 | 8 | const FolderIcon = props => 9 | props.open ? ( 10 | 11 | 12 | 13 | 14 | ) : ( 15 | 16 | 17 | 18 | 19 | ) 20 | 21 | FolderIcon.defaultProps = { 22 | width: '15px', 23 | style: { 24 | margin: '0 3px 3px 4px', 25 | color: '#2f3267', 26 | }, 27 | } 28 | 29 | export default FolderIcon 30 | -------------------------------------------------------------------------------- /src/components/Icons/HumanEditRedoIcon.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const HumanEditRedoIcon = () => ( 4 | 11 | 15 | 19 | 28 | 29 | 30 | 31 | 32 | 36 | 44 | 45 | 46 | 47 | 48 | 52 | 61 | 62 | 63 | 64 | 68 | 76 | 77 | 78 | 79 | 80 | 84 | 85 | ) 86 | 87 | export default HumanEditRedoIcon 88 | -------------------------------------------------------------------------------- /src/components/Icons/HumanEditUndoIcon.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const HumanEditUndoIcon = () => ( 4 | 11 | 15 | 19 | 27 | 28 | 29 | 30 | 34 | 42 | 43 | 44 | 45 | 49 | 57 | 58 | 59 | 60 | 64 | 72 | 73 | 74 | 75 | 79 | 80 | ) 81 | 82 | export default HumanEditUndoIcon 83 | -------------------------------------------------------------------------------- /src/components/Icons/JSONIcon.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-as-components' 2 | 3 | const JSONIcon = () => '{}' 4 | 5 | export default styled(JSONIcon).as.span` 6 | font-size: 13px; 7 | margin: 0 5px 0 4px; 8 | color: #71711d; 9 | ` 10 | -------------------------------------------------------------------------------- /src/components/Icons/PlusIcon.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | /* 4 | https://fontawesome.com/license Icon: https://fontawesome.com/icons/plus?style=solid 5 | */ 6 | const PlusIcon = props => ( 7 | 8 | 9 | 10 | ) 11 | 12 | PlusIcon.defaultProps = { 13 | width: '9px', 14 | } 15 | 16 | export default PlusIcon 17 | -------------------------------------------------------------------------------- /src/components/Icons/ReactIcon.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | 4 | const ReactIcon = props => ( 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | ) 14 | 15 | export default styled(ReactIcon)` 16 | margin: 0 0 0 -3px; 17 | ` 18 | 19 | ReactIcon.defaultProps = { 20 | width: '25px', 21 | } 22 | -------------------------------------------------------------------------------- /src/components/Icons/SCIcon.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | const SCIcon = styled.span.attrs({ 4 | children: '💅', 5 | })` 6 | display: inline-block; 7 | font-size: ${props => props.size || 12}px; 8 | margin: 0 3px 0 4px; 9 | line-height: 1; 10 | ` 11 | 12 | export default SCIcon 13 | -------------------------------------------------------------------------------- /src/components/Icons/index.js: -------------------------------------------------------------------------------- 1 | export { default as ReactIcon } from './ReactIcon' 2 | export { default as FolderIcon } from './FolderIcon' 3 | export { default as JSONIcon } from './JSONIcon' 4 | export { default as PlusIcon } from './PlusIcon' 5 | export { default as SCIcon } from './SCIcon' 6 | export { default as HumanEditRedoIcon } from './HumanEditRedoIcon' 7 | export { default as HumanEditUndoIcon } from './HumanEditUndoIcon' 8 | -------------------------------------------------------------------------------- /src/components/Input.js: -------------------------------------------------------------------------------- 1 | import T from 'prop-types' 2 | import React from 'react' 3 | import styled from 'styled-components' 4 | import AutosizeInput from 'react-input-autosize' 5 | 6 | export default class Input extends React.PureComponent { 7 | state = { 8 | displayInput: false, 9 | } 10 | 11 | onClick = () => { 12 | if (this.props.shouldActivateOnClick) { 13 | this.setState({ displayInput: true }) 14 | } 15 | } 16 | 17 | onBlur = () => { 18 | this.setState({ displayInput: false }) 19 | } 20 | 21 | onChange = e => { 22 | const { onChange, nameId } = this.props 23 | const pos = e.target.selectionStart - 1 24 | const nextName = e.target.value 25 | 26 | if (nextName && !(/^(?![0-9])[a-zA-Z0-9]+$/.exec(nextName))) { 27 | this.inputRef.setSelectionRange(pos, 0) 28 | setTimeout(() => this.inputRef.setSelectionRange(pos, 0)) 29 | return 30 | } 31 | 32 | onChange({ nameId, value: nextName }) 33 | } 34 | 35 | componentDidUpdate() { 36 | if (this.state.displayInput) { 37 | if (document.activeElement !== this.inputRef) { 38 | this.inputRef.focus() 39 | this.inputRef.setSelectionRange(0, this.inputRef.value.length) 40 | } 41 | } else { 42 | this.inputRef = null 43 | } 44 | } 45 | 46 | render() { 47 | const { value, pointer } = this.props 48 | const input = { 49 | value, 50 | onChange: this.onChange, 51 | onBlur: this.onBlur, 52 | } 53 | 54 | return ( 55 | this.state.displayInput ? ( 56 | 57 | { this.inputRef = ref }} type="text" {...input} /> 58 | 59 | ) : ( 60 | {value} 61 | ) 62 | ) 63 | } 64 | } 65 | 66 | Input.propTypes = { 67 | value: T.string.isRequired, 68 | pointer: T.bool, 69 | shouldActivateOnClick: T.bool, 70 | } 71 | 72 | Input.defaultProps = { 73 | shouldActivateOnClick: true, 74 | pointer: false, 75 | } 76 | 77 | // https://github.com/JedWatson/react-input-autosize/issues/135 78 | const ff = window.navigator.userAgent.includes('Firefox') 79 | const standardCorrection = 1.9 // keep this when 135 fixed 80 | const HackAutosizeInput = styled.span` 81 | div[style] { 82 | margin-right: ${ff ? `-${15 + standardCorrection}px` : `-${standardCorrection}px`}; 83 | } 84 | ` 85 | 86 | const DisplayName = styled.span` 87 | cursor: ${props => props.pointer ? 'pointer' : 'text'}; 88 | ` 89 | -------------------------------------------------------------------------------- /src/components/JSX/index.js: -------------------------------------------------------------------------------- 1 | import T from 'prop-types' 2 | import React from 'react' 3 | 4 | import { 5 | COMPONENT_INVOCATION, 6 | PARAM_INVOCATION, 7 | ARRAY_MAP_METHOD, 8 | VAR_INVOCATION, 9 | IMPORT_VAR, 10 | PROPERTY_ACCESS, 11 | } from 'constantz' 12 | import { MapInvocation, VarInvocation } from 'components' 13 | import { 14 | InvocationContainer, 15 | ComponentInvocationContainer, 16 | ParamInvocationContainer, 17 | } from 'containers' 18 | 19 | // functionize due to module importing problems 20 | const types = () => ({ 21 | [COMPONENT_INVOCATION]: [ComponentInvocationContainer], 22 | [PARAM_INVOCATION]: [ParamInvocationContainer], 23 | [ARRAY_MAP_METHOD]: [MapInvocation], 24 | [VAR_INVOCATION]: [VarInvocation], 25 | [IMPORT_VAR]: [VarInvocation], 26 | [PROPERTY_ACCESS]: [VarInvocation, { dot: true }], 27 | }) 28 | 29 | const JSX = ({ invocationId, initial, ...props }) => ( 30 | { 33 | const [Invocation, configProps = {}] = types()[invocation.type] || ComponentInvocationContainer 34 | const propsToPass = initial ? { ...props, ...configProps, initial } : { ...props, ...configProps } 35 | return ( 36 | 42 | ) 43 | }} 44 | /> 45 | ) 46 | 47 | JSX.propTypes = { 48 | invocationId: T.number.isRequired, 49 | initial: T.bool, 50 | depth: T.number, 51 | parentId: T.number, 52 | parentIsInline: T.bool, 53 | } 54 | 55 | JSX.defaultProps = { 56 | initial: false, 57 | depth: 1, 58 | parentId: null, 59 | parentIsInline: false, 60 | } 61 | 62 | export default JSX 63 | -------------------------------------------------------------------------------- /src/components/Json.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactJsonSyntaxHighlighter from 'react-json-syntax-highlighter' 3 | import styled from 'styled-as-components' 4 | 5 | const Json = ({ text }) => ( 6 | 7 | ) 8 | 9 | export default styled(Json).as.div` 10 | font-size: 14px; 11 | color: #8a6a6a; 12 | 13 | .string { 14 | color: #506317; 15 | } 16 | .boolean { 17 | color: #e00000; 18 | } 19 | .null { 20 | color: #e00000; 21 | } 22 | .key { 23 | color: #3f39a2; 24 | } 25 | .number { 26 | color: #000000; 27 | } 28 | ` 29 | -------------------------------------------------------------------------------- /src/components/Keyword.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-as-components' 2 | import theme from 'theme-proxy' 3 | 4 | const Keyword = styled.span` 5 | user-select: auto; 6 | color: ${theme.colors.darkgreen}; 7 | ${props => props.pointer && 'cursor: pointer;'} 8 | ` 9 | 10 | export default Keyword 11 | -------------------------------------------------------------------------------- /src/components/Logo/index.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | import logo from './logo.png' 3 | 4 | const Logo = styled.img.attrs({ 5 | src: logo, 6 | alt: 'logo', 7 | })` 8 | width: 47px; 9 | height: 40px; 10 | margin-right: 12px; 11 | ` 12 | 13 | export default Logo 14 | -------------------------------------------------------------------------------- /src/components/Logo/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielrob/aperitif-editor/3b1f2b71f2dae208b6f3f4e04b7bf0c552395c52/src/components/Logo/logo.png -------------------------------------------------------------------------------- /src/components/MapInvocation/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { JSX } from 'components' 3 | import { NameInput } from 'containers' 4 | import { indent } from 'utils' 5 | 6 | const MapInvocation = ({ invocation: { invocationId, nameId, invocationIds }, depth }) => ( 7 | 8 | .map( => ( 9 | 15 | {indent(depth)})) 16 | 17 | ) 18 | 19 | export default MapInvocation 20 | -------------------------------------------------------------------------------- /src/components/ObjectLiteral/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { indent } from 'utils' 4 | import { JSX } from 'components' 5 | import { DeclarationContainer, NameInput } from 'containers' 6 | 7 | const ObjectLiteral = ({ declarationIds, invocationIds, depth = 1, inline }) => ( 8 | 9 | {'{'} 10 | {inline ? ' ' :
} 11 | {declarationIds.map((did, index) => ( 12 | ( 16 | 17 | {!inline && indent(depth + 1)} 18 | 19 | {': '} 20 | 21 | {!inline && (index === declarationIds.length - 1) && ','} 22 | {!inline &&
} 23 |
24 | )} 25 | /> 26 | ))} 27 | {inline ? ' ' : indent(depth)} 28 | {'}'} 29 |
30 | ) 31 | 32 | export default ObjectLiteral 33 | -------------------------------------------------------------------------------- /src/components/ParamInvocation.js: -------------------------------------------------------------------------------- 1 | import T from 'prop-types' 2 | import { forbidExtraProps } from 'airbnb-prop-types' 3 | import React from 'react' 4 | import styled from 'styled-as-components' 5 | 6 | import theme from 'theme-proxy' 7 | import { paramInvocationPropTypes } from 'model-prop-types' 8 | 9 | import { indent } from 'utils' 10 | 11 | import { JSX } from 'components' 12 | import { Name } from 'containers' 13 | 14 | const ParamInvocation = ({ 15 | connectDragSource, 16 | isPIDragging, 17 | parentId, 18 | depth, 19 | invocation: { 20 | nameId, 21 | declIsSpreadMember, 22 | invocationIds, 23 | inline, 24 | }, 25 | }) => isPIDragging ? null : ( 26 | 27 | {!inline && indent(depth)} 28 | {connectDragSource( 29 | 30 | {'{'} 31 | {declIsSpreadMember && 'props.'} 32 | 33 | {invocationIds.length === 1 && 34 | 40 | } 41 | {'}'} 42 | 43 | )} 44 | 45 | ) 46 | 47 | 48 | /* 49 | propTypes 50 | */ 51 | ParamInvocation.propTypes = forbidExtraProps({ 52 | // passed by parent 53 | parentId: T.number.isRequired, 54 | depth: T.number.isRequired, 55 | 56 | // injected by makeSelectParamInvocation 57 | invocation: paramInvocationPropTypes.isRequired, 58 | 59 | // injected by DragSource 60 | isPIDragging: T.bool.isRequired, 61 | connectDragSource: T.func.isRequired, 62 | }) 63 | 64 | /* style, export */ 65 | export default styled(ParamInvocation).as.span` 66 | color: ${theme.colors.darkblue} 67 | .dragsource { 68 | user-select: text; 69 | cursor: pointer; 70 | } 71 | ` 72 | 73 | -------------------------------------------------------------------------------- /src/components/ProjectIndexDeclaration/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Semi, Keyword } from 'components' 3 | import { NameInput } from 'containers' 4 | import { indent } from 'utils' 5 | import { APP_CONTAINER_NAME_ID } from 'constantz' 6 | 7 | export default class ProjectIndexDeclaration extends React.PureComponent { 8 | render() { 9 | return ( 10 |
11 | import React from 'react' 12 | import ReactDOM from 'react-dom' 13 | import{' { '} {' } '} 14 | from{' '}'containers' 15 |
16 | ReactDOM.render(
17 | {indent(1)}{'<'}{' />'},
18 | {indent(1)}document.getElementById('root')
19 | ) 20 | 21 |
22 | ) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/components/PropTypes/index.js: -------------------------------------------------------------------------------- 1 | import C from 'check-types' 2 | import React from 'react' 3 | import JSstringify from 'javascript-stringify' 4 | 5 | import { indent } from 'utils' 6 | import { Semi } from 'components' 7 | import { Name, NameInput } from 'containers' 8 | 9 | const PropTypes = ({ props, nameId }) => !!props.length && ( 10 | 11 |
12 | .propTypes = {'{'} 13 | {props.filter(({ isSpreadMember, invokeCount }) => !isSpreadMember || invokeCount).map( 14 | ({ nameId, invokeCount, payload }) => ( 15 |
16 | {indent(1)} 17 | 22 | 23 | 24 | : {getPropType(payload)} 25 | {!!invokeCount && C.not.null(payload) && '.isRequired'} 26 | , 27 |
28 | ) 29 | )} 30 | {'}'} 31 |
32 | ) 33 | 34 | export default PropTypes 35 | 36 | const getPropType = (payload, pTName = 'PropTypes', nested) => { 37 | if (!payload) { 38 | return `${pTName}.any` 39 | } 40 | if (C.string(payload)) { 41 | return `${pTName}.string` 42 | } 43 | if (C.boolean(payload)) { 44 | return `${pTName}.bool` 45 | } 46 | if (C.number(payload)) { 47 | return `${pTName}.number` 48 | } 49 | if (C.object(payload)) { 50 | if (nested) { 51 | return `${pTName}.object` 52 | } 53 | return ( 54 | 55 | {pTName}.shape({'{'} 56 |
57 | {Object.keys(payload).map(key => ( 58 | 59 | {indent(2)} 60 | 65 | {key} 66 | 67 | : {getPropType(payload[key], pTName, true)}, 68 |
69 |
70 | ))} 71 | {indent(1)} 72 | {'}'}) 73 |
74 | ) 75 | } 76 | if (C.array(payload)) { 77 | if (nested) { 78 | return `${pTName}.array` 79 | } 80 | return `${pTName}.arrayOf(${getPropType(payload[0], pTName, true)})` 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/components/Props/components/Prop.js: -------------------------------------------------------------------------------- 1 | import C from 'check-types' 2 | import T from 'prop-types' 3 | import React from 'react' 4 | import styled from 'styled-as-components' 5 | import JSstringify from 'javascript-stringify' 6 | import { declParamPropTypes } from 'model-prop-types' 7 | 8 | import { spaces } from 'utils' 9 | import { Name } from 'containers' 10 | 11 | const Prop = ({ prop: { nameId, payload }, skipFinalComma, connectDragSource }) => ( 12 | 13 | {connectDragSource( 14 | 22 | 23 | 24 | )} 25 | {!skipFinalComma && `,${spaces(1)}`} 26 | 27 | ) 28 | 29 | Prop.propTypes = { 30 | declarationId: T.number.isRequired, 31 | skipFinalComma: T.bool.isRequired, 32 | prop: T.shape(declParamPropTypes).isRequired, 33 | connectDragSource: T.func.isRequired, 34 | } 35 | 36 | export default styled(Prop).as.span` 37 | cursor: pointer; 38 | ` 39 | 40 | const getDataTip = payload => 41 | JSstringify(payload, replacer, 2, { maxDepth: 2, maxValues: 15 }) 42 | .replace(/undefined,\n/g, '') 43 | .replace(/undefined\n/g, ' ...\n') 44 | 45 | const replacer = (value, indentation, stringify) => { 46 | if (C.string(value) && value.length > 70) { 47 | return stringify(`${value.substring(0, 70)}...`) 48 | } 49 | return stringify(value) 50 | } 51 | -------------------------------------------------------------------------------- /src/components/Props/components/SpreadProps.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-as-components' 3 | import { indent } from 'utils' 4 | 5 | const SpreadProps = ({ isOver, props, depth, spreadProps, connectDragSource, names, inline }) => 6 | (!!spreadProps.length || isOver) ? ( 7 | names[nameId].value).join(', ')} }`} 12 | data-for="prop" 13 | data-delay-show="100" 14 | > 15 | {!inline && indent(depth || 1)} 16 | {inline && ', '} 17 | {connectDragSource( 18 |
19 | {!!props.length && '...'}props 20 |
21 | )} 22 | {!inline &&
} 23 |
24 | ) : null 25 | 26 | export default styled(SpreadProps).as.span` 27 | position: relative; 28 | ${props => !!props.spreadProps.length && 'cursor: pointer;'} 29 | padding: 15px 15px 15px 0; 30 | margin: -15px -15px -15px 0; 31 | .dragsource { 32 | display: inline-block; 33 | user-select: text; 34 | } 35 | ` 36 | -------------------------------------------------------------------------------- /src/components/Props/components/index.js: -------------------------------------------------------------------------------- 1 | export { default as Prop } from './Prop' 2 | export { default as SpreadProps } from './SpreadProps' 3 | -------------------------------------------------------------------------------- /src/components/Props/containers/PropDragContainer.js: -------------------------------------------------------------------------------- 1 | import T from 'prop-types' 2 | import { forbidExtraProps } from 'airbnb-prop-types' 3 | import React from 'react' 4 | import { DragSource } from 'react-dnd' 5 | import { PROP } from 'constantz' 6 | import ReactTooltip from 'react-tooltip' 7 | 8 | import { declParamPropTypes } from 'model-prop-types' 9 | import { Prop } from '../components' 10 | 11 | const PropContainer = props => 12 | 13 | /* 14 | propTypes 15 | */ 16 | PropContainer.propTypes = forbidExtraProps({ 17 | declarationId: T.number.isRequired, 18 | prop: T.shape(declParamPropTypes).isRequired, 19 | skipFinalComma: T.bool.isRequired, 20 | connectDragSource: T.func.isRequired, 21 | }) 22 | 23 | /* 24 | dnd 25 | */ 26 | const propSource = { 27 | beginDrag({ prop: { id, nameId, name, payload, invokeCount, altIds }, declarationId }) { 28 | ReactTooltip.hide() // disable tooltips 29 | return { 30 | declarationId, 31 | paramId: id, 32 | name, 33 | nameId, 34 | payload, 35 | type: PROP, 36 | invokeCount, 37 | altIds, 38 | } 39 | }, 40 | endDrag() { 41 | ReactTooltip.show() // enable tooltips 42 | }, 43 | } 44 | 45 | const collect = connect => ({ 46 | connectDragSource: connect.dragSource(), 47 | }) 48 | 49 | /* 50 | compose export 51 | */ 52 | export default DragSource(PROP, propSource, collect)(PropContainer) 53 | -------------------------------------------------------------------------------- /src/components/Props/containers/SpreadPropsContainer.js: -------------------------------------------------------------------------------- 1 | import T from 'prop-types' 2 | import React from 'react' 3 | import { connect } from 'react-redux' 4 | import { DropTarget, DragSource } from 'react-dnd' 5 | import { findDOMNode } from 'react-dom' 6 | import { createStructuredSelector } from 'reselect' 7 | 8 | import { compose } from 'utils' 9 | import { DraggableTypes } from 'constantz' 10 | import { moveParamToSpread } from 'duck' 11 | import { selectNames } from 'selectors' 12 | 13 | import { SpreadProps } from '../components' 14 | 15 | class SpreadPropsContainer extends React.PureComponent { 16 | render() { 17 | const { 18 | moveParamToSpread, // drop target only 19 | connectDropTarget, 20 | ...props 21 | } = this.props 22 | return ( 23 | connectDropTarget(findDOMNode(innerRef))} 25 | {...props} 26 | /> 27 | ) 28 | } 29 | } 30 | 31 | SpreadPropsContainer.propTypes = { 32 | moveParamToSpread: T.func.isRequired, 33 | // names: T.shape(T.string).isRequired, 34 | declarationId: T.number.isRequired, 35 | } 36 | 37 | /* 38 | connect 39 | */ 40 | const mapDispatchToProps = { moveParamToSpread } 41 | 42 | const mapStateToProps = createStructuredSelector({ 43 | names: selectNames, 44 | }) 45 | 46 | /* 47 | dnd - source 48 | */ 49 | const sourceSpec = { 50 | beginDrag(props) { 51 | return props 52 | }, 53 | } 54 | 55 | const sourceCollect = (connect, monitor) => ({ 56 | connectDragSource: connect.dragSource(), 57 | isDragging: monitor.isDragging(), 58 | }) 59 | 60 | /* 61 | dnd - target 62 | */ 63 | const dropzoneTarget = { 64 | drop(props, monitor) { 65 | const { declarationId, moveParamToSpread } = props 66 | const { paramId } = monitor.getItem() 67 | moveParamToSpread({ 68 | declarationId, 69 | paramId, 70 | }) 71 | }, 72 | } 73 | 74 | const targetCollect = (connect, monitor) => ({ 75 | connectDropTarget: connect.dropTarget(), 76 | isOver: monitor.isOver(), 77 | dragItem: monitor.getItem(), 78 | }) 79 | 80 | /* 81 | compose export 82 | */ 83 | export default compose( 84 | connect(mapStateToProps, mapDispatchToProps), 85 | DragSource(DraggableTypes.PROPS_SPREAD, sourceSpec, sourceCollect), 86 | DropTarget(DraggableTypes.PROP, dropzoneTarget, targetCollect), 87 | )(SpreadPropsContainer) 88 | 89 | -------------------------------------------------------------------------------- /src/components/Props/containers/index.js: -------------------------------------------------------------------------------- 1 | export { default as PropDragContainer } from './PropDragContainer' 2 | export { default as SpreadPropsContainer } from './SpreadPropsContainer' 3 | -------------------------------------------------------------------------------- /src/components/Props/helpers.js: -------------------------------------------------------------------------------- 1 | export const sortProps = ({ name: propA }, { name: propB }) => { 2 | if (propA === 'id' || propB === 'id') { 3 | return propB === 'id' 4 | } 5 | if (propA.includes('Id') || propB.includes('Id')) { 6 | return propB.includes('Id') 7 | } 8 | return propA > propB 9 | } 10 | -------------------------------------------------------------------------------- /src/components/Props/index.js: -------------------------------------------------------------------------------- 1 | import T from 'prop-types' 2 | import { forbidExtraProps } from 'airbnb-prop-types' 3 | import React from 'react' 4 | import styled from 'styled-as-components' 5 | import { partition } from 'lodash' 6 | 7 | import { PROP } from 'constantz' 8 | import { StopDropTarget } from 'containers' 9 | import { indent } from 'utils' 10 | import { declParamPropTypes } from 'model-prop-types' 11 | 12 | import { SpreadPropsContainer, PropDragContainer } from './containers' 13 | 14 | const Props = ({ props: allProps, declarationId, depth, parentheses }) => { 15 | 16 | const [spreadProps, props] = partition(allProps, p => p.isSpreadMember) 17 | const hasProps = !!props.length 18 | const inline = props.length < 5 19 | return ( 20 | 21 | {' '} 22 | {hasProps && parentheses && '('} 23 | {hasProps && '{'}{' '} 24 | {hasProps && !inline &&
} 25 | 26 | {props 27 | .map((prop, index) => ( 28 | 29 | {!inline && indent(depth || 1)} 30 | 35 | {!inline &&
} 36 |
37 | ))} 38 | {!hasProps && '()'} 39 |
40 | {' '} 47 | {hasProps && !inline && indent((depth || 1) - 1)} 48 | {hasProps && '}'} 49 | {hasProps && parentheses && ')'}{' '} 50 |
51 | ) 52 | } 53 | 54 | 55 | /* 56 | propTypes 57 | */ 58 | Props.propTypes = forbidExtraProps({ 59 | props: T.arrayOf(T.shape(declParamPropTypes)).isRequired, 60 | declarationId: T.number.isRequired, 61 | depth: T.number, 62 | parentheses: T.bool, 63 | }) 64 | 65 | Props.defaultProps = { 66 | depth: 0, 67 | parentheses: false, 68 | } 69 | 70 | export default styled(Props).as.span` 71 | ` 72 | -------------------------------------------------------------------------------- /src/components/Semi.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | import styled from 'styled-as-components' 4 | 5 | import { updatePreferences } from 'duck' 6 | import { selectPreferences } from 'selectors' 7 | 8 | const Semi = ({ semis }) => ( 9 | 10 | {semis && ';'} 11 |
12 |
13 | ) 14 | 15 | const mapStateToProps = state => selectPreferences(state) 16 | const mapDispatchToProps = { updatePreferences } 17 | 18 | export default connect( 19 | mapStateToProps, 20 | mapDispatchToProps 21 | )(styled(Semi).as.span.attrs({ 22 | onClick: props => () => props.updatePreferences({ semis: !props.semis }), 23 | })` 24 | cursor: pointer; 25 | ${props => !props.semis && 'padding-left: 10px;'} 26 | `) 27 | -------------------------------------------------------------------------------- /src/components/Standard.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default class Standard extends React.PureComponent { 4 | render() { 5 | return ( 6 |
7 | Unkown declaration type 8 |
9 | ) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/components/StatelessFunctionComponent/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { JSX, Semi, Keyword } from 'components' 3 | import { ComponentTypeToggle, PropsContainer, PropTypesContainer, NameInput } from 'containers' 4 | import { INLINE, CLASS_COMPONENT } from 'constantz' 5 | 6 | export default class StatelessFunctionComponent extends React.PureComponent { 7 | render() { 8 | const { declarationId, exportType, nameId, declParamIds, invocations } = this.props 9 | 10 | return ( 11 |
12 |
13 | {exportType === INLINE && export}{' '} 14 | 19 | = 20 | 21 | => ( 22 | {invocations.length > 1 && '['} 23 |
{' '} 24 | {invocations.map(({ id }) => 25 | ) 26 | } 27 | {invocations.length > 1 && ']'}) 28 | 29 |
30 | ) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/components/StyledComponent/Pre.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | import theme from 'theme-proxy' 3 | 4 | // ff text selection adds newlines around pre elements 5 | const Pre = styled.div` 6 | ${props => !props.shouldDisplay && 'display: none;'} 7 | white-space: pre; 8 | margin: 0; 9 | padding: 5px 0 5px 0; 10 | color: ${theme.colors.orange}; 11 | ` 12 | 13 | export default Pre 14 | -------------------------------------------------------------------------------- /src/components/StyledComponent/TemplateStringTextArea.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-as-components' 2 | import theme from 'theme-proxy' 3 | import TextareaAutosize from 'react-autosize-textarea' 4 | 5 | const TemplateStringTextArea = styled(TextareaAutosize).attrs({ 6 | cols: 100, 7 | autoComplete: 'off', 8 | autoCorrect: 'off', 9 | autoCapitalize: 'off', 10 | spellCheck: 'false', 11 | })` 12 | color: ${theme.colors.orange}; 13 | border: none; 14 | width: 100%; 15 | outline: none; 16 | padding: 5px 0 5px 0; 17 | ` 18 | 19 | export default TemplateStringTextArea 20 | -------------------------------------------------------------------------------- /src/components/StyledComponent/TextAreaActivator.js: -------------------------------------------------------------------------------- 1 | import { debounce } from 'lodash' 2 | import React from 'react' 3 | 4 | /* 5 | Determines whether a text area or pre should be displayed 6 | */ 7 | export default class TextAreaActivator extends React.Component { 8 | state = { 9 | over: false, 10 | lock: false, 11 | } 12 | 13 | onMouseOver = () => this.setState({ over: true }) 14 | onMouseLeave = debounce(() => { 15 | if (this.state.over) { 16 | this.setState({ over: false }) 17 | } 18 | }, 100) 19 | onLock = () => this.setState({ lock: true }) 20 | onUnlock = () => this.setState({ lock: false }) 21 | 22 | render() { 23 | const { render } = this.props 24 | const { over, lock } = this.state 25 | 26 | return ( 27 | 28 | {render({ 29 | displayTextArea: over || lock, 30 | onLock: this.onLock, 31 | onUnlock: this.onUnlock, 32 | })} 33 | 34 | ) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/components/StyledComponent/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | 4 | import { updateDeclaration } from 'duck' 5 | import { Keyword, Backtick, Semi, Input } from 'components' 6 | import { DraggableDeclaration, NameInput } from 'containers' 7 | 8 | import TemplateStringTextArea from './TemplateStringTextArea' 9 | import Pre from './Pre' 10 | import TextAreaActivator from './TextAreaActivator' 11 | 12 | class StyledComponent extends React.PureComponent { 13 | onChange = e => { 14 | const { updateDeclaration, declarationId } = this.props 15 | updateDeclaration({ declarationId, text: e.target.value }) 16 | } 17 | 18 | onTagChange = ({ value: tag }) => { 19 | const { updateDeclaration, declarationId } = this.props 20 | updateDeclaration({ declarationId, tag }) 21 | } 22 | 23 | render() { 24 | const { declarationId, nameId, type, tag, text = ' ' } = this.props 25 | 26 | const tagInput = { 27 | value: tag, 28 | onChange: this.onTagChange, 29 | } 30 | 31 | return ( 32 |
33 | connectDragSource( 37 | 38 | const 39 | {connectDragPreview( )} 40 | 41 | )} 42 | /> 43 | {' '}={' '}styled. 44 | ( 46 | 47 | 54 |
{text || '\n'}
55 |
56 | )} 57 | /> 58 | 59 | 60 |
61 | ) 62 | } 63 | } 64 | 65 | const mapDispatchToProps = { updateDeclaration } 66 | 67 | export default connect(null, mapDispatchToProps)(StyledComponent) 68 | -------------------------------------------------------------------------------- /src/components/VarInvocation/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-as-components' 3 | import { JSX } from 'components' 4 | import { NameInput } from 'containers' 5 | import { indent } from 'utils' 6 | 7 | const VarInvocation = ({ invocation: { invocationId, nameId, invocationIds }, depth, dot }) => ( 8 | 9 | {dot && .} 10 | {indent(depth)} 11 | {invocationIds.length === 1 && 12 | 18 | } 19 | 20 | ) 21 | 22 | export default styled(VarInvocation).as.span` 23 | ${props => (props.invocation.inline || props.dot) && 'display: inline-block;'} 24 | ` 25 | -------------------------------------------------------------------------------- /src/components/Workspace/Divider.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import theme from 'theme-proxy' 4 | 5 | const dividerThickness = 2 6 | 7 | const DividerWrapper = styled.div` 8 | top: 0; 9 | width: ${dividerThickness}px; 10 | cursor: col-resize; 11 | background-color: ${theme.colors.darkblue} 12 | ` 13 | 14 | const Divider = ({ width, onMouseDown }) => ( 15 | 19 | ) 20 | 21 | export default Divider 22 | -------------------------------------------------------------------------------- /src/components/Workspace/Embed.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Embed = () => ( 4 |
5 |
6 |
7 | ) 8 | 9 | export default Embed 10 | -------------------------------------------------------------------------------- /src/components/Workspace/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-as-components' 3 | import { FileExplorerContainer, EditorContainer } from 'containers' 4 | 5 | import Divider from './Divider' 6 | import Embed from './Embed' 7 | 8 | const Workspace = ({ 9 | workspaceActions, 10 | handleDividerMouseDown, 11 | handleEmbedDividerMouseDown, 12 | width, 13 | embedWidth, 14 | }) => ( 15 | 16 | 17 | 18 | 19 | 23 | 24 | 25 | ) 26 | 27 | export default styled(Workspace).as.div` 28 | & > div { 29 | position: absolute; 30 | bottom: 0; 31 | } 32 | & > div:first-child { 33 | position: absolute; 34 | top: 0; 35 | left: 0; 36 | bottom: 0; 37 | width: ${props => props.width}px; 38 | } 39 | & > div:nth-child(3) { 40 | top: 0; 41 | left: ${props => props.width}px; 42 | right: ${props => props.embedWidth}px; 43 | } 44 | & > div:last-child { 45 | top: 0; 46 | left: ${props => document.body.clientWidth - props.embedWidth}px; 47 | right: 0; 48 | } 49 | ` 50 | -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | export * from './Icons' 2 | export * from './Buttons' 3 | export { default as Props } from './Props' 4 | export { default as Logo } from './Logo' 5 | export { default as Backtick } from './Backtick' 6 | export { default as ClassComponent } from './ClassComponent' 7 | export { default as Flex } from './Flex' 8 | export { default as Input } from './Input' 9 | export { default as Keyword } from './Keyword' 10 | export { default as ProjectIndexDeclaration } from './ProjectIndexDeclaration/index' 11 | export { default as Workspace } from './Workspace' 12 | export { default as StatelessFunctionComponent } from './StatelessFunctionComponent' 13 | export { default as StyledComponent } from './StyledComponent' 14 | export { default as Standard } from './Standard' 15 | export { default as JSX } from './JSX' 16 | export { default as MapInvocation } from './MapInvocation' 17 | export { default as VarInvocation } from './VarInvocation' 18 | export { default as ParamInvocation } from './ParamInvocation' 19 | export { default as PropTypes } from './PropTypes' 20 | export { default as Json } from './Json' 21 | export { default as Semi } from './Semi' 22 | export { default as ObjectLiteral } from './ObjectLiteral' 23 | export { default as ExportAppButton } from './ExportAppButton' 24 | export { default as AddComponentTooltip } from './AddComponentTooltip' 25 | -------------------------------------------------------------------------------- /src/configureStore.js: -------------------------------------------------------------------------------- 1 | import { createStore, combineReducers, applyMiddleware, compose } from 'redux' 2 | import { routerReducer, routerMiddleware } from 'react-router-redux' 3 | import { save as makeSaver, load as makeLoader } from 'redux-localstorage-simple' 4 | 5 | import { spyMiddleware } from 'middleware' 6 | import appReducer from 'duck' 7 | 8 | const save = makeSaver({ debounce: 1000 }) // middleware 9 | 10 | // eslint-disable-next-line no-underscore-dangle 11 | const composeEnhancers = (typeof window === 'object' && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) 12 | // eslint-disable-next-line no-underscore-dangle 13 | ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ shouldHotReload: false }) 14 | : compose 15 | 16 | export default function configureStore(history) { 17 | const middleware = applyMiddleware(routerMiddleware(history), spyMiddleware, save) 18 | const enhancer = composeEnhancers(middleware) 19 | 20 | return createStore( 21 | combineReducers({ 22 | app: appReducer, 23 | router: routerReducer, 24 | }), 25 | makeLoader(), 26 | enhancer, 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /src/constantz.js: -------------------------------------------------------------------------------- 1 | export const RESOLVE_ALIASES = ['containers', 'components'] 2 | 3 | // getInitialState creates these model ids 4 | export const COMPONENTS_FILE_ID = 1 5 | export const CONTAINERS_FILE_ID = 2 6 | export const REACT_CHILDREN_DECLARATION_PARAM_ID = 1 7 | export const REACT_CHILDREN_CALL_PARAM_ID = 1 8 | export const REACT_CHILDREN_INVOCATION_ID = 1 9 | export const INDEX_NAME_ID = 1 10 | export const KEY_NAME_ID = 2 11 | export const ID_NAME_ID = 3 12 | export const APP_CONTAINER_NAME_ID = 9 13 | 14 | 15 | /* 16 | fileTypes 17 | */ 18 | export const JS = 'js' 19 | export const SC = '💅' 20 | export const JSON_TYPE = 'json' 21 | export const DIR = 'dir' 22 | 23 | export const fileTypes = { JS, SC, DIR, JSON_TYPE } 24 | export const fileTypesArray = Object.keys(fileTypes).map(k => fileTypes[k]) 25 | 26 | 27 | /* 28 | declarationTypes 29 | */ 30 | export const PROJECT_INDEX = 'top level index.js file' 31 | export const LOOKTHROUGH = "I'm all about my invocation, that's it" 32 | export const CLASS_COMPONENT = 'class_component' 33 | export const CLASS_METHOD = 'class_method' 34 | export const CLASS_PROP = 'class_prop' 35 | export const STATELESS_FUNCTION_COMPONENT = 'stateless_function_component' 36 | export const STYLED_COMPONENT = 'styled_component' 37 | export const CONST = 'const' 38 | export const STANDARD = 'standard' 39 | export const PROPS = 'props' 40 | export const OBJECT_LITERAL_KEY = 'object literal key' 41 | 42 | export const declarationTypes = { 43 | PROJECT_INDEX, 44 | LOOKTHROUGH, 45 | CLASS_COMPONENT, 46 | CLASS_METHOD, 47 | CLASS_PROP, 48 | STATELESS_FUNCTION_COMPONENT, 49 | STYLED_COMPONENT, 50 | CONST, 51 | STANDARD, 52 | PROPS, 53 | OBJECT_LITERAL_KEY, 54 | } 55 | export const declarationTypesArray = Object.keys(declarationTypes).map(k => declarationTypes[k]) 56 | 57 | 58 | /* 59 | component declaration types 60 | */ 61 | export const componentDeclarationTypes = [ 62 | STATELESS_FUNCTION_COMPONENT, 63 | STYLED_COMPONENT, 64 | CLASS_COMPONENT, 65 | ] 66 | 67 | 68 | /* 69 | invocation types 70 | */ 71 | export const VAR_INVOCATION = 'var invocation' 72 | export const PROPERTY_ACCESS = '.property access' 73 | export const IMPORT_VAR = 'import var' 74 | export const ARRAY_MAP_METHOD = '.map invocation' 75 | export const COMPONENT_INVOCATION = 'component invocation' // also draggable 76 | export const PARAM_INVOCATION = 'param invocation' // also draggable 77 | 78 | 79 | /* 80 | draggable types 81 | */ 82 | export const PROP = 'prop' 83 | export const PROPS_SPREAD = '...props' 84 | export const FILE = 'file' 85 | export const CALL_PARAM = 'call param' 86 | 87 | export const DraggableTypes = { 88 | PROP, 89 | COMPONENT_INVOCATION, 90 | PARAM_INVOCATION, 91 | PROPS_SPREAD, 92 | FILE, 93 | DIR, 94 | CALL_PARAM, 95 | } 96 | 97 | 98 | /* 99 | export types 100 | */ 101 | export const DEFAULT = 'export default' 102 | export const DEFAULT_INLINE = 'export default ' 103 | export const INLINE = 'export when declared' 104 | 105 | export const exportTypes = { 106 | DEFAULT, 107 | DEFAULT_INLINE, 108 | INLINE, 109 | false: 'no export', 110 | } 111 | export const exportTypesArray = Object.keys(exportTypes).map(k => exportTypes[k]) 112 | 113 | export const ES_KEYWORDS = ['do', 'if', 'in', 'for', 'let', 'new', 'try', 'var', 'case', 'else', 'enum', 'eval', 'null', 'this', 'true', 'void', 'with', 'await', 'break', 'catch', 'class', 'const', 'false', 'super', 'throw', 'while', 'yield', 'delete', 'export', 'import', 'public', 'return', 'static', 'switch', 'typeof', 'default', 'extends', 'finally', 'package', 'private', 'continue', 'debugger', 'function', 'arguments', 'interface', 'protected', 'implements', 'instanceof'] // eslint-disable-line 114 | -------------------------------------------------------------------------------- /src/containers/AperitifPostContainer/examples.js: -------------------------------------------------------------------------------- 1 | import apiArrayResponse from './apiArrayResponse.json' 2 | import twitter from './twitter.json' 3 | import nothingtodo from './nothingtodo.json' 4 | 5 | export default { 6 | 'github-issues': apiArrayResponse, 7 | nothingtodo, 8 | twitter, 9 | } 10 | -------------------------------------------------------------------------------- /src/containers/AperitifPostContainer/getJSON.js: -------------------------------------------------------------------------------- 1 | import { isEmpty } from 'lodash' 2 | import C from 'check-types' 3 | 4 | export default function getJSON(value) { 5 | let json 6 | let error 7 | // try JSON.parse 8 | try { 9 | json = JSON.parse(value) 10 | } catch (e) { 11 | error = `${e.message}` // JSON.parse error 12 | } 13 | // try eval 14 | try { 15 | json = JSON.parse(JSON.stringify(eval(`(${value})`))) // eslint-disable-line no-eval 16 | } catch (e) { 17 | // do nothing. If eval worked then the parsing will work, if not too bad. 18 | } 19 | 20 | if (json) { 21 | if (isEmpty(json) || (C.array(json) && json.every(obj => isEmpty(obj)))) { 22 | return { 23 | error: 'The example must be non-empty', 24 | } 25 | } 26 | 27 | if ((C.object(json) || C.array.of.object(json))) { 28 | return { json } 29 | } 30 | 31 | error = 'The response shape must be an array of objects or an object' 32 | } 33 | return { error } 34 | } 35 | -------------------------------------------------------------------------------- /src/containers/AperitifPostContainer/gh-issues.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielrob/aperitif-editor/3b1f2b71f2dae208b6f3f4e04b7bf0c552395c52/src/containers/AperitifPostContainer/gh-issues.png -------------------------------------------------------------------------------- /src/containers/AperitifPostContainer/index.js: -------------------------------------------------------------------------------- 1 | import { trim } from 'lodash' 2 | import T from 'prop-types' 3 | import { forbidExtraProps } from 'airbnb-prop-types' 4 | import React from 'react' 5 | import { connect } from 'react-redux' 6 | import { createStructuredSelector } from 'reselect' 7 | 8 | import { selectProjectInitialized } from 'selectors' 9 | import { initializeApp, newContainerPlease } from 'duck' 10 | 11 | import exampleData from './examples' 12 | import getJSON from './getJSON' 13 | import AperitifPost from './AperitifPost' 14 | 15 | class AperitifPostContainer extends React.PureComponent { 16 | state = { 17 | textarea: '', 18 | error: null, 19 | } 20 | 21 | onChange = e => { 22 | const textarea = trim(e.target.value) 23 | const { error } = getJSON(textarea) 24 | 25 | this.setState({ textarea, error: (textarea && error) || null }) 26 | } 27 | 28 | onFocus = () => { 29 | this.setState({ error: null }) 30 | } 31 | 32 | populate = (key, baseName) => () => { 33 | this.setState({ 34 | textarea: JSON.stringify(exampleData[key], null, 2), 35 | baseName, 36 | }) 37 | } 38 | 39 | submit = () => { 40 | const { initializeApp, newContainerPlease, projectIsInitalized } = this.props 41 | const { textarea } = this.state 42 | let { baseName } = this.state 43 | const { error, json } = getJSON(textarea) 44 | 45 | if (document.getElementById('baseName') && !baseName) { 46 | baseName = document.getElementById('baseName').value 47 | } 48 | 49 | if (error) { 50 | return this.setState({ error }) 51 | } 52 | 53 | if (projectIsInitalized) { 54 | newContainerPlease(json, baseName) 55 | } else { 56 | initializeApp(json, baseName) 57 | } 58 | } 59 | 60 | render() { 61 | const { projectIsInitalized } = this.props 62 | const { error, textarea } = this.state 63 | 64 | return ( 65 | 79 | ) 80 | } 81 | } 82 | 83 | 84 | /* 85 | propTypes 86 | */ 87 | AperitifPostContainer.propTypes = forbidExtraProps({ 88 | projectIsInitalized: T.bool.isRequired, 89 | initializeApp: T.func.isRequired, 90 | newContainerPlease: T.func.isRequired, 91 | }) 92 | 93 | 94 | /* 95 | connect 96 | */ 97 | const mapStateToProps = createStructuredSelector({ 98 | projectIsInitalized: selectProjectInitialized, 99 | }) 100 | 101 | const mapDispatchToProps = { 102 | initializeApp, 103 | newContainerPlease, 104 | } 105 | 106 | export default connect(mapStateToProps, mapDispatchToProps)(AperitifPostContainer) 107 | -------------------------------------------------------------------------------- /src/containers/AperitifPostContainer/nothingtodo.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Today", 4 | "items": [ 5 | { 6 | "id": "It's your unique charisma, that caught my attention", 7 | "title": "Relax, and breathe", 8 | "image_url": "https://images.unsplash.com/photo-1510020553968-30f966e1ec9e" 9 | }, 10 | { 11 | "id": "It's your beautiful smile, I recognized it instantly", 12 | "title": "Smile, and remember, life is funny", 13 | "image_url": "https://images.unsplash.com/photo-1518020382113-a7e8fc38eac9" 14 | }, 15 | { 16 | "id": "You make time for the important things, I appreciate that about you", 17 | "title": "Take a moment, have some tea.", 18 | "image_url": "https://images.unsplash.com/photo-1521020853611-dc6ec88bb764" 19 | } 20 | ] 21 | }, 22 | { 23 | "name": "Tomorrow", 24 | "items": [ 25 | { 26 | "id": "beautiful moments", 27 | "title": "There will be so many beautiful moments", 28 | "image_url": "https://images.unsplash.com/photo-1494236581341-7d38b2e7d824" 29 | }, 30 | { 31 | "id": "the world is safe", 32 | "title": "Without you doing anything, the sun will come up, and the birds will sing", 33 | "image_url": "https://images.unsplash.com/photo-1519746889240-5cfec298abd5" 34 | } 35 | ] 36 | } 37 | ] 38 | -------------------------------------------------------------------------------- /src/containers/AperitifPostContainer/nothingtodo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielrob/aperitif-editor/3b1f2b71f2dae208b6f3f4e04b7bf0c552395c52/src/containers/AperitifPostContainer/nothingtodo.png -------------------------------------------------------------------------------- /src/containers/AperitifPostContainer/twitter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielrob/aperitif-editor/3b1f2b71f2dae208b6f3f4e04b7bf0c552395c52/src/containers/AperitifPostContainer/twitter.png -------------------------------------------------------------------------------- /src/containers/App/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { Helmet } from 'react-helmet' 3 | import { Switch, Route } from 'react-router-dom' 4 | import styled from 'styled-components' 5 | 6 | import { NotFoundPage, WorkspaceContainer } from 'containers' 7 | 8 | export default class App extends Component { 9 | render() { 10 | return ( 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | ) 21 | } 22 | } 23 | 24 | const AppWrapper = styled.div` 25 | position: absolute; 26 | top: 0; 27 | right: 0; 28 | left: 0; 29 | bottom: 0; 30 | ` 31 | -------------------------------------------------------------------------------- /src/containers/ComponentInvocationContainer/components/CallParam.js: -------------------------------------------------------------------------------- 1 | import T from 'prop-types' 2 | import { forbidExtraProps } from 'airbnb-prop-types' 3 | import React from 'react' 4 | import { JSX } from 'components' 5 | import { Name, NameInput } from 'containers' 6 | import { callParamPropTypes } from 'model-prop-types' 7 | 8 | const CallParam = ({ 9 | callParam: { id, valueInvocationId, declIsSpreadMember, nameId, invokeNameId, valueString }, 10 | tagHasPropsSpread, 11 | spreadPropsIsOverTag, 12 | connectDragSource, 13 | isDragging, 14 | }) => 15 | connectDragSource( 16 | 17 | {/* Either invokes an Invocation */} 18 | {valueInvocationId ? ( 19 | 20 | {' '} 21 | ={'{'} 22 | 23 | {'}'} 24 | 25 | ) : ( 26 | /* Or a DeclParam in which case display unless its a spread member & tag has spread */ 27 | !(declIsSpreadMember && (tagHasPropsSpread || spreadPropsIsOverTag)) && ( 28 | 29 | {' '} 30 | 31 | = 32 | {'{'} 33 | {declIsSpreadMember && 'props.'} 34 | {valueString || } 35 | {'}'} 36 | 37 | ) 38 | )} 39 | 40 | ) 41 | 42 | /* 43 | propTypes 44 | */ 45 | CallParam.propTypes = forbidExtraProps({ 46 | invocationId: T.number.isRequired, 47 | spreadPropsIsOverTag: T.bool.isRequired, 48 | tagHasPropsSpread: T.bool.isRequired, 49 | callParam: T.shape(callParamPropTypes).isRequired, 50 | connectDragSource: T.func.isRequired, 51 | }) 52 | 53 | export default CallParam 54 | -------------------------------------------------------------------------------- /src/containers/ComponentInvocationContainer/components/CloseTag.js: -------------------------------------------------------------------------------- 1 | import T from 'prop-types' 2 | import React from 'react' 3 | 4 | import { invocationPropTypes } from 'model-prop-types' 5 | import { indent } from 'utils' 6 | import { Name } from 'containers' 7 | 8 | const CloseTag = ({ depth, invocation: { inline, nameId, closed } }) => !closed ? ( 9 | 10 | {!inline && indent(depth)} 11 | {'<'}/{'>'} 12 | 13 | ) : null 14 | 15 | export default CloseTag 16 | 17 | CloseTag.propTypes = { 18 | depth: T.number.isRequired, 19 | invocation: invocationPropTypes.isRequired, 20 | } 21 | -------------------------------------------------------------------------------- /src/containers/ComponentInvocationContainer/components/ComponentInvocation.js: -------------------------------------------------------------------------------- 1 | import T from 'prop-types' 2 | import { forbidExtraProps } from 'airbnb-prop-types' 3 | import React from 'react' 4 | import styled from 'styled-as-components' 5 | 6 | import theme from 'theme-proxy' 7 | import { invocationPropTypes } from 'model-prop-types' 8 | import { COMPONENT_INVOCATION, PARAM_INVOCATION } from 'constantz' 9 | 10 | import { OpenTagContainer } from '../containers' 11 | import { InvocationChildren, CloseTag, CIDropzones } from './' 12 | 13 | const ComponentInvocation = ({ 14 | depth, 15 | invocation, 16 | invocation: { 17 | invocationId, 18 | invocationIds, 19 | closed, 20 | }, 21 | connectDropTarget, 22 | connectClosingDropTarget, 23 | isOverCIT1, 24 | isOverCIT2, 25 | dragItem, 26 | isOverCI, 27 | }) => ( 28 | 29 | {/* */} 30 | {connectDropTarget( 31 | 32 | 36 | 43 | 44 | )} 45 | {/* {children} */} 46 | 51 | {/* */} 52 | {connectClosingDropTarget( 53 | 54 | 61 | 65 | 66 | )} 67 | 68 | ) 69 | 70 | /* 71 | propTypes 72 | */ 73 | ComponentInvocation.propTypes = forbidExtraProps({ 74 | // parent 75 | depth: T.number.isRequired, 76 | initial: T.bool.isRequired, 77 | parentId: T.number, 78 | type: T.oneOf([COMPONENT_INVOCATION, PARAM_INVOCATION]), 79 | 80 | // container 81 | invocation: invocationPropTypes.isRequired, 82 | isOverCI: T.bool.isRequired, 83 | 84 | // React DnD: 85 | connectDropTarget: T.func.isRequired, 86 | connectClosingDropTarget: T.func.isRequired, 87 | isOverCIT1: T.bool.isRequired, 88 | isOverCIT2: T.bool.isRequired, 89 | dragItem: T.shape({ name: T.string }), 90 | }) 91 | 92 | ComponentInvocation.defaultProps = { 93 | dragItem: null, 94 | parentId: null, 95 | type: null, 96 | } 97 | 98 | /* 99 | style, export 100 | */ 101 | export default styled(ComponentInvocation).as.span` 102 | color: ${theme.colors.darkgreen}; 103 | padding-left: 0; 104 | cursor: ${props => (props.initial ? 'inherit' : 'pointer')} 105 | user-select: text; 106 | line-height: 2; 107 | ` 108 | -------------------------------------------------------------------------------- /src/containers/ComponentInvocationContainer/components/InvocationChildren.js: -------------------------------------------------------------------------------- 1 | import T from 'prop-types' 2 | import React from 'react' 3 | 4 | import { invocationPropTypes } from 'model-prop-types' 5 | import { JSX } from 'components' 6 | 7 | import { IntermediaryDropzonesContainer } from '../containers' 8 | 9 | const InvocationChildren = ({ 10 | invocation: { 11 | invocationId: parentId, 12 | invocationIds, 13 | inline, 14 | }, 15 | depth, 16 | }) => 17 | invocationIds.reduce( 18 | (out, invocationId, position) => { 19 | out.push( 20 | 27 | ) 28 | if (!inline) { 29 | out.push( 30 | 36 | ) 37 | } 38 | return out 39 | }, 40 | [] 41 | ) 42 | 43 | /* 44 | propTypes 45 | */ 46 | InvocationChildren.propTypes = { 47 | invocation: invocationPropTypes.isRequired, 48 | depth: T.number.isRequired, 49 | isOverCI: T.bool.isRequired, 50 | } 51 | 52 | export default InvocationChildren 53 | 54 | -------------------------------------------------------------------------------- /src/containers/ComponentInvocationContainer/components/OpenTag.js: -------------------------------------------------------------------------------- 1 | import { partition } from 'lodash' 2 | import T from 'prop-types' 3 | import { forbidExtraProps } from 'airbnb-prop-types' 4 | import React from 'react' 5 | import styled from 'styled-as-components' 6 | 7 | import theme from 'theme-proxy' 8 | import { invocationPropTypes } from 'model-prop-types' 9 | import { PROP, PROPS_SPREAD, CALL_PARAM } from 'constantz' 10 | import { indent } from 'utils' 11 | import { JSX } from 'components' 12 | import { Name, NameInput } from 'containers' 13 | import { CallParamDragContainer } from '../containers' 14 | 15 | const OpenTag = ({ 16 | isOverOpenTag, 17 | dragItem, 18 | depth, 19 | invocation: { 20 | invocationId, 21 | callParams, 22 | closed, 23 | hasPropsSpread, 24 | pseudoSpreadPropsName, 25 | nameId, 26 | }, 27 | }) => { 28 | // three types of drop 29 | const spreadPropsIsOver = isOverOpenTag && dragItem.type === PROPS_SPREAD 30 | const propIsOver = isOverOpenTag && dragItem.type === PROP && 31 | canDropPropToOpenTag(callParams, pseudoSpreadPropsName, dragItem) 32 | const callParamIsOver = isOverOpenTag && dragItem.type === CALL_PARAM && 33 | canDropCallParamToOpenTag(callParams, dragItem) 34 | 35 | const [keyParams, standardCallParams] = partition(callParams, p => p.name === 'key') 36 | const keyParam = keyParams[0] 37 | 38 | return ( 39 | 40 | {indent(depth)}{'<'} 41 | {/* key= special case */} 42 | {keyParam && ( 43 | 44 | {' '}={'{'}{'}'} 45 | 46 | )} 47 | {/* PROPS_SPREAD Dropzone */} 48 | {(hasPropsSpread || spreadPropsIsOver) && ( 49 | 50 | {' {'}...props{'}'} 51 | 52 | )} 53 | {/* spread props */} 54 | {pseudoSpreadPropsName && ( 55 | 56 | {' {'}...{pseudoSpreadPropsName}{'}'} 57 | 58 | )} 59 | {/* PROP Dropzone */} 60 | {propIsOver && ( 61 | 62 | {' '} 63 | ( 66 | 67 | {name}={'{'} 68 | {dragItem.isSpreadMember && 'props.'} 69 | {name} 70 | 71 | )} 72 | /> 73 | {'}'} 74 | 75 | )} 76 | {/* CALL_PARAM Dropzone */} 77 | {callParamIsOver && ( 78 | 79 | {' '} 80 | 81 | {dragItem.name}={'{'} 82 | {dragItem.declIsSpreadMember && 'props.'} 83 | {dragItem.name} 84 | 85 | 86 | )} 87 | {/* normal call params */} 88 | {standardCallParams.map(callParam => ( 89 | 95 | ))} 96 | {closed && ' /'} 97 | {'>'} 98 | 99 | ) 100 | } 101 | 102 | /* 103 | propTypes 104 | */ 105 | OpenTag.propTypes = forbidExtraProps({ 106 | invocation: invocationPropTypes.isRequired, 107 | depth: T.number.isRequired, 108 | // for wrapper 109 | innerRef: T.func.isRequired, 110 | // Injected by React DnD: 111 | isOverOpenTag: T.bool.isRequired, 112 | dragItem: T.shape({ type: T.string }).isRequired, 113 | }) 114 | 115 | const NewAttributePreview = styled.span` 116 | color: ${theme.color.darkgreen}; 117 | transition: 250ms; 118 | ` 119 | 120 | /* 121 | style, export 122 | */ 123 | export default styled(OpenTag).as.div` 124 | ${props => props.invocation.inline && 'display: inline-block;'} 125 | ` 126 | 127 | // helpers 128 | export const canDropPropToOpenTag = (targetCallParams, pseudoSpreadPropsName, propBeingDragged) => 129 | !targetCallParams.find(({ declParamId }) => declParamId === propBeingDragged.paramId) 130 | && pseudoSpreadPropsName !== propBeingDragged.name 131 | 132 | export const canDropCallParamToOpenTag = (targetCallParams, callParamBeingDragged) => 133 | !targetCallParams.find(({ name }) => name === callParamBeingDragged.name) 134 | -------------------------------------------------------------------------------- /src/containers/ComponentInvocationContainer/components/PropDropzone.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import T from 'prop-types' 3 | import { forbidExtraProps } from 'airbnb-prop-types' 4 | import styled, { css } from 'styled-as-components' 5 | import theme from 'theme-proxy' 6 | 7 | const PropDropzone = ({ children }) =>
{children}
8 | 9 | /* 10 | propTypes 11 | */ 12 | PropDropzone.propTypes = forbidExtraProps({ 13 | isOver: T.bool.isRequired, 14 | children: T.node.isRequired, 15 | innerRef: T.func.isRequired, 16 | }) 17 | 18 | /* 19 | style, export 20 | */ 21 | export default styled(PropDropzone).as.div` 22 | position: relative; 23 | padding: 0 300px 0 200px; 24 | margin-left: -200px; 25 | white-space: pre; 26 | > * { 27 | display: inline-block; 28 | border-radius: 6px; 29 | border: 1px dotted; 30 | padding: 10px; 31 | } 32 | transition: 70ms ease-in-out; 33 | font-size: ${props => props.isOver ? '16px' : '2px'}; 34 | backface-visibility: hidden; 35 | transform: translateZ(0) scale(1.0, 1.0); 36 | ${props => 37 | props.isOver && 38 | css` 39 | color: ${theme.colors.darkgreen}; 40 | `} 41 | ` 42 | 43 | -------------------------------------------------------------------------------- /src/containers/ComponentInvocationContainer/components/ReorderDropzone.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | const ReorderDropzone = styled.div` 4 | border-left: 1px dotted black; 5 | padding: 5px 5px; 6 | min-height: ${props => props.ciDimensions.clientHeight}px; 7 | min-width: ${props => props.ciDimensions.clientWidth - (props.depth * 20)}px; 8 | box-sizing: initial; 9 | ` 10 | 11 | export default ReorderDropzone 12 | -------------------------------------------------------------------------------- /src/containers/ComponentInvocationContainer/components/index.js: -------------------------------------------------------------------------------- 1 | export { default as ComponentInvocation } from './ComponentInvocation' 2 | export { default as CloseTag } from './CloseTag' 3 | export { default as InvocationChildren } from './InvocationChildren' 4 | export { default as PropDropzone } from './PropDropzone' 5 | export { default as CIDropzones } from './CIDropzones' 6 | export { default as ReorderDropzone } from './ReorderDropzone' 7 | export { default as OpenTag, canDropPropToOpenTag, canDropCallParamToOpenTag } from './OpenTag' 8 | export { default as CallParam } from './CallParam' 9 | -------------------------------------------------------------------------------- /src/containers/ComponentInvocationContainer/containers/AddInvocationFromFileDropzone.js: -------------------------------------------------------------------------------- 1 | import T from 'prop-types' 2 | import { forbidExtraProps } from 'airbnb-prop-types' 3 | import React from 'react' 4 | import { connect } from 'react-redux' 5 | import { DropTarget } from 'react-dnd' 6 | import { findDOMNode } from 'react-dom' 7 | 8 | import { compose } from 'utils' 9 | import { FILE, DIR } from 'constantz' 10 | import { addInvocationFromFileToCI } from 'duck' 11 | 12 | import { PropDropzone } from '../components' 13 | 14 | class AddInvocationFromFileDropzone extends React.PureComponent { 15 | render() { 16 | const { connectDropTarget, isOver, children } = this.props 17 | return ( 18 | connectDropTarget(findDOMNode(innerRef))} 20 | isOver={isOver} 21 | > 22 | {children} 23 | 24 | ) 25 | } 26 | } 27 | 28 | AddInvocationFromFileDropzone.propTypes = forbidExtraProps({ 29 | // passed by parent 30 | targetInvocationId: T.number.isRequired, 31 | targetPosition: T.number.isRequired, 32 | children: T.node.isRequired, 33 | 34 | // mapDispatchToProps 35 | addInvocationFromFileToCI: T.func.isRequired, 36 | 37 | // Injected by React DnD: 38 | connectDropTarget: T.func.isRequired, 39 | isOver: T.bool.isRequired, 40 | }) 41 | 42 | 43 | /* 44 | connect 45 | */ 46 | const mapDispatchToProps = { 47 | addInvocationFromFileToCI, 48 | } 49 | 50 | /* 51 | dnd 52 | */ 53 | const dropzoneTarget = { 54 | drop(props, monitor) { 55 | const { addInvocationFromFileToCI, targetInvocationId, targetPosition } = props 56 | const { fileId, isDirectory } = monitor.getItem() 57 | 58 | addInvocationFromFileToCI({ targetInvocationId, targetPosition, fileId, isDirectory }) 59 | }, 60 | } 61 | 62 | const collect = (connect, monitor) => ({ 63 | connectDropTarget: connect.dropTarget(), 64 | isOver: monitor.isOver(), 65 | }) 66 | 67 | /* 68 | compose export 69 | */ 70 | export default compose( 71 | connect(null, mapDispatchToProps), 72 | DropTarget([FILE, DIR], dropzoneTarget, collect) 73 | )(AddInvocationFromFileDropzone) 74 | 75 | -------------------------------------------------------------------------------- /src/containers/ComponentInvocationContainer/containers/CallParamDragContainer.js: -------------------------------------------------------------------------------- 1 | import T from 'prop-types' 2 | import { forbidExtraProps } from 'airbnb-prop-types' 3 | import React from 'react' 4 | import { DragSource } from 'react-dnd' 5 | import { CALL_PARAM } from 'constantz' 6 | 7 | import { compose } from 'utils' 8 | import { callParamPropTypes } from 'model-prop-types' 9 | import { CallParam } from '../components' 10 | 11 | const CallParamDragContainer = props => 12 | 13 | /* 14 | propTypes 15 | */ 16 | CallParamDragContainer.propTypes = forbidExtraProps({ 17 | invocationId: T.number.isRequired, 18 | spreadPropsIsOverTag: T.bool.isRequired, 19 | tagHasPropsSpread: T.bool.isRequired, 20 | callParam: T.shape(callParamPropTypes).isRequired, 21 | connectDragSource: T.func.isRequired, 22 | }) 23 | 24 | /* 25 | dnd - source 26 | */ 27 | const propSource = { 28 | beginDrag({ 29 | callParam: { id, name, nameId }, 30 | invocationId, 31 | }) { 32 | return { 33 | type: CALL_PARAM, 34 | paramId: id, 35 | name, 36 | nameId, 37 | sourceInvocationId: invocationId, 38 | } 39 | }, 40 | } 41 | 42 | const sourceCollect = (connect, monitor) => ({ 43 | connectDragSource: connect.dragSource(), 44 | isDragging: monitor.isDragging(), 45 | }) 46 | 47 | 48 | /* 49 | compose export 50 | */ 51 | export default compose( 52 | DragSource(CALL_PARAM, propSource, sourceCollect), 53 | )(CallParamDragContainer) 54 | -------------------------------------------------------------------------------- /src/containers/ComponentInvocationContainer/containers/IntermediaryDropzonesContainer.js: -------------------------------------------------------------------------------- 1 | import T from 'prop-types' 2 | import { forbidExtraProps } from 'airbnb-prop-types' 3 | import React from 'react' 4 | import { DropTarget } from 'react-dnd' 5 | import { compose } from 'utils' 6 | 7 | import { acceptedDropTypes, getIsValidOver } from '../helpers' 8 | import { CIDropzones } from '../components' 9 | 10 | class IntermediaryDropzonesContainer extends React.PureComponent { 11 | render() { 12 | const { connectDropTarget, isOverThisDropzone, inline, ...props } = this.props 13 | 14 | return connectDropTarget( 15 |
16 | 17 |
18 | ) 19 | } 20 | } 21 | 22 | IntermediaryDropzonesContainer.propTypes = forbidExtraProps({ 23 | // from parent 24 | invocationId: T.number.isRequired, 25 | depth: T.number.isRequired, 26 | position: T.number.isRequired, 27 | 28 | // Injected by React DnD: 29 | connectDropTarget: T.func.isRequired, 30 | isOverThisDropzone: T.bool.isRequired, 31 | dragItem: T.shape({ name: T.string }), 32 | }) 33 | 34 | IntermediaryDropzonesContainer.defaultProps = { 35 | dragItem: null, 36 | } 37 | 38 | /* 39 | dnd 40 | */ 41 | // target 42 | const dropzoneTarget = {} 43 | 44 | const targetCollect = (connect, monitor) => ({ 45 | connectDropTarget: connect.dropTarget(), 46 | isOverThisDropzone: getIsValidOver(monitor), 47 | dragItem: monitor.getItem(), 48 | }) 49 | 50 | /* 51 | compose export 52 | */ 53 | export default compose( 54 | DropTarget(acceptedDropTypes, dropzoneTarget, targetCollect) 55 | )(IntermediaryDropzonesContainer) 56 | 57 | -------------------------------------------------------------------------------- /src/containers/ComponentInvocationContainer/containers/OpenTagContainer.js: -------------------------------------------------------------------------------- 1 | import T from 'prop-types' 2 | import React from 'react' 3 | import { connect } from 'react-redux' 4 | import { DropTarget } from 'react-dnd' 5 | import { findDOMNode } from 'react-dom' 6 | 7 | import { invocationPropTypes } from 'model-prop-types' 8 | import { addAttributeToComponentInvocation, addPropsSpreadToComponentInvocation, moveCallParam } from 'duck' 9 | import { PROP, CALL_PARAM, PROPS_SPREAD } from 'constantz' 10 | import { compose } from 'utils' 11 | 12 | import { OpenTag, canDropPropToOpenTag, canDropCallParamToOpenTag } from '../components' 13 | 14 | class OpenTagContainer extends React.PureComponent { 15 | render() { 16 | const { 17 | connectDropTarget, 18 | dragItem, 19 | isOverOpenTag, 20 | depth, 21 | invocation, 22 | } = this.props 23 | 24 | return ( 25 | connectDropTarget(findDOMNode(innerRef))} 27 | depth={depth} 28 | dragItem={dragItem} 29 | isOverOpenTag={isOverOpenTag} 30 | invocation={invocation} 31 | /> 32 | ) 33 | } 34 | } 35 | 36 | 37 | /* 38 | PropTypes 39 | */ 40 | OpenTagContainer.propTypes = { 41 | depth: T.number.isRequired, 42 | 43 | invocation: invocationPropTypes.isRequired, 44 | 45 | // Injected by connect: 46 | addAttributeToComponentInvocation: T.func.isRequired, 47 | addPropsSpreadToComponentInvocation: T.func.isRequired, 48 | 49 | // Injected by React DnD: 50 | connectDropTarget: T.func.isRequired, 51 | isOverOpenTag: T.bool.isRequired, 52 | dragItem: T.shape({ type: T.string }).isRequired, 53 | } 54 | 55 | 56 | /* 57 | connect 58 | */ 59 | const mapDispatchToProps = { 60 | addAttributeToComponentInvocation, 61 | addPropsSpreadToComponentInvocation, 62 | moveCallParam, 63 | } 64 | 65 | 66 | /* 67 | dnd - target 68 | */ 69 | const dropTypes = [PROP, PROPS_SPREAD, CALL_PARAM] 70 | 71 | const dropzoneTarget = { 72 | drop(props, monitor) { 73 | const { invocation: { invocationId: targetInvocationId } } = props 74 | 75 | switch (monitor.getItemType()) { 76 | case PROP: { 77 | const { addAttributeToComponentInvocation } = props 78 | return addAttributeToComponentInvocation({ targetInvocationId, prop: monitor.getItem() }) 79 | } 80 | case PROPS_SPREAD: { 81 | const { addPropsSpreadToComponentInvocation } = props 82 | return addPropsSpreadToComponentInvocation({ targetInvocationId }) 83 | } 84 | case CALL_PARAM: { 85 | const { paramId, sourceInvocationId } = monitor.getItem() 86 | const { moveCallParam } = props 87 | return moveCallParam({ paramId, targetInvocationId, sourceInvocationId }) 88 | } 89 | // no default 90 | } 91 | }, 92 | 93 | canDrop(props, monitor) { 94 | const { invocation: { callParams, pseudoSpreadPropsName } } = props 95 | switch (monitor.getItemType()) { 96 | case PROP: { 97 | return canDropPropToOpenTag(callParams, pseudoSpreadPropsName, monitor.getItem()) 98 | } 99 | case CALL_PARAM: { 100 | return canDropCallParamToOpenTag(callParams, monitor.getItem()) 101 | } 102 | // no default 103 | } 104 | }, 105 | } 106 | 107 | const collect = (connect, monitor) => ({ 108 | connectDropTarget: connect.dropTarget(), 109 | isOverOpenTag: monitor.isOver(), 110 | dragItem: { 111 | ...monitor.getItem(), 112 | type: monitor.getItemType(), 113 | }, 114 | }) 115 | 116 | 117 | /* 118 | compose export 119 | */ 120 | export default compose( 121 | connect(null, mapDispatchToProps), 122 | DropTarget(dropTypes, dropzoneTarget, collect) 123 | )(OpenTagContainer) 124 | 125 | -------------------------------------------------------------------------------- /src/containers/ComponentInvocationContainer/containers/PropDropzoneContainer.js: -------------------------------------------------------------------------------- 1 | import T from 'prop-types' 2 | import { forbidExtraProps } from 'airbnb-prop-types' 3 | import React from 'react' 4 | import { connect } from 'react-redux' 5 | import { DropTarget } from 'react-dnd' 6 | import { findDOMNode } from 'react-dom' 7 | 8 | import { compose } from 'utils' 9 | import { PROP, PARAM_INVOCATION, REACT_CHILDREN_INVOCATION_ID } from 'constantz' 10 | import { 11 | addParamAsComponentInvocationChild, 12 | addNewComponentToInvocationWithMap, 13 | addNewComponentToInvocationWithSpread, 14 | addNewComponentToInvocationWithAttribute, 15 | addNewComponentToInvocationWithChildren, 16 | addNewStyledComponentToInvocation, 17 | addNewStyledUrlToInvocation, 18 | addStyledImgToInvocation, 19 | moveInvocation, 20 | } from 'duck' 21 | 22 | import { PropDropzone } from '../components' 23 | 24 | class PropDropzoneContainer extends React.PureComponent { 25 | render() { 26 | const { connectDropTarget, isOver, children } = this.props 27 | return ( 28 | connectDropTarget(findDOMNode(innerRef))} isOver={isOver}> 29 | {children} 30 | 31 | ) 32 | } 33 | } 34 | 35 | const dropActionMap = { 36 | asParamInvocation: 'addParamAsComponentInvocationChild', 37 | newWithSpread: 'addNewComponentToInvocationWithSpread', 38 | newWithMap: 'addNewComponentToInvocationWithMap', 39 | newWithAttribute: 'addNewComponentToInvocationWithAttribute', 40 | newWithChild: 'addNewComponentToInvocationWithChildren', 41 | newStyled: 'addNewStyledComponentToInvocation', 42 | newStyledUrl: 'addNewStyledUrlToInvocation', 43 | newStyledImg: 'addStyledImgToInvocation', 44 | } 45 | 46 | PropDropzoneContainer.propTypes = forbidExtraProps({ 47 | // passed by parent 48 | targetInvocationId: T.number.isRequired, 49 | targetPosition: T.number.isRequired, 50 | children: T.node.isRequired, 51 | dropActionKey: T.oneOf(Object.keys(dropActionMap)), 52 | 53 | // connect 54 | addParamAsComponentInvocationChild: T.func.isRequired, 55 | addNewComponentToInvocationWithMap: T.func.isRequired, 56 | addNewComponentToInvocationWithSpread: T.func.isRequired, 57 | addNewComponentToInvocationWithAttribute: T.func.isRequired, 58 | addNewComponentToInvocationWithChildren: T.func.isRequired, 59 | addNewStyledComponentToInvocation: T.func.isRequired, 60 | addNewStyledUrlToInvocation: T.func.isRequired, 61 | moveInvocation: T.func.isRequired, 62 | 63 | // React Dnd 64 | connectDropTarget: T.func.isRequired, 65 | isOver: T.bool.isRequired, 66 | }) 67 | 68 | PropDropzoneContainer.defaultProps = { 69 | dropActionKey: null, 70 | } 71 | 72 | 73 | /* 74 | connect 75 | */ 76 | const mapDispatchToProps = { 77 | addParamAsComponentInvocationChild, 78 | addNewComponentToInvocationWithMap, 79 | addNewComponentToInvocationWithSpread, 80 | addNewComponentToInvocationWithAttribute, 81 | addNewComponentToInvocationWithChildren, 82 | addNewStyledComponentToInvocation, 83 | addNewStyledUrlToInvocation, 84 | addStyledImgToInvocation, 85 | moveInvocation, 86 | } 87 | 88 | /* 89 | dnd - target 90 | */ 91 | const dropzoneTarget = { 92 | drop(props, monitor) { 93 | switch (monitor.getItemType()) { 94 | case PROP: { 95 | const { dropActionKey, targetInvocationId, targetPosition } = props 96 | const prop = monitor.getItem() 97 | 98 | return props[dropActionMap[dropActionKey]]({ targetInvocationId, targetPosition, prop }) 99 | } 100 | 101 | case PARAM_INVOCATION: { 102 | const { moveInvocation, targetInvocationId, targetPosition } = props 103 | const { sourceParentId, sourceInvocationId } = monitor.getItem() 104 | return moveInvocation({ 105 | sourceParentId, 106 | sourceInvocationId, 107 | targetInvocationId, 108 | targetPosition, 109 | }) 110 | } 111 | default: 112 | } 113 | }, 114 | canDrop(props, monitor) { 115 | const { targetInvocationId } = props 116 | const item = monitor.getItem() 117 | 118 | // Throw together for disabling moving {children} outside of it's parent invocation. 119 | return !( 120 | monitor.getItemType() === PARAM_INVOCATION && 121 | item.callParamId === REACT_CHILDREN_INVOCATION_ID && 122 | item.sourceParentId !== targetInvocationId 123 | ) 124 | }, 125 | } 126 | 127 | const collect = (connect, monitor) => ({ 128 | connectDropTarget: connect.dropTarget(), 129 | isOver: monitor.isOver(), 130 | }) 131 | 132 | /* 133 | compose export 134 | */ 135 | export default compose( 136 | connect(null, mapDispatchToProps), 137 | DropTarget([PROP, PARAM_INVOCATION], dropzoneTarget, collect) 138 | )(PropDropzoneContainer) 139 | -------------------------------------------------------------------------------- /src/containers/ComponentInvocationContainer/containers/ReorderDropzoneContainer.js: -------------------------------------------------------------------------------- 1 | import T from 'prop-types' 2 | import React from 'react' 3 | import { connect } from 'react-redux' 4 | import { DropTarget } from 'react-dnd' 5 | import { findDOMNode } from 'react-dom' 6 | 7 | import { compose } from 'utils' 8 | import { COMPONENT_INVOCATION } from 'constantz' 9 | import { moveInvocation } from 'duck' 10 | 11 | import { ReorderDropzone } from '../components' 12 | 13 | class ReorderDropzoneContainer extends React.PureComponent { 14 | render() { 15 | const { connectDropTarget, ciDimensions, ...props } = this.props 16 | return ( 17 | connectDropTarget(findDOMNode(innerRef))} 19 | ciDimensions={ciDimensions} 20 | {...props} 21 | /> 22 | ) 23 | } 24 | } 25 | 26 | ReorderDropzoneContainer.propTypes = { 27 | // passed by parent 28 | targetInvocationId: T.number.isRequired, 29 | targetPosition: T.number.isRequired, 30 | parentId: T.number.isRequired, 31 | type: T.oneOf([COMPONENT_INVOCATION]).isRequired, 32 | 33 | // from dragItem via parent 34 | sourceInvocationId: T.number.isRequired, 35 | ciDimensions: T.shape({ clientWidth: T.number, clientHeight: T.number }).isRequired, 36 | depth: T.number.isRequired, 37 | 38 | // connect 39 | moveInvocation: T.func.isRequired, 40 | 41 | // React Dnd 42 | connectDropTarget: T.func.isRequired, 43 | } 44 | 45 | 46 | /* 47 | connect 48 | */ 49 | const mapDispatchToProps = { moveInvocation } 50 | 51 | /* 52 | dnd 53 | */ 54 | const dropzoneTarget = { 55 | drop(props) { 56 | const { 57 | moveInvocation, 58 | sourceInvocationId, 59 | targetInvocationId, 60 | targetPosition, 61 | parentId, 62 | } = props 63 | moveInvocation({ 64 | sourceParentId: parentId, 65 | sourceInvocationId, 66 | targetInvocationId, 67 | targetPosition, 68 | }) 69 | }, 70 | } 71 | 72 | const collect = connect => ({ 73 | connectDropTarget: connect.dropTarget(), 74 | }) 75 | 76 | /* 77 | compose export 78 | */ 79 | export default compose( 80 | connect(null, mapDispatchToProps), 81 | DropTarget(COMPONENT_INVOCATION, dropzoneTarget, collect) 82 | )(ReorderDropzoneContainer) 83 | -------------------------------------------------------------------------------- /src/containers/ComponentInvocationContainer/containers/index.js: -------------------------------------------------------------------------------- 1 | export { default as AddInvocationFromFileDropzone } from './AddInvocationFromFileDropzone' 2 | export { default as IntermediaryDropzonesContainer } from './IntermediaryDropzonesContainer' 3 | export { default as ParamInvocationContainer } from '../../ParamInvocationContainer/index' 4 | export { default as ReorderDropzoneContainer } from './ReorderDropzoneContainer' 5 | export { default as PropDropzoneContainer } from './PropDropzoneContainer' 6 | export { default as OpenTagContainer } from './OpenTagContainer' 7 | export { default as CallParamDragContainer } from './CallParamDragContainer' 8 | -------------------------------------------------------------------------------- /src/containers/ComponentInvocationContainer/getCIDimensionsInjector.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const getCIDimensionsInjector = Component => 4 | class ClientHeightInjector extends React.PureComponent { 5 | constructor() { 6 | super() 7 | this.state = { 8 | componentSnapshot: {}, 9 | } 10 | this.componentInvocation = React.createRef() 11 | } 12 | componentDidMount() { 13 | const { clientHeight, clientWidth } = this.componentInvocation.current 14 | 15 | // https://reactjs.org/docs/react-component.html#componentdidmount 16 | // It can, however, be necessary for cases ... when you need to measure a DOM node 17 | // eslint-disable-next-line react/no-did-mount-set-state 18 | this.setState({ 19 | componentSnapshot: { 20 | clientHeight, 21 | clientWidth, 22 | }, 23 | }) 24 | } 25 | componentDidUpdate() { 26 | if (!this.componentInvocation.current) { 27 | return 28 | } 29 | // hacks gonna be hacks: somehow misses a dimensions update without this 30 | setTimeout(() => { 31 | if (!this.componentInvocation.current) { // since this is async (part of above hack) 32 | return 33 | } 34 | const { clientHeight, clientWidth } = this.componentInvocation.current 35 | const { componentSnapshot } = this.state 36 | if ( 37 | componentSnapshot.clientHeight === clientHeight && 38 | componentSnapshot.clientWidth === clientWidth 39 | ) { 40 | return 41 | } 42 | // eslint-disable-next-line react/no-did-update-set-state 43 | this.setState({ 44 | componentSnapshot: { 45 | clientHeight, 46 | clientWidth, 47 | }, 48 | }) 49 | }) 50 | } 51 | render() { 52 | return ( 53 | 58 | ) 59 | } 60 | } 61 | 62 | export default getCIDimensionsInjector 63 | -------------------------------------------------------------------------------- /src/containers/ComponentInvocationContainer/helpers.js: -------------------------------------------------------------------------------- 1 | import { COMPONENT_INVOCATION, PROP, FILE, DIR, PARAM_INVOCATION } from 'constantz' 2 | 3 | // exported target helpers 4 | export const getIsValidOver = monitor => 5 | monitor.isOver() && !(monitor.getItemType() === FILE && !monitor.getItem().declarationIds.length) 6 | 7 | export const acceptedDropTypes = [PROP, FILE, DIR, COMPONENT_INVOCATION, PARAM_INVOCATION] 8 | -------------------------------------------------------------------------------- /src/containers/ComponentInvocationContainer/index.js: -------------------------------------------------------------------------------- 1 | import T from 'prop-types' 2 | import C from 'check-types' 3 | import { forbidExtraProps } from 'airbnb-prop-types' 4 | import React from 'react' 5 | import { connect } from 'react-redux' 6 | import { DropTarget, DragSource } from 'react-dnd' 7 | 8 | import { invocationPropTypes } from 'model-prop-types' 9 | import { COMPONENT_INVOCATION } from 'constantz' 10 | import { compose } from 'utils' 11 | 12 | import { acceptedDropTypes, getIsValidOver } from './helpers' 13 | import { ComponentInvocation } from './components' 14 | import getCIDimensionsInjector from './getCIDimensionsInjector' 15 | import makeSelectInvocation from './makeSelectInvocation' 16 | 17 | class ComponentInvocationContainer extends React.PureComponent { 18 | render() { 19 | const { 20 | connectDragSource, 21 | componentInvocationRef, 22 | isDragging, 23 | parentIsInline, 24 | invocation: { invocationIds, inline, ...invocation }, 25 | invocationId, // selector / dnd only 26 | parentId, // dnd only 27 | ciDimensions, // dnd only 28 | ...props 29 | } = this.props 30 | 31 | const { isOverCIT1, isOverCIT2 } = props 32 | const isOverCI = isOverCIT1 || isOverCIT2 33 | const isClosed = !invocationIds.length && (!isOverCI || C.boolean(props.dragItem.payload)) 34 | const displayInline = (parentIsInline || inline) && !isOverCI 35 | 36 | // https://github.com/react-dnd/react-dnd/issues/998 37 | return !isDragging ? connectDragSource( 38 |
39 |
40 | 50 |
51 |
52 | ) : null 53 | } 54 | } 55 | 56 | ComponentInvocationContainer.propTypes = forbidExtraProps({ 57 | // passed by parent 58 | invocationId: T.number.isRequired, 59 | initial: T.bool, 60 | depth: T.number.isRequired, 61 | parentId: T.number, 62 | parentIsInline: T.bool, 63 | 64 | // connect 65 | invocation: invocationPropTypes.isRequired, 66 | 67 | // getCIDimensionsInjector 68 | componentInvocationRef: T.shape({ current: T.any }).isRequired, 69 | ciDimensions: T.shape({ clientWidth: T.number, clientHeight: T.number }).isRequired, 70 | 71 | // DragSource 72 | connectDragSource: T.func.isRequired, 73 | isDragging: T.bool, 74 | 75 | // DragTarget 1 76 | connectDropTarget: T.func.isRequired, 77 | isOverCIT1: T.bool.isRequired, 78 | dragItem: T.shape({ name: T.string }), 79 | 80 | // DragTarget 2 81 | connectClosingDropTarget: T.func.isRequired, 82 | isOverCIT2: T.bool.isRequired, 83 | 84 | }) 85 | 86 | ComponentInvocationContainer.defaultProps = { 87 | parentId: null, 88 | initial: false, 89 | dragItem: null, 90 | isDragging: false, 91 | parentIsInline: false, 92 | } 93 | 94 | 95 | /* 96 | connect 97 | */ 98 | const makeMapStateToProps = () => { 99 | const getInvocation = makeSelectInvocation() 100 | return (state, props) => ({ 101 | invocation: getInvocation(state, props), 102 | }) 103 | } 104 | 105 | const mapDispatchToProps = { } 106 | 107 | 108 | /* 109 | dnd - source 110 | */ 111 | const propSource = { 112 | beginDrag(props) { 113 | const { invocationId, ciDimensions, depth, parentId } = props 114 | return { 115 | type: COMPONENT_INVOCATION, 116 | sourceInvocationId: invocationId, 117 | sourceParentId: parentId, 118 | ciDimensions, 119 | depth, 120 | parentId, 121 | } 122 | }, 123 | canDrag(props) { 124 | return !props.initial 125 | }, 126 | } 127 | 128 | const sourceCollect = (connect, monitor) => ({ 129 | connectDragSource: connect.dragSource(), 130 | isDragging: monitor.isDragging(), 131 | }) 132 | 133 | 134 | /* 135 | dnd - target 136 | */ 137 | const dropzoneTarget = {} 138 | 139 | const targetCollect = (connect, monitor) => ({ 140 | connectDropTarget: connect.dropTarget(), 141 | isOverCIT1: getIsValidOver(monitor), 142 | dragItem: monitor.getItem(), 143 | }) 144 | 145 | const targetTwoCollect = (connect, monitor) => ({ 146 | connectClosingDropTarget: connect.dropTarget(), 147 | isOverCIT2: getIsValidOver(monitor), 148 | }) 149 | 150 | 151 | /* 152 | compose export 153 | */ 154 | export default compose( 155 | connect(makeMapStateToProps, mapDispatchToProps), 156 | getCIDimensionsInjector, 157 | DragSource(COMPONENT_INVOCATION, propSource, sourceCollect), 158 | DropTarget(acceptedDropTypes, dropzoneTarget, targetCollect), 159 | DropTarget(acceptedDropTypes, dropzoneTarget, targetTwoCollect) 160 | )(ComponentInvocationContainer) 161 | -------------------------------------------------------------------------------- /src/containers/ComponentInvocationContainer/makeSelectInvocation.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect' 2 | 3 | import { selectNames, makeSelectInvocation as msI, selectCallParams, selectDeclParams } from 'selectors' 4 | 5 | // component invocation selector => 6 | const makeSelectInvocation = () => createSelector( 7 | selectNames, 8 | selectCallParams, 9 | selectDeclParams, 10 | msI(), 11 | (names, callParams, declParams, invocation) => { 12 | const { 13 | invocationId, 14 | nameId, 15 | type, 16 | invocationIds, 17 | callParamIds, 18 | inline, 19 | pseudoSpreadPropsNameId, 20 | hasPropsSpread, 21 | } = invocation 22 | 23 | return { 24 | invocationId, 25 | nameId, 26 | type, 27 | invocationIds, 28 | callParams: callParamIds.map(id => { 29 | const { declParamId, nameId, ...callParam } = callParams[id] 30 | 31 | // A call param is either an 'invocation' of a declaration param 32 | if (declParamId) { 33 | const { nameId: declParamNameId, isSpreadMember } = declParams[declParamId] 34 | return { 35 | id, 36 | name: names[nameId || declParamNameId].value, 37 | nameId: nameId || declParamNameId, 38 | invokeNameId: declParamNameId, 39 | declParamId, 40 | declIsSpreadMember: isSpreadMember, 41 | } 42 | } 43 | 44 | // or it has a valueInvocationId and will have it's nameId set properly. 45 | return { 46 | id, 47 | nameId, 48 | name: names[nameId].value, 49 | ...callParam, 50 | } 51 | }), 52 | inline, 53 | pseudoSpreadPropsName: pseudoSpreadPropsNameId ? names[pseudoSpreadPropsNameId].value : null, 54 | hasPropsSpread, 55 | } 56 | }) 57 | 58 | export default makeSelectInvocation 59 | -------------------------------------------------------------------------------- /src/containers/ComponentTypeToggle.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | import { convertToClassCompmonent, convertToStatelessFunctionComponent } from 'duck' 4 | import { Keyword } from 'components' 5 | import { STATELESS_FUNCTION_COMPONENT, CLASS_COMPONENT } from 'constantz' 6 | 7 | const toggleMap = { 8 | [STATELESS_FUNCTION_COMPONENT]: 'convertToStatelessFunctionComponent', 9 | [CLASS_COMPONENT]: 'convertToClassCompmonent', 10 | } 11 | 12 | class ComponentTypeToggle extends React.PureComponent { 13 | onClick = () => { 14 | const { declarationId, targetType, ...props } = this.props 15 | props[toggleMap[targetType]]({ declarationId }) 16 | } 17 | 18 | render() { 19 | return ( 20 | 21 | {this.props.text} 22 | 23 | ) 24 | } 25 | } 26 | 27 | const mapDispatchToProps = { 28 | convertToClassCompmonent, 29 | convertToStatelessFunctionComponent, 30 | } 31 | 32 | export default connect(null, mapDispatchToProps)(ComponentTypeToggle) 33 | -------------------------------------------------------------------------------- /src/containers/DeclParams/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | import { createStructuredSelector } from 'reselect' 4 | 5 | import makeSelectDeclParams from './makeSelectDeclParams' 6 | 7 | class DeclParams extends React.PureComponent { 8 | render() { 9 | const { render, params } = this.props 10 | return render(params) 11 | } 12 | } 13 | 14 | const makeMapStateToProps = () => { 15 | const selectDeclParams = makeSelectDeclParams() 16 | return createStructuredSelector({ 17 | params: selectDeclParams, 18 | }) 19 | } 20 | 21 | export default connect( 22 | makeMapStateToProps, 23 | null 24 | )(DeclParams) 25 | -------------------------------------------------------------------------------- /src/containers/DeclParams/makeSelectDeclParams.js: -------------------------------------------------------------------------------- 1 | import { groupBy } from 'lodash' 2 | import { createSelector } from 'reselect' 3 | 4 | import { selectNames, selectDeclParams } from 'selectors' 5 | 6 | const makeSelectDeclParams = () => createSelector( 7 | selectNames, 8 | selectDeclParams, 9 | (state, props) => props.ids, 10 | (names, declParams, declParamIds = []) => 11 | Object.entries(groupBy( 12 | declParamIds.map(id => { 13 | const { nameId, ...rest } = declParams[id] 14 | return { 15 | nameId, 16 | name: names[nameId].value, 17 | ...rest, 18 | } 19 | }), 20 | 'name' 21 | )).map(([, group]) => ({ 22 | ...group[0], 23 | invokeCount: group.reduce((count, { invokeCount }) => count + invokeCount, 0), 24 | altIds: group.map(({ id }) => id), 25 | })) 26 | ) 27 | 28 | export default makeSelectDeclParams 29 | -------------------------------------------------------------------------------- /src/containers/DeclarationContainer.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | import { createStructuredSelector } from 'reselect' 4 | 5 | import { makeSelectDeclaration } from 'selectors' 6 | 7 | class DeclarationContainer extends React.PureComponent { 8 | render() { 9 | return ( 10 | 11 | {this.props.render(this.props.declaration)} 12 | 13 | ) 14 | } 15 | } 16 | 17 | const makeMapStateToProps = () => { 18 | const selectDeclaration = makeSelectDeclaration() 19 | return createStructuredSelector({ 20 | declaration: selectDeclaration, 21 | }) 22 | } 23 | 24 | export default connect(makeMapStateToProps)(DeclarationContainer) 25 | -------------------------------------------------------------------------------- /src/containers/DraggableDeclaration.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { DragSource } from 'react-dnd' 3 | import { compose } from 'utils' 4 | 5 | class DraggableDeclaration extends React.PureComponent { 6 | render() { 7 | const { connectDragSource, connectDragPreview } = this.props 8 | return ( 9 | 10 | {this.props.render(connectDragSource, connectDragPreview)} 11 | 12 | ) 13 | } 14 | } 15 | 16 | /* 17 | dnd - source 18 | */ 19 | const sourceSpec = { 20 | beginDrag(props) { 21 | const { declarationId } = props 22 | return { 23 | declarationId, 24 | } 25 | }, 26 | } 27 | 28 | const sourceCollect = (connect) => ({ 29 | connectDragSource: connect.dragSource(), 30 | connectDragPreview: connect.dragPreview(), 31 | }) 32 | 33 | 34 | /* 35 | compose export 36 | */ 37 | export default compose( 38 | DragSource(({ type }) => type, sourceSpec, sourceCollect), 39 | )(DraggableDeclaration) 40 | 41 | -------------------------------------------------------------------------------- /src/containers/EditorContainer/components/DefaultExport.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Semi, Keyword } from 'components' 3 | import { NameInput } from 'containers' 4 | 5 | const DefaultExport = ({ nameId }) => 6 | nameId ? ( 7 | 8 | export default 9 | {' '} 10 | 11 | 12 | 13 | ) : null 14 | 15 | export default DefaultExport 16 | -------------------------------------------------------------------------------- /src/containers/EditorContainer/components/Editor.js: -------------------------------------------------------------------------------- 1 | import T from 'prop-types' 2 | import { forbidExtraProps } from 'airbnb-prop-types' 3 | import React from 'react' 4 | import styled from 'styled-as-components' 5 | import theme from 'theme-proxy' 6 | import ReactTooltip from 'react-tooltip' 7 | 8 | import { 9 | StatelessFunctionComponent, 10 | ClassComponent, 11 | StyledComponent, 12 | ProjectIndexDeclaration, 13 | Standard, 14 | Json, 15 | ExportAppButton, 16 | } from 'components' 17 | import { 18 | STATELESS_FUNCTION_COMPONENT, 19 | STYLED_COMPONENT, 20 | CLASS_COMPONENT, 21 | PROJECT_INDEX, 22 | JSON_TYPE, 23 | } from 'constantz' 24 | 25 | import { Imports, DefaultExport } from './' 26 | 27 | /* 28 | Component 29 | */ 30 | class Editor extends React.PureComponent { 31 | static templates = { 32 | [STATELESS_FUNCTION_COMPONENT]: StatelessFunctionComponent, 33 | [STYLED_COMPONENT]: StyledComponent, 34 | [CLASS_COMPONENT]: ClassComponent, 35 | [PROJECT_INDEX]: ProjectIndexDeclaration, 36 | [JSON_TYPE]: Json, 37 | } 38 | 39 | componentDidUpdate() { 40 | if (!this.props.dragItem) { 41 | ReactTooltip.rebuild() 42 | } 43 | } 44 | 45 | render() { 46 | const { 47 | imports, 48 | declarations, 49 | defaultExport, 50 | workspaceActions: { exportToStackBlitz, downloadApp }, 51 | } = this.props 52 | return ( 53 | 54 | {/* Miscellaneous */} 55 | 61 | 62 |
{dataTip}
} 67 | /> 68 | 69 | {/* Text */} 70 | 71 | {declarations.map(declaration => { 72 | const { type, declarationId } = declaration 73 | const Renderer = Editor.templates[type] || Standard 74 | return ( 75 |
76 | 77 |
78 |
79 | ) 80 | })} 81 | 82 |
83 |
84 | ) 85 | } 86 | } 87 | 88 | /* 89 | propTypes 90 | */ 91 | Editor.propTypes = forbidExtraProps({ 92 | workspaceActions: T.objectOf(T.func), 93 | imports: T.arrayOf(T.object).isRequired, 94 | declarations: T.arrayOf(T.object).isRequired, 95 | defaultExport: T.number, 96 | currentFileId: T.number, 97 | dragItem: T.bool, 98 | }) 99 | 100 | Editor.defaultProps = { 101 | defaultExport: null, 102 | currentFileId: null, 103 | dragItem: false, 104 | workspaceActions: {}, 105 | } 106 | 107 | /* 108 | style + export 109 | */ 110 | export default styled(Editor).as.div.attrs({ 111 | id: 'editor', 112 | })` 113 | background-color: ${theme.colors.white}; 114 | padding: 50px 100px; 115 | color: ${theme.colors.darkblue}; 116 | min-width: 960px; 117 | ` 118 | -------------------------------------------------------------------------------- /src/containers/EditorContainer/components/Imports.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Semi, Keyword } from 'components' 3 | 4 | const Imports = ({ imports }) => 5 | imports.map(({ id, isNamed, importName, source }) => ( 6 | 7 | import {isNamed && '{ '} 8 | {importName} 9 | {isNamed && ' }'} from '{source}' 10 | 11 | 12 | )).concat(!!imports.length &&
) 13 | 14 | export default Imports 15 | -------------------------------------------------------------------------------- /src/containers/EditorContainer/components/index.js: -------------------------------------------------------------------------------- 1 | export { default as Editor } from './Editor' 2 | export { default as DefaultExport } from './DefaultExport' 3 | export { default as Imports } from './Imports' 4 | -------------------------------------------------------------------------------- /src/containers/EditorContainer/selectors/getCurrentFileDefaultExport.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect' 2 | import { DEFAULT } from 'constantz' 3 | 4 | import { getCurrentFileDeclarations } from './selectors' 5 | 6 | export const getCurrentFileDefaultExport = createSelector( 7 | getCurrentFileDeclarations, 8 | (declarations) => { 9 | const exprWithDefExport = declarations.find(({ exportType }) => exportType === DEFAULT) 10 | return exprWithDefExport && exprWithDefExport.nameId 11 | } 12 | ) 13 | -------------------------------------------------------------------------------- /src/containers/EditorContainer/selectors/index.js: -------------------------------------------------------------------------------- 1 | export * from './selectors' 2 | export * from './selectCurrentFileDeclarations' 3 | export * from './getCurrentFileImports' 4 | export * from './getCurrentFileDefaultExport' 5 | 6 | -------------------------------------------------------------------------------- /src/containers/EditorContainer/selectors/selectCurrentFileDeclarations.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect' 2 | 3 | import { declarationTypes } from 'constantz' 4 | 5 | import { 6 | selectInvocations, 7 | selectDeclParams, 8 | } from 'selectors' 9 | 10 | import { getCurrentFileDeclarations } from './selectors' 11 | 12 | const { LOOKTHROUGH } = declarationTypes 13 | 14 | export const selectCurrentFileDeclarations = createSelector( 15 | getCurrentFileDeclarations, 16 | selectDeclParams, 17 | selectInvocations, 18 | (declarations, params, invocations) => 19 | declarations 20 | .filter(({ type }) => type !== LOOKTHROUGH) 21 | .map(({ id, nameId, type, invocationIds, exportType, ...rest }) => ({ 22 | ...rest, 23 | declarationId: id, 24 | nameId, 25 | type, 26 | invocations: invocationIds.map(invocationId => invocations[invocationId]), 27 | exportType, 28 | })) 29 | ) 30 | 31 | -------------------------------------------------------------------------------- /src/containers/EditorContainer/selectors/selectors.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect' 2 | import { selectFiles, selectCurrentFileId, selectDeclarations } from 'selectors' 3 | 4 | export const selectCurrentFile = createSelector( 5 | selectFiles, 6 | selectCurrentFileId, 7 | (files, currentFileId) => currentFileId ? files[currentFileId] : null 8 | ) 9 | 10 | export const getCurrentFileDeclarations = createSelector( 11 | selectDeclarations, 12 | selectCurrentFile, 13 | (declarations, currentFile) => 14 | currentFile ? currentFile.declarationIds.map(id => declarations[id]) : [] 15 | ) 16 | -------------------------------------------------------------------------------- /src/containers/FileExplorerContainer/components/File.js: -------------------------------------------------------------------------------- 1 | import T from 'prop-types' 2 | import { forbidExtraProps, or, explicitNull } from 'airbnb-prop-types' 3 | import React from 'react' 4 | import styled from 'styled-as-components' 5 | 6 | import { COMPONENTS_FILE_ID, CONTAINERS_FILE_ID } from 'constantz' 7 | import { NameInput } from 'containers' 8 | import { filePropTypes } from 'model-prop-types' 9 | 10 | import { FileContainer, AddContainerButton, AddComponentButton } from '../containers' 11 | import FileIcon from './FileIcon' 12 | 13 | class File extends React.PureComponent { 14 | render() { 15 | const { 16 | file, 17 | file: { 18 | fileId, 19 | nameId, 20 | name, 21 | extension, 22 | fileChildren, 23 | isSelected, 24 | isEmptyDir, 25 | }, 26 | parentName, 27 | isDragging, 28 | connectDragPreview, 29 | connectDropTarget, 30 | path, 31 | } = this.props 32 | const isIndex = name.includes('index') 33 | const displayName = (!isIndex && isDragging && parentName) || name 34 | 35 | return connectDropTarget( 36 |
37 | 38 | 39 | {connectDragPreview( 40 |
41 | {!isIndex && parentName ? ( 42 | 47 | ) : ( 48 | displayName 49 | )} 50 |
, 51 | { captureDraggingState: true } 52 | )} 53 | {extension} 54 | {fileId === COMPONENTS_FILE_ID && } 55 | {fileId === CONTAINERS_FILE_ID && } 56 |
57 | {fileChildren.map(fileId => ( 58 | 64 | ))} 65 |
66 | ) 67 | } 68 | } 69 | 70 | File.propTypes = forbidExtraProps({ 71 | // passed by parent / file explorer 72 | // eslint-disable-next-line react/require-default-props 73 | parentName: or([T.string.isRequired, explicitNull()]), 74 | path: T.arrayOf(T.number).isRequired, 75 | 76 | file: T.shape(filePropTypes).isRequired, 77 | // Injected by React DnD: 78 | connectDragPreview: T.func.isRequired, 79 | connectDropTarget: T.func.isRequired, 80 | isDragging: T.bool.isRequired, 81 | 82 | // for wrapper 83 | innerRef: T.func.isRequired, 84 | onClick: T.func.isRequired, 85 | }) 86 | 87 | const NoWrap = styled.div` 88 | white-space: nowrap; 89 | ` 90 | 91 | export default styled(File).as.div` 92 | ${props => props.parentName && 'cursor: pointer;'} 93 | ${props => props.parentName && 'margin-left: 15px;'} 94 | ${props => props.file.isDirectory && 'padding: 5px 0;'} 95 | ` 96 | -------------------------------------------------------------------------------- /src/containers/FileExplorerContainer/components/FileExplorer.js: -------------------------------------------------------------------------------- 1 | import T from 'prop-types' 2 | import { forbidExtraProps } from 'airbnb-prop-types' 3 | import React from 'react' 4 | import styled from 'styled-as-components' 5 | import theme from 'theme-proxy' 6 | 7 | import { ExportAppButton, HumanEditUndoIcon, HumanEditRedoIcon, StarOnGithub, IssueOnGithub } from 'components' 8 | import { FileContainer } from '../containers' 9 | 10 | const FileExplorer = ({ rootFiles, resetProject, undo, redo }) => ( 11 | 12 | 19 | } /> 20 | } /> 21 | 22 | 23 | {rootFiles.map(fileId => ( 24 | 28 | ))} 29 | 30 | 31 | ) 32 | 33 | 34 | /* 35 | propTypes 36 | */ 37 | FileExplorer.propTypes = forbidExtraProps({ 38 | // container 39 | rootFiles: T.arrayOf(T.number).isRequired, 40 | resetProject: T.func.isRequired, 41 | undo: T.func.isRequired, 42 | redo: T.func.isRequired, 43 | }) 44 | 45 | 46 | export default styled(FileExplorer).as.div` 47 | user-select: none; 48 | position: relative; 49 | padding: 50px 40px 60px 40px; 50 | background-color: ${theme.colors.washedpink}; 51 | color: ${theme.colors.darkblue}; // #545ab7 52 | height: 100%; 53 | overflow-y: auto; 54 | ` 55 | -------------------------------------------------------------------------------- /src/containers/FileExplorerContainer/components/FileIcon.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-as-components' 3 | 4 | import { JS, SC, JSON_TYPE } from 'constantz' 5 | import { ReactIcon, FolderIcon, JSONIcon, SCIcon } from 'components' 6 | 7 | const FileIcon = ({ file: { name, isCurrent, type, isDirectory, containsCurrent } }) => ( 8 | 9 | {isCurrent && } 10 | {type === JS && } 11 | {type === SC && } 12 | {type === JSON_TYPE && } 13 | {isDirectory && ( 14 | 15 | )} 16 | 17 | ) 18 | 19 | const Pointer = styled.span.attrs({ 20 | children: '👉', 21 | })` 22 | margin-left: -25px; 23 | margin-right: 2px; 24 | position: relative; 25 | bottom: -2px; 26 | font-size: 20px; 27 | display: inline-block; 28 | margin-top: -4px; 29 | margin-bottom: -4px; 30 | ` 31 | 32 | export default styled(FileIcon).as.span`` 33 | -------------------------------------------------------------------------------- /src/containers/FileExplorerContainer/components/index.js: -------------------------------------------------------------------------------- 1 | export { default as File } from './File' 2 | export { default as FileExplorer } from './FileExplorer' 3 | -------------------------------------------------------------------------------- /src/containers/FileExplorerContainer/containers/AddComponentButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { AddButton } from 'components' 3 | import { connect } from 'react-redux' 4 | import { newComponentBundlePlease } from 'duck' 5 | import { pDsP } from 'utils' 6 | 7 | const AddComponentButton = ({ newComponentBundlePlease, ...props }) => ( 8 | 14 | ) 15 | 16 | export default connect( 17 | null, 18 | { newComponentBundlePlease } 19 | )(AddComponentButton) 20 | -------------------------------------------------------------------------------- /src/containers/FileExplorerContainer/containers/AddContainerButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { AddButton } from 'components' 3 | import { connect } from 'react-redux' 4 | import { openAPIInputScreen } from 'duck' 5 | import { pDsP } from 'utils' 6 | 7 | const AddContainerButton = ({ openAPIInputScreen, ...props }) => ( 8 | 9 | ) 10 | 11 | export default connect(null, { openAPIInputScreen })(AddContainerButton) 12 | -------------------------------------------------------------------------------- /src/containers/FileExplorerContainer/containers/index.js: -------------------------------------------------------------------------------- 1 | export { default as FileContainer } from './FileContainer' 2 | export { default as AddComponentButton } from './AddComponentButton' 3 | export { default as AddContainerButton } from './AddContainerButton' 4 | -------------------------------------------------------------------------------- /src/containers/FileExplorerContainer/index.js: -------------------------------------------------------------------------------- 1 | import T from 'prop-types' 2 | import { forbidExtraProps } from 'airbnb-prop-types' 3 | import React from 'react' 4 | import { connect } from 'react-redux' 5 | import { createStructuredSelector } from 'reselect' 6 | import { DropTarget } from 'react-dnd' 7 | import { ActionCreators as UndoableActionCreators } from 'redux-undo' 8 | 9 | import { selectRootFiles } from 'selectors' 10 | import { DIR, FILE } from 'constantz' 11 | import { compose } from 'utils' 12 | import { moveFile, resetProject } from 'duck' 13 | 14 | import { FileExplorer } from './components' 15 | 16 | const FileExplorerContainer = ({ 17 | connectDropTarget, 18 | moveFile, // dnd only 19 | ...props 20 | }) => ( 21 |
22 | {connectDropTarget(
)} 23 | 24 |
25 | ) 26 | 27 | /* 28 | propTypes 29 | */ 30 | FileExplorerContainer.propTypes = forbidExtraProps({ 31 | // connect 32 | rootFiles: T.arrayOf(T.number).isRequired, 33 | resetProject: T.func.isRequired, 34 | moveFile: T.func.isRequired, 35 | undo: T.func.isRequired, 36 | redo: T.func.isRequired, 37 | // dnd 38 | connectDropTarget: T.func.isRequired, 39 | }) 40 | 41 | 42 | const mapStateToProps = createStructuredSelector({ 43 | rootFiles: selectRootFiles, 44 | }) 45 | 46 | const mapDispatchToProps = { 47 | moveFile, 48 | resetProject, 49 | undo: UndoableActionCreators.undo, 50 | redo: UndoableActionCreators.redo, 51 | } 52 | 53 | // target 54 | const dropzoneTarget = { 55 | drop(props, monitor) { 56 | const { fileId, moveFile } = props 57 | const { fileId: sourceFileId } = monitor.getItem() 58 | moveFile({ targetDirectoryId: fileId, sourceFileId, toRoot: true }) 59 | }, 60 | } 61 | 62 | const targetCollect = (connect) => ({ 63 | connectDropTarget: connect.dropTarget(), 64 | }) 65 | 66 | /* 67 | compose export 68 | */ 69 | export default compose( 70 | connect(mapStateToProps, mapDispatchToProps), 71 | DropTarget([FILE, DIR], dropzoneTarget, targetCollect) 72 | )(FileExplorerContainer) 73 | -------------------------------------------------------------------------------- /src/containers/InvocationContainer.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | import { createStructuredSelector } from 'reselect' 4 | 5 | import { makeSelectInvocation } from 'selectors' 6 | 7 | class InvocationContainer extends React.PureComponent { 8 | render() { 9 | return this.props.render(this.props.invocation) 10 | } 11 | } 12 | 13 | const makeMapStateToProps = () => { 14 | const selectInvocation = makeSelectInvocation() 15 | return createStructuredSelector({ 16 | invocation: selectInvocation, 17 | }) 18 | } 19 | 20 | export default connect( 21 | makeMapStateToProps, 22 | null 23 | )(InvocationContainer) 24 | -------------------------------------------------------------------------------- /src/containers/KeyPressListeners/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | import { ActionCreators as UndoActionCreators } from 'redux-undo' 4 | 5 | class KeyPressListeners extends React.PureComponent { 6 | keydown = event => { 7 | if (event.keyCode === 27) { 8 | document.activeElement.blur() 9 | } 10 | if (event.keyCode === 90 && (event.ctrlKey || event.metaKey)) { 11 | if (event.shiftKey) { 12 | this.props.redo() 13 | } else { 14 | this.props.undo() 15 | } 16 | event.preventDefault() 17 | } 18 | if (event.keyCode === 89 && (event.ctrlKey || event.metaKey)) { 19 | this.props.redo() 20 | event.preventDefault() 21 | } 22 | } 23 | componentDidMount() { 24 | document.addEventListener('keydown', this.keydown, false) 25 | } 26 | componentWillUnmount() { 27 | document.removeEventListener('keydown', this.keydown, false) 28 | } 29 | render() { 30 | return null 31 | } 32 | } 33 | 34 | const { undo, redo } = UndoActionCreators 35 | 36 | const mapDispatchToProps = { 37 | undo, 38 | redo, 39 | } 40 | 41 | /* 42 | compose export 43 | */ 44 | export default connect( 45 | null, 46 | mapDispatchToProps 47 | )(KeyPressListeners) 48 | -------------------------------------------------------------------------------- /src/containers/Name.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | import { createStructuredSelector } from 'reselect' 4 | import { changeName } from 'duck' 5 | 6 | import { makeSelectName } from 'selectors' 7 | 8 | class Name extends React.PureComponent { 9 | render() { 10 | const { render, name: { value } = {}, changeName } = this.props 11 | return render ? render(value, changeName) : value || null 12 | } 13 | } 14 | 15 | const mapDispatchToProps = { changeName } 16 | 17 | const makeMapStateToProps = () => { 18 | const selectName = makeSelectName() 19 | return createStructuredSelector({ 20 | name: selectName, 21 | }) 22 | } 23 | 24 | export default connect( 25 | makeMapStateToProps, 26 | mapDispatchToProps 27 | )(Name) 28 | -------------------------------------------------------------------------------- /src/containers/NameInput.js: -------------------------------------------------------------------------------- 1 | import T from 'prop-types' 2 | import React from 'react' 3 | import { Input } from 'components' 4 | import Name from './Name' 5 | 6 | const NameInput = props => ( 7 | } 10 | /> 11 | ) 12 | 13 | NameInput.propTypes = { 14 | nameId: T.number.isRequired, 15 | } 16 | 17 | export default NameInput 18 | -------------------------------------------------------------------------------- /src/containers/NotFoundPage/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { FormattedMessage } from 'react-intl' 3 | import styled from 'styled-as-components' 4 | 5 | import messages from './messages' 6 | 7 | const NotFound = () => 8 | 9 | export default styled(NotFound).as.div` 10 | 11 | ` 12 | -------------------------------------------------------------------------------- /src/containers/NotFoundPage/messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | export default defineMessages({ 4 | header: { 5 | id: 'app.containers.NotFoundPage.header', 6 | defaultMessage: 'Page not found.', 7 | }, 8 | }) 9 | -------------------------------------------------------------------------------- /src/containers/ParamInvocationContainer/index.js: -------------------------------------------------------------------------------- 1 | import T from 'prop-types' 2 | import { forbidExtraProps } from 'airbnb-prop-types' 3 | import React from 'react' 4 | import { connect } from 'react-redux' 5 | import { DragSource } from 'react-dnd' 6 | 7 | import { paramInvocationPropTypes } from 'model-prop-types' 8 | import { PARAM_INVOCATION } from 'constantz' 9 | import { compose } from 'utils' 10 | 11 | import { ParamInvocation } from 'components' 12 | import makeSelectParamInvocation from './makeSelectParamInvocation' 13 | 14 | const ParamInvocationContainer = ({ 15 | invocationId, // connect only 16 | invocation, 17 | parentIsInline, 18 | ...props 19 | }) => ( 20 | 27 | ) 28 | 29 | /* 30 | propTypes 31 | */ 32 | ParamInvocationContainer.propTypes = forbidExtraProps({ 33 | // passed by parent 34 | invocationId: T.number.isRequired, 35 | parentId: T.number.isRequired, 36 | parentIsInline: T.bool.isRequired, 37 | depth: T.number.isRequired, 38 | 39 | // injected by makeSelectParamInvocation 40 | invocation: paramInvocationPropTypes.isRequired, 41 | 42 | // injected by DragSource 43 | isPIDragging: T.bool.isRequired, 44 | connectDragSource: T.func.isRequired, 45 | }) 46 | 47 | 48 | /* 49 | connect 50 | */ 51 | const makeMapStateToProps = () => { 52 | const getInvocation = makeSelectParamInvocation() 53 | return (state, props) => ({ 54 | invocation: getInvocation(state, props), 55 | }) 56 | } 57 | 58 | /* 59 | dnd 60 | */ 61 | const propSource = { 62 | beginDrag(props) { 63 | const { 64 | invocation: { callParamId, nameId, invocationId }, 65 | parentId, 66 | } = props 67 | return { 68 | // hover preview 69 | nameId, 70 | type: PARAM_INVOCATION, 71 | // drop 72 | sourceParentId: parentId, 73 | sourceInvocationId: invocationId, 74 | // canDrop 75 | callParamId, 76 | } 77 | }, 78 | } 79 | 80 | const collect = (connect, monitor) => ({ 81 | connectDragSource: connect.dragSource(), 82 | isPIDragging: monitor.isDragging(), 83 | }) 84 | 85 | /* 86 | compose export 87 | */ 88 | export default compose( 89 | connect(makeMapStateToProps, {}), 90 | DragSource(PARAM_INVOCATION, propSource, collect) 91 | )(ParamInvocationContainer) 92 | -------------------------------------------------------------------------------- /src/containers/ParamInvocationContainer/makeSelectParamInvocation.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect' 2 | 3 | import { makeSelectInvocation, selectCallParams, selectDeclParams } from 'selectors' 4 | 5 | // param invocation selector => {param} 6 | const makeSelectParamInvocation = () => createSelector( 7 | selectCallParams, 8 | selectDeclParams, 9 | makeSelectInvocation(), 10 | (callParams, declParams, invocation) => { 11 | const { invocationId, nameId, callParamIds, invocationIds } = invocation 12 | // callParamIds is a singleton for paramInvocations so e.g. allParams[[1]] is fine 🕊 13 | const { id, declParamId } = callParams[callParamIds] 14 | 15 | const { isSpreadMember } = declParams[declParamId] 16 | 17 | return { 18 | invocationId, 19 | callParamId: id, 20 | nameId, 21 | declIsSpreadMember: isSpreadMember, 22 | invocationIds, 23 | } 24 | }) 25 | 26 | 27 | export default makeSelectParamInvocation 28 | -------------------------------------------------------------------------------- /src/containers/PropTypesContainer.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { PropTypes } from 'components' 3 | import DeclParams from './DeclParams' 4 | 5 | const PropTypesContainer = ({ declParamIds, ...props }) => ( 6 | } 9 | /> 10 | ) 11 | 12 | export default PropTypesContainer 13 | 14 | -------------------------------------------------------------------------------- /src/containers/PropsContainer.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Props } from 'components' 3 | import DeclParams from './DeclParams' 4 | 5 | const PropsContainer = ({ declParamIds, ...props }) => ( 6 | } 9 | /> 10 | ) 11 | 12 | export default PropsContainer 13 | 14 | -------------------------------------------------------------------------------- /src/containers/SelectedThemeProvider/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { connect } from 'react-redux' 3 | import { ThemeProvider } from 'styled-components' 4 | 5 | import defaultTheme from './themes' 6 | 7 | class SelectedThemeProvider extends Component { 8 | shouldComponentUpdate() { 9 | return false // only default theme for now 10 | } 11 | 12 | render() { 13 | return ( 14 | 15 | {this.props.children} 16 | 17 | ) 18 | } 19 | } 20 | 21 | const mapStateToProps = () => ({ theme: defaultTheme }) 22 | 23 | export default connect(mapStateToProps)(SelectedThemeProvider) 24 | -------------------------------------------------------------------------------- /src/containers/SelectedThemeProvider/themes/default.js: -------------------------------------------------------------------------------- 1 | export default { 2 | colors: { 3 | white: '#fff', 4 | darkgreen: '#377100', 5 | washedDarkGreen: '#30630012', 6 | darkblue: '#010431', 7 | washedpink: '#ffc3f90a', 8 | orange: '#925e24', 9 | error: '#8e0726', 10 | }, 11 | lineHeightInPx: '24px', 12 | } 13 | -------------------------------------------------------------------------------- /src/containers/SelectedThemeProvider/themes/index.js: -------------------------------------------------------------------------------- 1 | import defaultTheme from './default' 2 | 3 | export default defaultTheme 4 | -------------------------------------------------------------------------------- /src/containers/StopDropTarget/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { DropTarget } from 'react-dnd' 3 | 4 | class StopDropTarget extends React.Component { 5 | render() { 6 | const { connectDropTarget, children } = this.props 7 | 8 | return connectDropTarget( 9 | 10 | {children} 11 | 12 | ) 13 | } 14 | } 15 | 16 | // target 17 | const dropzoneTarget = { 18 | drop() { 19 | // sets monitor.didDrop() to true 20 | }, 21 | } 22 | 23 | const targetCollect = connect => ({ 24 | connectDropTarget: connect.dropTarget(), 25 | }) 26 | 27 | export default DropTarget(props => props.type, dropzoneTarget, targetCollect)(StopDropTarget) 28 | -------------------------------------------------------------------------------- /src/containers/ToTextContainer/ToText.js: -------------------------------------------------------------------------------- 1 | import { set } from 'lodash' 2 | import ReactDOM from 'react-dom' 3 | import React from 'react' 4 | import { StyleSheetManager } from 'styled-components' 5 | 6 | import { DIR, SC } from 'constantz' 7 | import { lastItem } from 'utils' 8 | import { EditorContainer } from 'containers' 9 | 10 | import copyNodeText from './copyNodeText' 11 | import postProcessFileTree from './postProcessFileTree' 12 | 13 | /** 14 | * ToText 15 | * 16 | * Exporting is achieved by rendering the project files to a hidden dom node using the exact same 17 | * EditorContainer component tree the real editor uses. The HTML text selection api is then used 18 | * to copy the text in that hidden node. ToText uses the component lifecycle itself + setState 19 | * to loop through the files, until all files have been copied. ToText is then unmounted. 20 | */ 21 | export default class ToText extends React.Component { 22 | static target = document.getElementById('frame').contentDocument 23 | 24 | constructor(props) { 25 | super(props) 26 | const { files, rootFiles } = props 27 | 28 | this.fileTree = {} 29 | this.hiddenNodeRef = React.createRef() 30 | this.currentTraversal = [ 31 | { 32 | id: 'root', 33 | children: rootFiles, 34 | currentIndex: 0, 35 | max: rootFiles.length - 1, 36 | }, 37 | ] 38 | 39 | let file = files[rootFiles[0]] 40 | 41 | while (file.type === DIR) { 42 | this.currentTraversal.push({ 43 | id: file.id, 44 | children: file.children, 45 | currentIndex: 0, 46 | max: file.children.length - 1, 47 | }) 48 | file = files[file.children[0]] 49 | } 50 | 51 | // initialState 52 | this.state = { currentFileId: file.id } 53 | } 54 | 55 | copyAndNextFile() { 56 | const { files, names, semis } = this.props 57 | const { currentTraversal, fileTree } = this // Note: these are mutation managed state 58 | 59 | // add currently rendered file's text to output tree 60 | const namePath = currentTraversal.reduce((out, node) => { 61 | const file = files[node.children[node.currentIndex]] 62 | const baseName = names[file.nameId].value 63 | const ext = (file.type === SC && '|js') || (file.type === DIR ? '' : `|${file.type}`) 64 | 65 | return `${out ? `${out}.` : ''}${baseName}${ext}` 66 | }, null) 67 | 68 | set(fileTree, namePath, copyNodeText(this.hiddenNodeRef.current)) 69 | 70 | let node 71 | // walk project's file tree - start by going up if needed 72 | do { 73 | node = lastItem(currentTraversal) 74 | 75 | while (node.id !== 'root' && node.currentIndex === node.max) { 76 | currentTraversal.pop() 77 | node = lastItem(currentTraversal) 78 | } 79 | 80 | if (node.id === 'root' && node.currentIndex === node.max) { 81 | this.props.onFinish(postProcessFileTree(fileTree, semis)) 82 | return 83 | } 84 | 85 | // next item 86 | node.currentIndex += 1 87 | } while ( 88 | files[node.children[node.currentIndex]].type === DIR && 89 | !files[node.children[node.currentIndex]].children.length 90 | ) 91 | 92 | // if directory and no children, call the go up again code 93 | 94 | // go down if needed 95 | let file = files[node.children[node.currentIndex]] 96 | 97 | while (file.type === DIR) { 98 | currentTraversal.push({ 99 | id: file.id, 100 | children: file.children, 101 | currentIndex: 0, 102 | max: file.children.length - 1, 103 | }) 104 | file = files[file.children[0]] 105 | } 106 | 107 | // render new current file 108 | node = lastItem(currentTraversal) 109 | 110 | const nextCurrentFileId = node.children[node.currentIndex] 111 | 112 | this.setState({ 113 | currentFileId: nextCurrentFileId, 114 | }) 115 | } 116 | 117 | componentDidMount() { 118 | this.copyAndNextFile() 119 | } 120 | 121 | componentDidUpdate() { 122 | this.copyAndNextFile() 123 | } 124 | 125 | render() { 126 | const { currentFileId } = this.state 127 | 128 | return ( 129 | 130 |
131 | {ReactDOM.createPortal( 132 | , 133 | this.constructor.target.body, 134 | )} 135 |
136 |
137 | ) 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/containers/ToTextContainer/copyNodeText.js: -------------------------------------------------------------------------------- 1 | export default function copyNodeText() { 2 | const d = document.getElementById('frame').contentDocument 3 | const e = d.getElementById('editor') 4 | if (d.getSelection) { 5 | const s = d.getSelection() 6 | const r = d.createRange() 7 | r.selectNode(e) 8 | s.removeAllRanges() 9 | s.addRange(r) 10 | } else if (d.selection) { 11 | const r = d.body.createTextRange() 12 | r.moveToElementText(e) 13 | r.select() 14 | } 15 | 16 | let text = '' 17 | if (d.getSelection) { 18 | text = d.getSelection().toString() 19 | } else if (d.selection && d.selection.type !== 'Control') { 20 | text = d.selection.createRange().text // eslint-disable-line prefer-destructuring 21 | } 22 | return text 23 | } 24 | 25 | // /* eslint-disable */ 26 | // export default function copyNodeText(node) { 27 | // ;(function selectText(e, r, s, d) { 28 | // d = document 29 | // if ((s = window.getSelection)) { 30 | // ;(r = d.createRange()).selectNode(e) 31 | // ;(s = s()).removeAllRanges() 32 | // s.addRange(r) 33 | // } else if (d.selection) { 34 | // ;(r = d.body.createTextRange()).moveToElementText(e) 35 | // r.select() 36 | // } 37 | // })(node) 38 | 39 | // return (function getSelectionText() { 40 | // var text = '' 41 | // if (window.getSelection) { 42 | // text = window.getSelection().toString() 43 | // } else if (document.selection && document.selection.type !== 'Control') { 44 | // text = document.selection.createRange().text 45 | // } 46 | // return text 47 | // })() 48 | 49 | // // ;(function clearSelection() { 50 | // // if (window.getSelection) { 51 | // // window.getSelection().removeAllRanges() 52 | // // } else if (document.selection) { 53 | // // document.selection.empty() 54 | // // } 55 | // // })() 56 | // } 57 | -------------------------------------------------------------------------------- /src/containers/ToTextContainer/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | 4 | import { selectFiles, selectRootFiles, selectNames, selectSemis } from 'selectors' 5 | import { createStructuredSelector } from 'reselect' 6 | 7 | import ToText from './ToText' 8 | 9 | const ToTextContainer = props => 10 | 11 | const mapStateToProps = createStructuredSelector({ 12 | files: selectFiles, 13 | rootFiles: selectRootFiles, 14 | names: selectNames, 15 | semis: selectSemis, 16 | }) 17 | 18 | export default connect( 19 | mapStateToProps, 20 | null 21 | )(ToTextContainer) 22 | -------------------------------------------------------------------------------- /src/containers/ToTextContainer/indexHtml.js: -------------------------------------------------------------------------------- 1 | const indexHtml = 2 | ` 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | ` 42 | export default indexHtml 43 | -------------------------------------------------------------------------------- /src/containers/ToTextContainer/postProcessFileTree.js: -------------------------------------------------------------------------------- 1 | import { RESOLVE_ALIASES } from 'constantz' 2 | 3 | import indexHtml from './indexHtml' 4 | 5 | /** 6 | * postProcessFileTree: 7 | * - adds index.html 8 | * - adds index.js files for top level components / containers folders 9 | * - adds package.json 10 | */ 11 | /* eslint-disable no-param-reassign */ 12 | export default function postProcessFileTree(fileTree, semis) { 13 | // index.html 14 | fileTree['index.html'] = indexHtml 15 | 16 | // indexes 17 | Object.entries(fileTree).forEach(([name, value]) => { 18 | if (RESOLVE_ALIASES.includes(name)) { 19 | fileTree[name]['index|js'] = Object.keys(value).sort().reduce((out, key) => { 20 | const name = key.split('|')[0] 21 | const exportLine = `export { default as ${name} } from './${name}'${semis ? ';' : ''}` 22 | return out ? `${out}\n${exportLine}` : exportLine 23 | }, '') 24 | } 25 | }) 26 | return fileTree 27 | } 28 | -------------------------------------------------------------------------------- /src/containers/WorkspaceContainer/download.js: -------------------------------------------------------------------------------- 1 | import JSZip from 'jszip' 2 | import { saveAs } from 'file-saver' 3 | 4 | const recurse = (zip, tree) => { 5 | Object.entries(tree).forEach(([key, value]) => { 6 | if (typeof value === 'string') { 7 | zip.file(key.replace('|', '.'), value) 8 | } else { 9 | const dir = zip.folder(key) 10 | recurse(dir, value) 11 | } 12 | }) 13 | } 14 | 15 | export default function download(fileTree) { 16 | const zip = new JSZip() 17 | 18 | recurse(zip, fileTree) 19 | 20 | zip.generateAsync({ type: 'blob' }).then((content) => { 21 | saveAs(content, 'app.zip') 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /src/containers/WorkspaceContainer/embedStackBlitz.js: -------------------------------------------------------------------------------- 1 | import stackblitz from '@stackblitz/sdk' 2 | 3 | import getStackBlitzProjectDef from './getStackBlitzProjectDef' 4 | 5 | export default function embedStackBlitz(fileTree) { 6 | const def = getStackBlitzProjectDef(fileTree) 7 | def.files['note.md'] = '## Note\nChanges in Aperitif will overwrite stackblitz editor changes' 8 | return stackblitz.embedProject( 9 | document.getElementById('stackBlitzEmbed'), 10 | def, 11 | { 12 | openFile: 'note.md', 13 | view: 'preview', 14 | height: '100%', 15 | width: '100%', 16 | hideExplorer: false, 17 | hideNavigation: true, 18 | forceEmbedLayout: true, // Disables the full stackblitz UI on larger screen sizes 19 | } 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /src/containers/WorkspaceContainer/embedUpdate.js: -------------------------------------------------------------------------------- 1 | import getStackBlitzProjectDef from './getStackBlitzProjectDef' 2 | 3 | let previousFileTree = null 4 | 5 | export default function embedStackBlitz(fileTree, { vm }) { 6 | const { files } = getStackBlitzProjectDef(fileTree) 7 | files['note.md'] = '## Note\nChanges in Aperitif will overwrite stackblitz editor changes' 8 | 9 | vm.applyFsDiff({ 10 | create: files, 11 | destroy: Object.keys(previousFileTree || {}), 12 | }).then(() => { 13 | vm.editor.openFile('note.md') 14 | }) 15 | 16 | previousFileTree = files 17 | } 18 | -------------------------------------------------------------------------------- /src/containers/WorkspaceContainer/getStackBlitzProjectDef.js: -------------------------------------------------------------------------------- 1 | import flatten from 'flat' 2 | 3 | export default function getStackBlitzProjectDef(fileTree) { 4 | const files = Object.entries(flatten(fileTree, { delimiter: '/' })) 5 | .reduce((out, [name, value]) => { 6 | out[name.replace('|', '.')] = value // eslint-disable-line no-param-reassign 7 | return out 8 | }, {}) 9 | 10 | return { 11 | files, 12 | title: 'string', 13 | description: 'string', 14 | template: 'create-react-app', 15 | tags: [], 16 | dependencies: { 17 | react: 'latest', 18 | 'react-dom': 'latest', 19 | 'styled-components': 'latest', 20 | 'prop-types': 'latest', 21 | }, 22 | settings: { 23 | clearConsole: false, 24 | }, 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/containers/WorkspaceContainer/makeReduxStackblitzUpdateReconciler.js: -------------------------------------------------------------------------------- 1 | import { debounce } from 'lodash' 2 | import { CHANGE_FILE, OPEN_API_INPUT_SCREEN, UPDATE_NAME, UPDATE_DECLARATION } from 'duck' 3 | 4 | export default function makeReduxStackblitzUpdateReconciler(doUpdate) { 5 | const debounceSlow = debounce(doUpdate, 500) 6 | 7 | return ({ action }) => setTimeout(() => { 8 | switch (action.type) { 9 | // do nothing 10 | case OPEN_API_INPUT_SCREEN: 11 | case CHANGE_FILE: { 12 | return 13 | } 14 | 15 | // debounce 16 | case UPDATE_DECLARATION: // StyledComponent TextArea 17 | case UPDATE_NAME: { 18 | debounceSlow() 19 | return 20 | } 21 | 22 | default: 23 | doUpdate() 24 | } 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /src/containers/WorkspaceContainer/toStackBlitz.js: -------------------------------------------------------------------------------- 1 | import stackblitz from '@stackblitz/sdk' 2 | 3 | import getStackBlitzProjectDef from './getStackBlitzProjectDef' 4 | 5 | export default function toStackBlitz(fileTree) { 6 | stackblitz.openProject(getStackBlitzProjectDef(fileTree), { 7 | openFile: '', // Show a specific file on page load 8 | newWindow: true, // Open in new window or in current window 9 | hideDevTools: true, // Hide the debugging console 10 | devToolsHeight: 0, // Set the height of the debugging console 11 | }) 12 | } 13 | -------------------------------------------------------------------------------- /src/containers/index.js: -------------------------------------------------------------------------------- 1 | export { default as AperitifPostContainer } from './AperitifPostContainer' 2 | export { default as App } from './App' 3 | export { default as ComponentTypeToggle } from './ComponentTypeToggle' 4 | export { default as DeclarationContainer } from './DeclarationContainer' 5 | export { default as ComponentInvocationContainer } from './ComponentInvocationContainer' 6 | export { default as EditorContainer } from './EditorContainer' 7 | export { default as FileExplorerContainer } from './FileExplorerContainer' 8 | export { default as Name } from './Name' 9 | export { default as NameInput } from './NameInput' 10 | export { default as NotFoundPage } from './NotFoundPage' 11 | export { default as SelectedThemeProvider } from './SelectedThemeProvider' 12 | export { default as WorkspaceContainer } from './WorkspaceContainer' 13 | export { default as InvocationContainer } from './InvocationContainer' 14 | export { default as DraggableDeclaration } from './DraggableDeclaration' 15 | export { default as ParamInvocationContainer } from './ParamInvocationContainer' 16 | export { default as KeyPressListeners } from './KeyPressListeners' 17 | export { default as StopDropTarget } from './StopDropTarget' 18 | export { default as PropTypesContainer } from './PropTypesContainer' 19 | export { default as PropsContainer } from './PropsContainer' 20 | export { default as ToTextContainer } from './ToTextContainer' 21 | -------------------------------------------------------------------------------- /src/duck/editor.js: -------------------------------------------------------------------------------- 1 | import { createAction } from 'redux-actions' 2 | 3 | import { DIR } from 'constantz' 4 | 5 | export const CHANGE_FILE = 'CHANGE_FILE' 6 | export const OPEN_API_INPUT_SCREEN = 'OPEN_API_INPUT_SCREEN' 7 | 8 | export default (state, action) => ({ 9 | ...state, 10 | editor: editorReducer(state.editor, action, state), 11 | }) 12 | 13 | function editorReducer(state, action, appState) { 14 | switch (action.type) { 15 | case CHANGE_FILE: { 16 | const { currentFileId } = state 17 | const { files, names } = appState 18 | let { payload: nextId } = action 19 | const { type, children } = files[nextId] 20 | 21 | if (type === DIR) { 22 | nextId = children.find(fileId => names[files[fileId].nameId].value.includes('index')) 23 | } 24 | 25 | return { 26 | ...state, 27 | currentFileId: nextId || currentFileId, 28 | selectedFileId: action.payload, 29 | } 30 | } 31 | 32 | case OPEN_API_INPUT_SCREEN: { 33 | return { 34 | ...state, 35 | currentFileId: null, 36 | selectedFileId: null, 37 | } 38 | } 39 | 40 | default: { 41 | return state 42 | } 43 | } 44 | } 45 | 46 | export const changeFile = createAction( 47 | CHANGE_FILE 48 | ) 49 | 50 | export const openAPIInputScreen = createAction( 51 | OPEN_API_INPUT_SCREEN 52 | ) 53 | -------------------------------------------------------------------------------- /src/duck/index.js: -------------------------------------------------------------------------------- 1 | import undoable, { groupByActionTypes, excludeAction } from 'redux-undo' 2 | 3 | import coreReducer, { UPDATE_NAME, UPDATE_DECLARATION } from './duck' 4 | import editorReducer from './editor' 5 | import preferencesReducer from './preferences' 6 | 7 | import { getInitialState } from './tasks' 8 | 9 | export default undoable(reduceReducers, { 10 | groupBy: groupByActionTypes([UPDATE_NAME, UPDATE_DECLARATION]), 11 | filter: excludeAction(['@@INIT', '@@router/LOCATION_CHANGE']), // for persistence rehydration 12 | limit: 20, 13 | }) 14 | 15 | function reduceReducers(state = getInitialState(), action) { 16 | const reducers = [coreReducer, editorReducer, preferencesReducer] 17 | return reducers.reduce((state, reducer) => reducer(state, action), state) 18 | } 19 | 20 | export * from './duck' 21 | export * from './editor' 22 | export * from './preferences' 23 | -------------------------------------------------------------------------------- /src/duck/preferences.js: -------------------------------------------------------------------------------- 1 | import { createAction } from 'redux-actions' 2 | 3 | export const UPDATE_PREFERENCES = 'UPDATE_PREFERENCES' 4 | 5 | export default function (state, action) { 6 | switch (action.type) { 7 | case UPDATE_PREFERENCES: { 8 | return { 9 | ...state, 10 | preferences: { 11 | ...state.preferences, 12 | ...action.payload, 13 | }, 14 | } 15 | } 16 | 17 | default: { 18 | return state 19 | } 20 | } 21 | } 22 | 23 | export const updatePreferences = createAction( 24 | UPDATE_PREFERENCES 25 | ) 26 | -------------------------------------------------------------------------------- /src/duck/tasks/createComponentBundle.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable prefer-const */ 2 | import orm from 'orm' 3 | import { pascalCase } from 'utils' 4 | import { DIR, SC, STYLED_COMPONENT, INDEX_NAME_ID, COMPONENTS_FILE_ID } from 'constantz' 5 | 6 | const createComponentBundle = ({ 7 | baseName, 8 | session, 9 | declParamIds = [], 10 | invocationIds = [], 11 | tagPrediction = 'div', 12 | }) => { 13 | const { Name, Declaration, Invocation, File } = session 14 | 15 | /* names - for the new component bundle */ 16 | const componentName = getNewComponentName(Name.all().ref(), pascalCase(baseName)) 17 | const componentNameId = Name.create(componentName) 18 | const wrapperNameId = Name.create(`${componentName}Wrapper`) 19 | 20 | // NEW COMPONENT WRAPPER 21 | const wrapperDeclarationId = Declaration.create({ 22 | nameId: wrapperNameId, 23 | type: STYLED_COMPONENT, 24 | tag: tagPrediction, 25 | }) 26 | 27 | // NEW COMPONENT 28 | let wrapperInvocationId 29 | 30 | const newComponentDeclarationId = Declaration.create({ 31 | nameId: componentNameId, 32 | invocationIds: [ 33 | (wrapperInvocationId = Invocation.create({ 34 | nameId: wrapperNameId, 35 | source: null, 36 | invocationIds, 37 | declarationId: wrapperDeclarationId, 38 | })), 39 | ], 40 | declParamIds, 41 | }) 42 | 43 | const directoryId = File.create({ 44 | nameId: componentNameId, 45 | type: DIR, 46 | children: [ 47 | File.create({ nameId: INDEX_NAME_ID, declarationIds: [newComponentDeclarationId] }), 48 | File.create({ nameId: wrapperNameId, type: SC, declarationIds: [wrapperDeclarationId] }), 49 | ], 50 | }) 51 | 52 | File.withId(COMPONENTS_FILE_ID).children.insert(directoryId) 53 | 54 | // refresh session data 55 | orm.session({ 56 | ...session.state, 57 | }) 58 | 59 | 60 | return [ 61 | componentNameId, 62 | newComponentDeclarationId, 63 | wrapperInvocationId, 64 | ] 65 | } 66 | 67 | export default createComponentBundle 68 | 69 | const getNewComponentName = (names, baseName = 'NewComponent') => { 70 | let nextNameSuffix = null 71 | const checkName = nameId => names[nameId].value === `${baseName}${nextNameSuffix || ''}` 72 | while (Object.keys(names).find(checkName)) { 73 | nextNameSuffix += 1 74 | } 75 | return `${baseName}${nextNameSuffix || ''}` 76 | } 77 | -------------------------------------------------------------------------------- /src/duck/tasks/getInitialState.js: -------------------------------------------------------------------------------- 1 | import { DIR, PARAM_INVOCATION } from 'constantz' 2 | 3 | import orm from 'orm' 4 | 5 | export default function getInitialState() { 6 | const session = orm.session({}) 7 | 8 | const { 9 | Name, 10 | DeclParam, 11 | CallParam, 12 | Invocation, 13 | File, 14 | } = session 15 | 16 | Name.create('index') // INDEX_NAME_ID: 1 17 | Name.create('key') // KEY_NAME_ID: 2 18 | Name.create('id') // ID_NAME_ID: 3 19 | 20 | const childrenNameId = Name.create('children') 21 | 22 | // {children} param invocation. 23 | // REACT_CHILDREN_INVOCATION_ID: 1 24 | Invocation.create({ 25 | nameId: childrenNameId, 26 | type: PARAM_INVOCATION, 27 | callParamIds: [ 28 | // REACT_CHILDREN_CALL_PARAM_ID: 1 29 | CallParam.create({ 30 | // REACT_CHILDREN_DECLARATION_PARAM_ID: 1 31 | declParamId: DeclParam.create({ 32 | nameId: childrenNameId, 33 | }), 34 | }), 35 | ], 36 | }) 37 | 38 | const rootFiles = [ 39 | File.create({ type: DIR, nameId: Name.create('components') }), // COMPONENTS_FILE_ID: 1 40 | File.create({ type: DIR, nameId: Name.create('containers') }), // CONTAINERS_FILE_ID: 2 41 | ] 42 | 43 | return { 44 | ...session.state, 45 | editor: { 46 | rootFiles, 47 | currentFileId: null, 48 | selectedFileId: null, 49 | projectInitialized: false, 50 | }, 51 | preferences: { 52 | semis: false, 53 | }, 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/duck/tasks/getRecursiveInvocationAndMaybeRelatedEntitiesRemover.js: -------------------------------------------------------------------------------- 1 | import { INDEX_NAME_ID } from 'constantz' 2 | 3 | // First attempt, possibly unstable! (Ok, _now_ I need automated tests...) 4 | export default function getRecursiveInvocationAndMaybeRelatedEntitiesRemover(session) { 5 | const { DeclParam, Declaration, Invocation, File } = session 6 | 7 | function maybeRemoveInvocationsDeclaration(invocationId) { 8 | const { declarationId: targetDeclarationId } = Invocation.withId(invocationId) 9 | // remove any files related to that declaration 10 | if ( 11 | Invocation.where(({ declarationId }) => declarationId === targetDeclarationId).length === 1 12 | ) { 13 | const { fileId } = Declaration.withId(targetDeclarationId) 14 | const { parentId, nameId } = File.withId(fileId) 15 | 16 | Declaration.declParams.forEach(id => DeclParam.withId(id).delete()) 17 | Declaration.invocations.forEach(id => recurseOnInvocations(id)) 18 | 19 | File.withId(fileId).declarations.remove(targetDeclarationId) 20 | 21 | if (!File.declarations.length) { 22 | File.withId(fileId).delete(nameId !== INDEX_NAME_ID && 'nameId') 23 | } 24 | 25 | if (nameId === INDEX_NAME_ID) { 26 | const { parentId: superId } = File.withId(parentId) 27 | File.withId(parentId).delete('nameId') 28 | File.withId(superId).children.remove(parentId) 29 | } else { 30 | File.withId(parentId).children.remove(fileId) 31 | } 32 | } 33 | 34 | // delete the invocation 35 | Invocation.withId(invocationId).delete() 36 | } 37 | 38 | function recurseOnInvocations(invocationId) { 39 | Invocation.withId(invocationId).invocations.forEach(id => recurseOnInvocations(id)) 40 | maybeRemoveInvocationsDeclaration(invocationId) 41 | } 42 | 43 | return invocationId => recurseOnInvocations(invocationId) 44 | } 45 | -------------------------------------------------------------------------------- /src/duck/tasks/getRecursivePropRemover.js: -------------------------------------------------------------------------------- 1 | export default function getRecursivePropRemover(session) { 2 | const { CallParam, DeclParam, Invocation } = session 3 | 4 | return function recursivePropRemover(invocationId, callParamId, options = { keep: false }) { 5 | 6 | const callParam = CallParam.withId(callParamId) 7 | 8 | const callParamNameId = callParam.nameId 9 | 10 | // param invocation 11 | if (!callParamNameId) { 12 | const piId = CallParam.invocationId 13 | const pIParent = Invocation.find((id, { invocationIds }) => invocationIds.includes(piId)) 14 | const pIParentId = pIParent.id 15 | 16 | Invocation.withId(pIParentId).invocations.remove(piId) 17 | Invocation.withId(piId).delete() 18 | CallParam.delete() 19 | return 20 | } 21 | 22 | const declParam = DeclParam.find((id, { nameId }) => nameId === callParamNameId) 23 | 24 | const declParamId = declParam.id 25 | 26 | Invocation 27 | .withId(invocationId) 28 | .declaration 29 | .declParams 30 | .remove(declParamId) 31 | 32 | 33 | CallParam.where( 34 | ({ declParamId: id }) => id === declParamId 35 | ).each( 36 | ({ invocationId, id }) => recursivePropRemover(invocationId, id) 37 | ) 38 | 39 | if (!options.keep) { 40 | CallParam.withId(callParamId).delete() 41 | DeclParam.withId(declParamId).delete() 42 | } 43 | 44 | return options.keep ? declParamId : null 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/duck/tasks/index.js: -------------------------------------------------------------------------------- 1 | export { default as addNewContainer } from './addNewContainer' 2 | export { default as createComponentBundle } from './createComponentBundle' 3 | export { default as getInitialState } from './getInitialState' 4 | export { default as initializeFromData } from './initializeFromData' 5 | export { default as getRecursivePropRemover } from './getRecursivePropRemover' 6 | export { default as getRecursiveInvocationAndMaybeRelatedEntitiesRemover } from './getRecursiveInvocationAndMaybeRelatedEntitiesRemover' 7 | -------------------------------------------------------------------------------- /src/duck/tasks/initializeFromData.js: -------------------------------------------------------------------------------- 1 | import { PROJECT_INDEX, INDEX_NAME_ID, exportTypes } from 'constantz' 2 | import orm from 'orm' 3 | import addNewContainer from './addNewContainer' 4 | 5 | export default function initializeFromData(state, apiResponse, baseName) { 6 | const session = orm.session(state) 7 | 8 | const { Declaration, File } = session 9 | 10 | addNewContainer(session, apiResponse, baseName) 11 | 12 | // files 13 | const indexFile = File.create({ 14 | nameId: INDEX_NAME_ID, 15 | declarationIds: [ 16 | Declaration.create({ 17 | type: PROJECT_INDEX, 18 | nameId: null, 19 | exportType: exportTypes.false, 20 | }), 21 | ], 22 | }) 23 | 24 | return { 25 | ...session.state, 26 | editor: { 27 | rootFiles: [...session.state.editor.rootFiles, indexFile], 28 | currentFileId: indexFile, 29 | selectedFileId: indexFile, 30 | projectInitialized: true, 31 | }, 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/i18n.js: -------------------------------------------------------------------------------- 1 | /** 2 | * i18n.js 3 | * 4 | * This will setup the i18n language files and locale data for your app. 5 | * 6 | */ 7 | import { addLocaleData } from 'react-intl' 8 | import enLocaleData from 'react-intl/locale-data/en' 9 | import deLocaleData from 'react-intl/locale-data/de' 10 | import frLocaleData from 'react-intl/locale-data/fr' 11 | 12 | import enTranslationMessages from './translations/en.json' 13 | import frTranslationMessages from './translations/fr.json' 14 | import deTranslationMessages from './translations/de.json' 15 | 16 | addLocaleData(enLocaleData) 17 | addLocaleData(deLocaleData) 18 | addLocaleData(frLocaleData) 19 | 20 | // fallback locale if users locale isn't found 21 | export const DEFAULT_LOCALE = 'en' 22 | 23 | export const appLocales = [ 24 | 'en', 25 | 'de', 26 | 'fr', 27 | ] 28 | 29 | export const formatTranslationMessages = (locale, messages) => { 30 | const defaultFormattedMessages = locale !== DEFAULT_LOCALE 31 | ? formatTranslationMessages(DEFAULT_LOCALE, enTranslationMessages) 32 | : {} 33 | return Object.keys(messages).reduce((formattedMessages, key) => { 34 | const formattedMessage = !messages[key] && locale !== DEFAULT_LOCALE 35 | ? defaultFormattedMessages[key] 36 | : messages[key] 37 | return Object.assign(formattedMessages, { [key]: formattedMessage }) 38 | }, {}) 39 | } 40 | 41 | export const translationMessages = { 42 | en: formatTranslationMessages('en', enTranslationMessages), 43 | de: formatTranslationMessages('de', deTranslationMessages), 44 | fr: formatTranslationMessages('fr', frTranslationMessages), 45 | } 46 | 47 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import './app' 2 | -------------------------------------------------------------------------------- /src/middleware/index.js: -------------------------------------------------------------------------------- 1 | export { default as spyMiddleware } from './spyMiddleware' 2 | -------------------------------------------------------------------------------- /src/middleware/spyMiddleware.js: -------------------------------------------------------------------------------- 1 | export const emitter = { 2 | listeners: [], 3 | addListener(l) { 4 | this.listeners.push(l) 5 | }, 6 | removeListener(l) { 7 | this.listeners = this.listeners.filter(lis => lis !== l) 8 | }, 9 | emit(...args) { 10 | this.listeners.forEach(l => l(...args)) 11 | }, 12 | } 13 | 14 | export default function spyMiddleware({ getState }) { 15 | return next => action => { 16 | next(action) 17 | emitter.emit({ action, getState }) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/model-prop-types.js: -------------------------------------------------------------------------------- 1 | import T from 'prop-types' 2 | import { fileTypesArray, declarationTypesArray, exportTypesArray } from 'constantz' 3 | 4 | // call param 5 | export const callParamPropTypes = { 6 | id: T.number.isRequired, 7 | valueInvocationId: T.number, 8 | declIsSpreadMember: T.bool.isRequired, 9 | name: T.string.isRequired, 10 | nameId: T.number.isRequired, 11 | invokeNameId: T.number.isRequired, 12 | valueString: T.string, 13 | } 14 | 15 | 16 | // makeSelectInvocation 17 | export const invocationPropTypes = T.shape({ 18 | invocationId: T.number.isRequired, 19 | nameId: T.number.isRequired, 20 | invocationIds: T.arrayOf(T.number).isRequired, 21 | callParams: T.arrayOf(callParamPropTypes).isRequired, 22 | hasPropsSpread: T.bool.isRequired, 23 | pseudoSpreadPropsName: T.string, 24 | inline: T.bool.isRequired, 25 | }) 26 | 27 | // makeSelectParamInvocation 28 | export const paramInvocationPropTypes = T.shape({ 29 | invocationId: T.number.isRequired, 30 | callParamId: T.number.isRequired, 31 | nameId: T.number.isRequired, 32 | declIsSpreadMember: T.bool.isRequired, 33 | chainedInvocations: T.arrayOf(T.object), 34 | }) 35 | 36 | 37 | // decl param 38 | export const declParamPropTypes = { 39 | id: T.number.isRequired, 40 | nameId: T.number.isRequired, 41 | payload: T.any, 42 | isSpreadMember: T.bool.isRequired, 43 | invokeCount: T.number.isRequired, 44 | altIds: T.arrayOf(T.number).isRequired, 45 | } 46 | 47 | 48 | // makeSelectFile 49 | export const filePropTypes = { 50 | fileId: T.number.isRequired, 51 | name: T.string.isRequired, 52 | nameId: T.number.isRequired, 53 | extension: T.string.isRequired, 54 | type: T.oneOf(fileTypesArray).isRequired, 55 | fileChildren: T.arrayOf(T.number).isRequired, 56 | declarationIds: T.arrayOf(T.number).isRequired, 57 | isDirectory: T.bool.isRequired, 58 | isCurrent: T.bool.isRequired, 59 | isSelected: T.bool.isRequired, 60 | isEmptyDir: T.bool.isRequired, 61 | containsCurrent: T.bool.isRequired, 62 | } 63 | 64 | // declaration 65 | export const declarationPropTypes = T.shape({ 66 | declarationId: T.number.isRequired, 67 | nameId: T.number.isRequired, 68 | fileId: T.number, 69 | type: T.oneOf(declarationTypesArray).isRequired, 70 | declParamIds: T.arrayOf(T.number).isRequired, 71 | exportType: T.oneOf(exportTypesArray).isRequired, 72 | declarationIds: T.arrayOf(T.number).isRequired, 73 | invocationIds: T.arrayOf(T.number).isRequired, 74 | tag: T.string, 75 | text: T.string, 76 | }) 77 | -------------------------------------------------------------------------------- /src/orm/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './orm' 2 | -------------------------------------------------------------------------------- /src/orm/models/CallParam.js: -------------------------------------------------------------------------------- 1 | import Model, { fk } from '../Model' 2 | 3 | class CallParam extends Model {} 4 | 5 | CallParam.modelName = 'CallParam' 6 | CallParam.stateKey = 'callParams' 7 | 8 | CallParam.fields = { 9 | nameId: fk('Name'), 10 | valueInvocationId: fk('Invocation'), 11 | declParamId: fk('DeclParam'), 12 | } 13 | 14 | export default CallParam 15 | -------------------------------------------------------------------------------- /src/orm/models/DeclParam.js: -------------------------------------------------------------------------------- 1 | import Model, { attr, fk } from '../Model' 2 | 3 | class DeclParam extends Model { 4 | incrementUsage = () => this.migrate({ invokeCount: invokeCount => invokeCount + 1 }) 5 | decrementUsage = () => this.migrate({ invokeCount: invokeCount => invokeCount - 1 }) 6 | } 7 | 8 | DeclParam.modelName = 'DeclParam' 9 | DeclParam.stateKey = 'declParams' 10 | 11 | DeclParam.fields = { 12 | nameId: fk('Name'), 13 | payload: attr(null), 14 | // type specific items 15 | isSpreadMember: attr(false), // react component declarations 16 | invokeCount: attr(0), // how many invocations of this param 17 | } 18 | 19 | export default DeclParam 20 | -------------------------------------------------------------------------------- /src/orm/models/Declaration.js: -------------------------------------------------------------------------------- 1 | import { STATELESS_FUNCTION_COMPONENT, DEFAULT } from 'constantz' 2 | 3 | import Model, { attr, array, fk } from '../Model' 4 | 5 | class Declaration extends Model {} 6 | 7 | Declaration.modelName = 'Declaration' 8 | Declaration.stateKey = 'declarations' 9 | 10 | Declaration.fields = { 11 | nameId: fk('Name'), 12 | fileId: fk('File'), 13 | type: attr(STATELESS_FUNCTION_COMPONENT), 14 | declParamIds: array([], 'DeclParam', 'declarationId'), 15 | exportType: attr(DEFAULT), 16 | declarationIds: array([]), 17 | invocationIds: array([]), 18 | tag: attr(null), 19 | text: attr(), 20 | } 21 | 22 | export default Declaration 23 | -------------------------------------------------------------------------------- /src/orm/models/File.js: -------------------------------------------------------------------------------- 1 | import { JS } from 'constantz' 2 | import Model, { attr, array, fk } from '../Model' 3 | 4 | class File extends Model { 5 | 6 | } 7 | 8 | File.modelName = 'File' 9 | File.stateKey = 'files' 10 | 11 | File.fields = { 12 | nameId: fk('Name'), 13 | type: attr(JS), 14 | children: array([], 'File', 'parentId'), 15 | declarationIds: array([], 'Declaration'), // defines a many-to-one relation 16 | } 17 | 18 | export default File 19 | -------------------------------------------------------------------------------- /src/orm/models/Invocation.js: -------------------------------------------------------------------------------- 1 | import { COMPONENT_INVOCATION } from 'constantz' 2 | 3 | import Model, { attr, array, fk } from '../Model' 4 | 5 | class Invocation extends Model {} 6 | 7 | Invocation.modelName = 'Invocation' 8 | Invocation.stateKey = 'invocations' 9 | 10 | Invocation.fields = { 11 | nameId: fk('Name'), 12 | declarationId: fk('Declaration'), // Not required 13 | source: attr(null), 14 | callParamIds: array([], 'CallParam', 'invocationId'), 15 | invocationIds: array(), 16 | // type specific items 17 | type: attr(COMPONENT_INVOCATION), 18 | hasPropsSpread: attr(false), 19 | pseudoSpreadPropsNameId: fk('Name'), 20 | inline: attr(false), 21 | } 22 | 23 | export default Invocation 24 | -------------------------------------------------------------------------------- /src/orm/models/Name.js: -------------------------------------------------------------------------------- 1 | import C from 'check-types' 2 | import Model, { attr, array } from '../Model' 3 | 4 | // Name is just a flat dictionary, so we override a few methods here. 5 | class Name extends Model { 6 | constructor(...args) { 7 | super(...args) 8 | const { create } = this 9 | 10 | // couldn't get super.create to work, so override this way 11 | this.create = (name, mutable = true) => { 12 | if (C.string(name)) { 13 | return create({ value: name, mutable }) 14 | } 15 | return create(name) 16 | } 17 | } 18 | } 19 | 20 | Name.modelName = 'Name' 21 | Name.stateKey = 'names' 22 | 23 | Name.fields = { 24 | value: attr(), 25 | mutable: attr(true), 26 | composite: array(), 27 | } 28 | 29 | export default Name 30 | -------------------------------------------------------------------------------- /src/orm/models/index.js: -------------------------------------------------------------------------------- 1 | import Name from './Name' 2 | import DeclParam from './DeclParam' 3 | import CallParam from './CallParam' 4 | import Invocation from './Invocation' 5 | import Declaration from './Declaration' 6 | import File from './File' 7 | 8 | export { default as Name } from './Name' 9 | export { default as DeclParam } from './DeclParam' 10 | export { default as CallParam } from './CallParam' 11 | export { default as Invocation } from './Invocation' 12 | export { default as Declaration } from './Declaration' 13 | export { default as File } from './File' 14 | 15 | export default [ 16 | Name, 17 | DeclParam, 18 | CallParam, 19 | Declaration, 20 | Invocation, 21 | File, 22 | ] 23 | -------------------------------------------------------------------------------- /src/orm/orm.js: -------------------------------------------------------------------------------- 1 | import Models from './models' 2 | 3 | class ORM { 4 | constructor() { 5 | Object.defineProperty(this, 'currentFile', { 6 | get() { 7 | return (this.state && this.state.files[this.state.editor.currentFileId]) || {} 8 | }, 9 | }) 10 | } 11 | 12 | registeredModels = {} 13 | state = {} 14 | 15 | session = state => { 16 | this.state = state 17 | return this 18 | } 19 | 20 | register = (...Models) => { 21 | this.modelClasses = this.modelClasses || {} 22 | Models.forEach(Model => { 23 | this[Model.modelName] = new Model(this, Model) 24 | this.modelClasses[Model.modelName] = Model 25 | }) 26 | return this 27 | } 28 | } 29 | 30 | const orm = new ORM() 31 | orm.register(...Models) 32 | 33 | export default orm 34 | -------------------------------------------------------------------------------- /src/orm/orm.test.js: -------------------------------------------------------------------------------- 1 | import orm from './orm' 2 | import Model, { attr, array } from './Model' 3 | 4 | // tests 5 | it('TModel', () => { 6 | orm.register(TestModel) // eslint-disable-line no-use-before-define 7 | const session = orm.session({ hi: 'hi' }) 8 | const { TModel } = session 9 | 10 | let preState 11 | 12 | // hasId 13 | expect(TModel.hasId(1)).toBeFalsy() 14 | // create 15 | preState = session.state 16 | TModel.create({ text: 'text' }) 17 | expect(session.state).toMatchObject({ tmodels: { 1: { id: 1, text: 'text' } }, hi: 'hi' }) 18 | expect(session.state).not.toBe(preState) 19 | // hasId 20 | expect(TModel.hasId(1)).toBeTruthy() 21 | // all 22 | expect(TModel.all().ref()).toMatchObject({ 1: { id: 1, text: 'text' } }) 23 | // withId 24 | expect(TModel.withId(1).ref()).toMatchObject({ id: 1, text: 'text' }) 25 | // update 26 | preState = session.state 27 | TModel.withId(1).update({ text: 'hi' }) 28 | expect(TModel.withId(1).ref()).toMatchObject({ id: 1, text: 'hi' }) 29 | expect(session.state).not.toBe(preState) 30 | 31 | // array attribute insert 32 | preState = session.state 33 | TModel.withId(1).arrayAttr.insert(2) 34 | expect(TModel.withId(1).ref()).toMatchObject({ arrayAttr: [2] }) 35 | expect(session.state).not.toBe(preState) 36 | 37 | // array attribute remove 38 | preState = session.state 39 | TModel.withId(1).arrayAttr.remove(2) 40 | expect(TModel.withId(1).ref()).toMatchObject({ arrayAttr: [] }) 41 | expect(session.state).not.toBe(preState) 42 | 43 | // delete 44 | preState = session.state 45 | TModel.withId(1).delete() 46 | expect(TModel.hasId(1)).toBeFalsy() 47 | expect(session.state).not.toBe(preState) 48 | 49 | expect(session.state).toEqual({ tmodels: {}, hi: 'hi' }) 50 | }) 51 | 52 | /* 53 | TestModel 54 | */ 55 | class TestModel extends Model {} 56 | 57 | TestModel.modelName = 'TModel' 58 | TestModel.stateKey = 'tmodels' 59 | 60 | TestModel.fields = { 61 | text: attr({ required: true }), 62 | arrayAttr: array(), 63 | } 64 | 65 | -------------------------------------------------------------------------------- /src/selectors.js: -------------------------------------------------------------------------------- 1 | import { partition } from 'lodash' 2 | import { createSelector } from 'reselect' 3 | import { DIR, SC } from 'constantz' 4 | import { sortAlphabetically } from 'utils' 5 | 6 | /* 7 | Base selectors 8 | */ 9 | export const selectProjectInitialized = s => s.app.present.editor.projectInitialized 10 | export const selectCurrentFileId = (s, p) => p.currentFileId || s.app.present.editor.currentFileId 11 | export const selectSelectedFileId = s => s.app.present.editor.selectedFileId 12 | export const selectRootFiles = s => s.app.present.editor.rootFiles 13 | export const selectNames = s => s.app.present.names || {} 14 | export const selectFiles = s => s.app.present.files || {} 15 | export const selectDeclarations = s => s.app.present.declarations || {} 16 | export const selectInvocations = s => s.app.present.invocations || {} 17 | export const selectCallParams = s => s.app.present.callParams || {} 18 | export const selectDeclParams = s => s.app.present.declParams || {} 19 | export const selectPreferences = s => s.app.present.preferences 20 | export const selectSemis = s => s.app.present.preferences.semis 21 | 22 | 23 | /* 24 | Model selector creators - `makeSelect${modelName}` 25 | */ 26 | export const makeSelectInvocation = () => createSelector( 27 | selectInvocations, 28 | (state, props) => props.invocationId, 29 | (invocations, invocationId) => { 30 | const { id, ...rest } = invocations[invocationId] 31 | return { 32 | invocationId, 33 | ...rest, 34 | } 35 | } 36 | ) 37 | 38 | export const makeSelectName = () => (state, props) => selectNames(state)[props.nameId] 39 | 40 | export const makeSelectDeclaration = () => createSelector( 41 | selectDeclarations, 42 | (state, props) => props.declarationId, 43 | (declarations, declarationId) => { 44 | const { id, ...rest } = declarations[declarationId] 45 | return { 46 | declarationId, 47 | ...rest, 48 | } 49 | } 50 | ) 51 | 52 | export const makeSelectFile = () => createSelector( 53 | selectNames, 54 | selectFiles, 55 | selectCurrentFileId, 56 | selectSelectedFileId, 57 | (state, props) => props.fileId, 58 | (names, files, currentFileId, selectedFileId, fileId) => { 59 | const { nameId, type, children, declarationIds } = files[fileId] 60 | const sortedChildren = children.sort((a, b) => { 61 | const aName = names[files[a].nameId].value 62 | const bName = names[files[b].nameId].value 63 | return ( 64 | (aName === 'index' && 1) || 65 | (bName === 'index' && -1) || 66 | sortAlphabetically(aName, bName) 67 | ) 68 | }) 69 | 70 | const [directories, fichiers] = partition(sortedChildren, id => 71 | files[id].type === DIR 72 | ) 73 | const [styled, others] = partition(fichiers, id => 74 | files[id].type === SC 75 | ) 76 | 77 | const isDir = type === DIR 78 | 79 | return { 80 | fileId, 81 | nameId, 82 | name: names[nameId].value, 83 | type, 84 | extension: (type === SC && '.js') || (!isDir ? `.${type}` : ''), 85 | fileChildren: [...directories, ...styled, ...others], 86 | isDirectory: isDir, 87 | isCurrent: fileId === currentFileId, 88 | isSelected: fileId === selectedFileId, 89 | isEmptyDir: !children.length && type === DIR, 90 | containsCurrent: children.includes(currentFileId), 91 | declarationIds, 92 | } 93 | } 94 | ) 95 | 96 | -------------------------------------------------------------------------------- /src/styleUtils.js: -------------------------------------------------------------------------------- 1 | 2 | export const buffer = amount => `padding: ${amount}px; margin: -${amount}px;` 3 | 4 | -------------------------------------------------------------------------------- /src/theme-proxy.js: -------------------------------------------------------------------------------- 1 | import invariant from 'invariant' 2 | 3 | const colors = new Proxy({}, { 4 | get(target, name) { 5 | return props => { 6 | invariant( 7 | props.theme.colors[name], 8 | `${name} is not a color \npossible colors: ${Object.keys(props.theme.colors)}` 9 | ) 10 | return props.theme.colors[name] 11 | } 12 | }, 13 | }) 14 | 15 | const themeProxy = new Proxy({}, { 16 | get(target, name) { 17 | switch (name) { 18 | case 'color': return colors 19 | case 'colors': return colors 20 | default: return props => { 21 | invariant(props.theme[name], `${name} is not a theme property`) 22 | return props.theme[name] 23 | } 24 | } 25 | }, 26 | }) 27 | 28 | export default themeProxy 29 | -------------------------------------------------------------------------------- /src/translations/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "app.containers.NotFoundPage.header": "Seite nicht gefunden" 3 | } 4 | -------------------------------------------------------------------------------- /src/translations/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "app.containers.NotFoundPage.header": "Page not found." 3 | } 4 | -------------------------------------------------------------------------------- /src/translations/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "app.containers.NotFoundPage.header": "Page non trouvée." 3 | } 4 | -------------------------------------------------------------------------------- /src/utils/camelcase.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // See https://github.com/facebook/create-react-app/blob/master/packages/react-scripts/template/README.md#npm-run-build-fails-to-minify 3 | // https://www.npmjs.com/package/camelcase 4 | const preserveCamelCase = input => { 5 | let isLastCharLower = false; 6 | let isLastCharUpper = false; 7 | let isLastLastCharUpper = false; 8 | 9 | for (let i = 0; i < input.length; i++) { 10 | const c = input[i]; 11 | 12 | if (isLastCharLower && /[a-zA-Z]/.test(c) && c.toUpperCase() === c) { 13 | input = input.slice(0, i) + '-' + input.slice(i); 14 | isLastCharLower = false; 15 | isLastLastCharUpper = isLastCharUpper; 16 | isLastCharUpper = true; 17 | i++; 18 | } else if (isLastCharUpper && isLastLastCharUpper && /[a-zA-Z]/.test(c) && c.toLowerCase() === c) { 19 | input = input.slice(0, i - 1) + '-' + input.slice(i - 1); 20 | isLastLastCharUpper = isLastCharUpper; 21 | isLastCharUpper = false; 22 | isLastCharLower = true; 23 | } else { 24 | isLastCharLower = c.toLowerCase() === c; 25 | isLastLastCharUpper = isLastCharUpper; 26 | isLastCharUpper = c.toUpperCase() === c; 27 | } 28 | } 29 | 30 | return input; 31 | }; 32 | 33 | export default (input, options) => { 34 | options = Object.assign({ 35 | pascalCase: false 36 | }, options); 37 | 38 | const postProcess = x => options.pascalCase ? x.charAt(0).toUpperCase() + x.slice(1) : x; 39 | 40 | if (Array.isArray(input)) { 41 | input = input.map(x => x.trim()) 42 | .filter(x => x.length) 43 | .join('-'); 44 | } else { 45 | input = input.trim(); 46 | } 47 | 48 | if (input.length === 0) { 49 | return ''; 50 | } 51 | 52 | if (input.length === 1) { 53 | return options.pascalCase ? input.toUpperCase() : input.toLowerCase(); 54 | } 55 | 56 | if (/^[a-z\d]+$/.test(input)) { 57 | return postProcess(input); 58 | } 59 | 60 | const hasUpperCase = input !== input.toLowerCase(); 61 | 62 | if (hasUpperCase) { 63 | input = preserveCamelCase(input); 64 | } 65 | 66 | input = input 67 | .replace(/^[_.\- ]+/, '') 68 | .toLowerCase() 69 | .replace(/[_.\- ]+(\w|$)/g, (m, p1) => p1.toUpperCase()); 70 | 71 | return postProcess(input); 72 | }; 73 | /* 74 | 75 | MIT License 76 | 77 | Copyright (c) Sindre Sorhus (sindresorhus.com) 78 | 79 | 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: 80 | 81 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 82 | 83 | 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. 84 | 85 | */ -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | import C from 'check-types' 2 | import classNames from 'classnames' 3 | import camelCase from './camelcase' 4 | 5 | export const addClassNames = (...args) => ({ className: classNames(...args) }) 6 | export const capitalize = s => s ? s.charAt(0).toUpperCase() + s.slice(1) : undefined 7 | export const pascalCase = s => camelCase(s, { pascalCase: true }) 8 | export const compose = (...fns) => fns.reduce((f, g) => (...args) => f(g(...args))) 9 | export const composed = (...fns) => fns.reverse().reduce((f, g) => (...args) => f(g(...args))) 10 | export const spaces = (amount = 1) => '\u00A0'.repeat(amount) 11 | export const indent = (amount = 1) => spaces(amount * 2) 12 | export const getId = ((id = 0) => () => (`${id += 1}`))() // eslint-disable-line 13 | export const oneOf = (...fns) => item => 14 | fns.slice(1).reduce((out, fn) => out || fn(item), fns[0](item)) 15 | export const sortAlphabetically = (a, b) => (a < b && '-1') || (b < a && '1') || 0 16 | export const toArray = obj => Object.entries(obj).map(([, value]) => value) 17 | export const lastItem = arr => arr[arr.length - 1] 18 | export const pD = fn => (e, ...args) => { e.preventDefault(); fn(...args) } 19 | export const pDsP = fn => (e, ...args) => { e.preventDefault(); e.stopPropagation(); fn(...args) } 20 | export const isUrl = s => C.string(s) && s.startsWith('http') 21 | export const isImageUrl = s => isUrl(s) && ( 22 | s.match(/(.jpg|.jpeg|.png|.gif)$/) || s.includes('images.unsplash') 23 | ) 24 | export const isNonImageUrl = s => isUrl(s) && !isImageUrl(s) 25 | export const not = fn => (...args) => !fn(...args) 26 | export const all = (...fns) => (...args) => fns.reduce((f, g) => f && g(...args), true) 27 | 28 | export { default as camelCase } from './camelcase' 29 | --------------------------------------------------------------------------------