├── .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 |
12 |
13 |
14 |
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 |
45 | You need to enable JavaScript to run this app.
46 |
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 |
31 |
35 |
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 |
30 |
34 |
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 |
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 |
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 |
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 |
26 | You need to enable JavaScript to run this app.
27 |
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 |
--------------------------------------------------------------------------------