├── .gitignore ├── src ├── actions │ ├── index.js │ ├── requestLibraryActions.js │ └── workspaceActions.js ├── reducers │ ├── index.js │ ├── libraryReducer.js │ ├── workspace │ │ ├── evaluationReducer.js │ │ ├── selectionReducer.js │ │ ├── dragReducer.js │ │ ├── addRemoveElementReducer.js │ │ ├── updateElementReducer.js │ │ └── workspaceReducerUtils.js │ └── workspaceReducer.js ├── containers │ ├── TestInput.js │ ├── TestOutput.js │ ├── SelectablePipe.js │ ├── Primitive.js │ ├── Draggable.js │ ├── Brick.js │ ├── Library.js │ ├── MainBrick.js │ ├── SelectedElementDialog.js │ └── Workspace.js ├── utils │ ├── colors.js │ ├── componentNames.js │ ├── ComponentFactory.js │ ├── unitTestUtils.js │ ├── index.js │ ├── slotSelection.js │ ├── slotPosition.js │ ├── evalUtils.js │ └── translationUtils.js ├── components │ ├── ElementDetails │ │ ├── DefaultDetails.js │ │ ├── DialogButton.js │ │ ├── ElementDetailsFactory.js │ │ ├── MainBrickDetails.js │ │ ├── PrimitiveDetails.js │ │ ├── TypesSelect.js │ │ ├── TestNodeDetails.js │ │ └── CustomValueInput.js │ ├── Logo.js │ ├── Translate.js │ ├── tutorialSteps.js │ ├── Ellipse.js │ ├── LambdaBricksApp.js │ ├── TestSummary.js │ ├── Module.js │ ├── TestResult.js │ ├── Pipe.js │ ├── WorkspaceSurface.js │ ├── Slot.js │ ├── SelectedElementDialog.js │ ├── SelectablePipe.js │ ├── SlotGroup.js │ ├── composeBrick.js │ ├── TestInput.js │ ├── TestOutput.js │ ├── Library.js │ ├── Primitive.js │ ├── constants.js │ ├── Brick.js │ ├── MainBrick.js │ ├── Workspace.js │ └── Tutorial.js ├── index.js ├── tutorial-index.js ├── store │ └── configureStore.js └── propTypes │ └── index.js ├── docs ├── images │ ├── guards.jpg │ ├── screenshot.png │ ├── search-brick.png │ ├── video-preview.png │ ├── custom-boolean-value-input.png │ └── custom-number-value-input.png ├── tutorial.html ├── index.html ├── library │ └── js │ │ ├── 3.json │ │ ├── 2.json │ │ ├── 4.json │ │ └── 1.json ├── styles.css └── react-joyride-compiled.css ├── package.json ├── README.md └── LICENSE-AGPL /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | dist/app.js 4 | dist/tutorial/app.js 5 | -------------------------------------------------------------------------------- /src/actions/index.js: -------------------------------------------------------------------------------- 1 | export * from './requestLibraryActions' 2 | export * from './workspaceActions' 3 | -------------------------------------------------------------------------------- /docs/images/guards.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lambdabricks/bricks-front-react/HEAD/docs/images/guards.jpg -------------------------------------------------------------------------------- /docs/images/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lambdabricks/bricks-front-react/HEAD/docs/images/screenshot.png -------------------------------------------------------------------------------- /docs/images/search-brick.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lambdabricks/bricks-front-react/HEAD/docs/images/search-brick.png -------------------------------------------------------------------------------- /docs/images/video-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lambdabricks/bricks-front-react/HEAD/docs/images/video-preview.png -------------------------------------------------------------------------------- /docs/images/custom-boolean-value-input.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lambdabricks/bricks-front-react/HEAD/docs/images/custom-boolean-value-input.png -------------------------------------------------------------------------------- /docs/images/custom-number-value-input.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lambdabricks/bricks-front-react/HEAD/docs/images/custom-number-value-input.png -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import { library } from './libraryReducer' 3 | import { workspace } from './workspaceReducer' 4 | 5 | export default combineReducers({ 6 | library, 7 | workspace 8 | }) 9 | -------------------------------------------------------------------------------- /src/containers/TestInput.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | 3 | import { handleSelectElement } from '../utils' 4 | import TestInput from '../components/TestInput' 5 | 6 | const mapDispatchToProps = (dispatch) => { 7 | return { 8 | handleClick: handleSelectElement(dispatch) 9 | } 10 | } 11 | 12 | export default connect(null, mapDispatchToProps)(TestInput) 13 | -------------------------------------------------------------------------------- /src/utils/colors.js: -------------------------------------------------------------------------------- 1 | import { colors } from '../components/constants' 2 | 3 | export const getFillColor = (type, value) => { 4 | const typeColor = colors[type] 5 | 6 | switch (type) { 7 | case "boolean": 8 | const valueColor = colors[value] 9 | return value && valueColor ? valueColor : typeColor 10 | default: 11 | return typeColor 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/containers/TestOutput.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | 3 | import { handleSelectElement } from '../utils' 4 | import TestOutput from '../components/TestOutput' 5 | 6 | const mapDispatchToProps = (dispatch) => { 7 | return { 8 | handleClick: handleSelectElement(dispatch) 9 | } 10 | } 11 | 12 | export default connect(null, mapDispatchToProps)(TestOutput) 13 | -------------------------------------------------------------------------------- /src/containers/SelectablePipe.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | 3 | import { handleSelectElement } from '../utils' 4 | import SelectablePipe from '../components/SelectablePipe' 5 | 6 | const mapDispatchToProps = (dispatch) => { 7 | return { 8 | handleClick: handleSelectElement(dispatch) 9 | } 10 | } 11 | 12 | export default connect(null, mapDispatchToProps)(SelectablePipe) 13 | -------------------------------------------------------------------------------- /src/utils/componentNames.js: -------------------------------------------------------------------------------- 1 | export const BRICK = 'BRICK' 2 | export const MAIN_BRICK = 'MAIN_BRICK' 3 | export const PIPE = 'PIPE' 4 | export const PRIMITIVE = 'PRIMITIVE' 5 | export const SELECTABLE_PIPE = 'SELECTABLE_PIPE' 6 | export const SLOT = 'SLOT' 7 | export const SURFACE = 'SURFACE' 8 | export const TEST_INPUT = 'TEST_INPUT' 9 | export const TEST_OUTPUT = 'TEST_OUTPUT' 10 | export const TEST_RESULT = 'TEST_RESULT' 11 | -------------------------------------------------------------------------------- /docs/tutorial.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Lambda Bricks 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 |
13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/containers/Primitive.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | 3 | import Draggable from './Draggable' 4 | import Primitive from '../components/Primitive' 5 | 6 | import { addPipeOrSelectSlot } from '../actions' 7 | 8 | const mapDispatchToProps = (dispatch) => { 9 | return { 10 | selectSlot: (elementId, slotId) => { 11 | dispatch(addPipeOrSelectSlot('input', elementId, slotId)) 12 | } 13 | } 14 | } 15 | 16 | export default connect(null, mapDispatchToProps)(Draggable(Primitive)) 17 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Lambda Bricks 4 | 5 | 6 | 7 | 8 | 9 | 14 | 15 | 16 |
17 | 18 |
19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /docs/library/js/3.json: -------------------------------------------------------------------------------- 1 | { 2 | "modules": 3 | [ 4 | { 5 | "label": "String", 6 | "name": "string", 7 | "functions": 8 | [ 9 | { "id" : 21, "label": "concat", "name" : "concat", "arity" : 2 }, 10 | { "id" : 22, "label": "length", "name" : "length", "arity" : 1 }, 11 | { "id" : 23, "label": "reverse", "name" : "reverse", "arity" : 1 } 12 | ] 13 | } 14 | ], 15 | "primitives": 16 | { 17 | "string": 18 | { 19 | "id": 103, 20 | "label": "String", 21 | "type": "string" 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/components/ElementDetails/DefaultDetails.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react' 2 | 3 | import DialogButton from './DialogButton' 4 | 5 | export default class DefaultDetails extends Component { 6 | render() { 7 | const { 8 | deleteElement, 9 | id 10 | } = this.props 11 | 12 | return ( 13 | deleteElement(id) } 15 | message="delete" 16 | /> 17 | ) 18 | } 19 | } 20 | 21 | DefaultDetails.propTypes = { 22 | deleteElement: PropTypes.func.isRequired, 23 | id: PropTypes.number.isRequired 24 | } 25 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { parse } from 'query-string' 2 | import React from 'react' 3 | import ReactDOM from 'react-dom' 4 | import { Provider } from 'react-redux' 5 | 6 | import LambdaBricksApp from './components/LambdaBricksApp' 7 | import configureStore from './store/configureStore' 8 | 9 | const store = configureStore() 10 | const params = parse(location.search) 11 | 12 | ReactDOM.render( 13 | 14 | 18 | , 19 | document.getElementById('main') 20 | ) 21 | -------------------------------------------------------------------------------- /src/tutorial-index.js: -------------------------------------------------------------------------------- 1 | import { parse } from 'query-string' 2 | import React from 'react' 3 | import ReactDOM from 'react-dom' 4 | import { Provider } from 'react-redux' 5 | 6 | import Tutorial from './components/Tutorial' 7 | import configureStore from './store/configureStore' 8 | 9 | const store = configureStore() 10 | const params = parse(location.search) 11 | const step = parseInt(params['step']) || 1 12 | 13 | ReactDOM.render( 14 | 15 | 19 | , 20 | document.getElementById('main') 21 | ) 22 | -------------------------------------------------------------------------------- /src/components/ElementDetails/DialogButton.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react' 2 | 3 | import Translate from '../Translate' 4 | 5 | export default class DialogButton extends Component { 6 | render() { 7 | const { 8 | message, 9 | onClick 10 | } = this.props 11 | 12 | return ( 13 | 18 | ) 19 | } 20 | } 21 | 22 | DialogButton.propTypes = { 23 | message: PropTypes.string.isRequired, 24 | onClick: PropTypes.func.isRequired 25 | } 26 | -------------------------------------------------------------------------------- /docs/library/js/2.json: -------------------------------------------------------------------------------- 1 | { 2 | "modules": 3 | [ 4 | { 5 | "label": "Arithmetic", 6 | "name": "arithmetic", 7 | "functions": 8 | [ 9 | { "id" : 41, "label": "+", "name" : "+", "arity" : 2 }, 10 | { "id" : 42, "label": "-", "name" : "-", "arity" : 2 }, 11 | { "id" : 43, "label": "*", "name" : "*", "arity" : 2 }, 12 | { "id" : 44, "label": "/", "name" : "/", "arity" : 2 } 13 | ] 14 | } 15 | ], 16 | "primitives": 17 | { 18 | "number": 19 | { 20 | "id": 102, 21 | "label": "Number", 22 | "type": "number" 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/store/configureStore.js: -------------------------------------------------------------------------------- 1 | import { applyMiddleware, compose, createStore } from 'redux' 2 | import { batchedSubscribe } from 'redux-batched-subscribe'; 3 | import { unstable_batchedUpdates as batchedUpdates } from 'react-dom'; 4 | import thunkMiddleware from 'redux-thunk' 5 | 6 | import rootReducer from '../reducers' 7 | 8 | const middleware = compose( 9 | applyMiddleware(thunkMiddleware), 10 | batchedSubscribe(batchedUpdates) 11 | ) 12 | 13 | const finalCreateStore = middleware(createStore) 14 | 15 | export default function configureStore(initialState) { 16 | return finalCreateStore(rootReducer, initialState) 17 | } 18 | -------------------------------------------------------------------------------- /src/containers/Draggable.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import { startDrag } from '../actions' 3 | import { LEFT } from '../components/constants' 4 | 5 | const mapDispatchToProps = (dispatch) => { 6 | return { 7 | handleMouseDown: (elementId, mouseEvent, elementPosition) => { 8 | if(mouseEvent.button != LEFT) 9 | return 10 | 11 | dispatch( 12 | startDrag( 13 | elementId, 14 | { x: mouseEvent.pageX, y: mouseEvent.pageY }, 15 | elementPosition 16 | ) 17 | ) 18 | } 19 | } 20 | } 21 | 22 | export default connect(null, mapDispatchToProps) 23 | -------------------------------------------------------------------------------- /src/components/Logo.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | 3 | export default class Logo extends Component { 4 | render() { 5 | return ( 6 | 17 | ) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/utils/ComponentFactory.js: -------------------------------------------------------------------------------- 1 | import Brick from '../containers/Brick' 2 | import SelectablePipe from '../containers/SelectablePipe' 3 | import MainBrick from '../containers/MainBrick' 4 | import Primitive from '../containers/Primitive' 5 | 6 | import { 7 | BRICK, 8 | MAIN_BRICK, 9 | SELECTABLE_PIPE, 10 | PRIMITIVE 11 | } from './componentNames' 12 | 13 | export const getComponent = (type) => { 14 | switch (type) { 15 | case BRICK: 16 | return Brick 17 | case MAIN_BRICK: 18 | return MainBrick 19 | case SELECTABLE_PIPE: 20 | return SelectablePipe 21 | case PRIMITIVE: 22 | return Primitive 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/reducers/libraryReducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | REQUEST_LIBRARY, 3 | RECEIVE_LIBRARY 4 | } from '../actions' 5 | 6 | const initialLibrary = { 7 | isFetching: false, 8 | items: {} 9 | } 10 | 11 | export const library = (state = initialLibrary, action) => { 12 | const { payload, type } = action 13 | 14 | switch (type) { 15 | case RECEIVE_LIBRARY: 16 | return Object.assign({}, state, { 17 | isFetching: false, 18 | didInvalidate: false, 19 | items: payload.items 20 | }) 21 | case REQUEST_LIBRARY: 22 | return Object.assign({}, state, { 23 | isFetching: true, 24 | didInvalidate: false 25 | }) 26 | default: 27 | return state 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/components/ElementDetails/ElementDetailsFactory.js: -------------------------------------------------------------------------------- 1 | import DefaultDetails from './DefaultDetails' 2 | import MainBrickDetails from './MainBrickDetails' 3 | import PrimitiveDetails from './PrimitiveDetails' 4 | import TestNodeDetails from './TestNodeDetails' 5 | 6 | import { 7 | MAIN_BRICK, 8 | PRIMITIVE, 9 | TEST_INPUT, 10 | TEST_OUTPUT 11 | } from '../../utils/componentNames' 12 | 13 | export const getDetailsComponent = (type) => { 14 | switch (type) { 15 | case MAIN_BRICK: 16 | return MainBrickDetails 17 | case PRIMITIVE: 18 | return PrimitiveDetails 19 | case TEST_INPUT: 20 | case TEST_OUTPUT: 21 | return TestNodeDetails 22 | default: 23 | return DefaultDetails 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/containers/Brick.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | 3 | import Brick from '../components/Brick' 4 | import { addPipeOrSelectSlot } from '../actions' 5 | import Draggable from './Draggable' 6 | 7 | const mapDispatchToProps = (dispatch) => { 8 | return { 9 | selectBrickInputSlot: (elementId, slotId) => { 10 | // A Brick's input slot is on the contrary an output for a pipe 11 | dispatch(addPipeOrSelectSlot('output', elementId, slotId)) 12 | }, 13 | selectBrickOutputSlot: (elementId, slotId) => { 14 | // A Brick's output slot is on the contrary an input for a pipe 15 | dispatch(addPipeOrSelectSlot('input', elementId, slotId)) 16 | } 17 | } 18 | } 19 | 20 | export default connect(null, mapDispatchToProps)(Draggable(Brick)) 21 | -------------------------------------------------------------------------------- /src/components/Translate.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react' 2 | 3 | import { getMessage } from '../utils/translationUtils' 4 | 5 | export default class Translate extends Component { 6 | render() { 7 | const { 8 | childProps, 9 | HtmlElement, 10 | message 11 | } = this.props 12 | const { locale } = this.context 13 | 14 | return ( 15 | 16 | { getMessage(locale, message) } 17 | 18 | ) 19 | } 20 | } 21 | 22 | Translate.contextTypes = { 23 | locale: PropTypes.string.isRequired 24 | } 25 | 26 | Translate.propTypes = { 27 | childProps: PropTypes.object, 28 | HtmlElement: PropTypes.string.isRequired, 29 | message: PropTypes.string.isRequired 30 | } 31 | -------------------------------------------------------------------------------- /src/containers/Library.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import { addBrick, addPrimitive, fetchLibraryIfNeeded } from '../actions' 3 | import Library from '../components/Library' 4 | 5 | const mapStateToProps = (state) => { 6 | return { 7 | ...state.library 8 | } 9 | } 10 | 11 | const mapDispatchToProps = (dispatch) => { 12 | return { 13 | fetchLibrary: (id) => { 14 | dispatch(fetchLibraryIfNeeded(id)) 15 | }, 16 | onFunctionClick: (moduleName, brick) => { 17 | dispatch(addBrick({ arity: brick.arity, moduleName, name: brick.name })) 18 | }, 19 | onPrimitiveClick: (primitive) => { 20 | dispatch(addPrimitive(primitive)) 21 | } 22 | } 23 | } 24 | 25 | export default connect(mapStateToProps, mapDispatchToProps)(Library) 26 | -------------------------------------------------------------------------------- /src/components/tutorialSteps.js: -------------------------------------------------------------------------------- 1 | import { 2 | CLEAN, 3 | FUNCTION 4 | } from './constants' 5 | 6 | const tutorialSteps = { 7 | 1: { 8 | libraryId: '2', 9 | openSiteTourAtStart: true, 10 | worspaceType: CLEAN 11 | }, 12 | 2: { 13 | libraryId: '3', 14 | openSiteTourAtStart: false, 15 | worspaceType: CLEAN 16 | }, 17 | 3: { 18 | libraryId: '4', 19 | openSiteTourAtStart: false, 20 | worspaceType: CLEAN 21 | }, 22 | 4: { 23 | libraryId: '4', 24 | openSiteTourAtStart: false, 25 | worspaceType: CLEAN 26 | }, 27 | 5: { 28 | libraryId: '4', 29 | openSiteTourAtStart: false, 30 | worspaceType: FUNCTION 31 | } 32 | } 33 | 34 | export const getTutorialConfig = (step) => tutorialSteps[step] 35 | 36 | export const totalSteps = () => Object.keys(tutorialSteps).length 37 | -------------------------------------------------------------------------------- /src/containers/MainBrick.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | 3 | import { addPipeOrSelectSlot } from '../actions' 4 | import { UNIT_TEST } from '../components/constants' 5 | import { handleSelectElement } from '../utils' 6 | import MainBrick from '../components/MainBrick' 7 | 8 | const mapDispatchToProps = (dispatch, ownProps) => { 9 | const { workspaceType } = ownProps 10 | 11 | return { 12 | handleClick: workspaceType == UNIT_TEST ? handleSelectElement(dispatch) : () => { }, 13 | selectBrickInputSlot: (elementId, slotId) => { 14 | dispatch(addPipeOrSelectSlot('input', elementId, slotId)) 15 | }, 16 | selectBrickOutputSlot: (elementId, slotId) => { 17 | dispatch(addPipeOrSelectSlot('output', elementId, slotId)) 18 | } 19 | } 20 | } 21 | 22 | export default connect(null, mapDispatchToProps)(MainBrick) 23 | -------------------------------------------------------------------------------- /src/components/Ellipse.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import { Path, Shape } from 'react-art' 3 | 4 | import { SizePropTypes } from '../propTypes' 5 | 6 | class Ellipse extends Component { 7 | render() { 8 | const { 9 | fillColor, 10 | size 11 | } = this.props 12 | 13 | const xRadius = size.width / 2 14 | const yRadius = size.height / 2 15 | 16 | const path = new Path() 17 | path.move(0, yRadius) 18 | path.arc(size.width, 0, xRadius, yRadius) 19 | path.arc(-size.width, 0, xRadius, yRadius) 20 | path.close() 21 | 22 | return ( 23 | 28 | ) 29 | } 30 | } 31 | 32 | Ellipse.propTypes = { 33 | fillColor: PropTypes.string.isRequired, 34 | size: SizePropTypes.isRequired 35 | } 36 | 37 | export default Ellipse 38 | -------------------------------------------------------------------------------- /src/utils/unitTestUtils.js: -------------------------------------------------------------------------------- 1 | import { testColors } from '../components/constants' 2 | 3 | export const FAILING = 'failing' 4 | export const PASSING = 'passing' 5 | export const PENDING = 'pending' 6 | 7 | export const getTestResult = (mainBrick, unitTest) => { 8 | const testOutputId = mainBrick.testOutputIds[0] 9 | 10 | if(!testOutputId) { 11 | return PENDING 12 | } 13 | 14 | const brickValue = mainBrick.outputSlots[testOutputId].value 15 | const value = unitTest.values[brickValue && brickValue.slotId] 16 | const testOutput = unitTest.values[testOutputId] 17 | 18 | if(value && value.type && value.value && 19 | testOutput && testOutput.type && testOutput.value 20 | ) { 21 | if(value.type == testOutput.type && value.value == testOutput.value) { 22 | return PASSING 23 | } else { 24 | return FAILING 25 | } 26 | } 27 | 28 | return PENDING 29 | } 30 | 31 | export const getTestResultColor = (unitTest) => { 32 | return testColors[unitTest.result] 33 | } 34 | -------------------------------------------------------------------------------- /src/propTypes/index.js: -------------------------------------------------------------------------------- 1 | import { PropTypes } from 'react' 2 | 3 | export const BindingPropTypes = PropTypes.shape({ 4 | type: PropTypes.string, 5 | value: PropTypes.string 6 | }) 7 | 8 | export const PositionPropTypes = PropTypes.shape({ 9 | x: PropTypes.number.isRequired, 10 | y: PropTypes.number.isRequired 11 | }) 12 | 13 | export const PrimitivePropTypes = PropTypes.shape({ 14 | id: PropTypes.number.isRequired, 15 | label: PropTypes.string.isRequired, 16 | type: PropTypes.string.isRequired, 17 | values: PropTypes.arrayOf( 18 | PropTypes.shape({ 19 | label: PropTypes.string.isRequired, 20 | name: PropTypes.string.isRequired 21 | }) 22 | ) 23 | }) 24 | 25 | export const SizePropTypes = PropTypes.shape({ 26 | height: PropTypes.number.isRequired, 27 | width: PropTypes.number.isRequired 28 | }) 29 | 30 | export const SlotPropTypes = PropTypes.objectOf( 31 | PropTypes.shape({ 32 | id: PropTypes.number.isRequired, 33 | index: PropTypes.number.isRequired 34 | }).isRequired 35 | ) 36 | -------------------------------------------------------------------------------- /src/components/LambdaBricksApp.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | 3 | import { UNIT_TEST } from './constants' 4 | import Library from '../containers/Library' 5 | import Logo from './Logo' 6 | import Workspace from '../containers/Workspace' 7 | 8 | export default class LambdaBricksApp extends Component { 9 | render() { 10 | const { libraryId, workspaceType } = this.props 11 | 12 | return ( 13 |
14 | 15 | 16 | 17 |
18 | ) 19 | } 20 | 21 | getChildContext() { 22 | return { 23 | locale: 'en' 24 | } 25 | } 26 | } 27 | 28 | LambdaBricksApp.childContextTypes = { 29 | locale: PropTypes.string.isRequired 30 | } 31 | 32 | LambdaBricksApp.defaultProps = { 33 | libraryId: '1', 34 | workspaceType: UNIT_TEST 35 | } 36 | 37 | LambdaBricksApp.PropTypes = { 38 | libraryId: PropTypes.string.isRequired, 39 | workspaceType: PropTypes.string.isRequired 40 | } 41 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | import { selectElement } from '../actions' 2 | import { LEFT } from '../components/constants' 3 | 4 | export * from './colors' 5 | export * from './slotPosition' 6 | export * from './slotSelection' 7 | 8 | export const isNotEmpty = (object) => { 9 | return Object.keys(object).length > 0 10 | } 11 | 12 | export const handleSelectElement = (dispatch) => { 13 | return (elementId, mouseEvent, workspaceIndex) => { 14 | if(mouseEvent.button != LEFT) 15 | return 16 | 17 | dispatch( 18 | selectElement( 19 | elementId, 20 | { x: mouseEvent.pageX, y: mouseEvent.pageY }, 21 | workspaceIndex 22 | ) 23 | ) 24 | } 25 | } 26 | 27 | export const unique = (array) => { 28 | return array.filter((() => { 29 | var seen = {} 30 | return (element, index, array) => !(element in seen) && (seen[element] = 1) 31 | })()) 32 | } 33 | 34 | export const bound = (value, lowerLimit, upperLimit) => { 35 | if(value < lowerLimit) { 36 | return lowerLimit 37 | } 38 | if(value > upperLimit) { 39 | return upperLimit 40 | } 41 | 42 | return value 43 | } 44 | -------------------------------------------------------------------------------- /src/components/ElementDetails/MainBrickDetails.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react' 2 | 3 | import DialogButton from './DialogButton' 4 | 5 | export default class MainBrickDetails extends Component { 6 | render() { 7 | const { 8 | addUnitTest, 9 | deleteUnitTest, 10 | totalUnitTests, 11 | workspaceIndex 12 | } = this.props 13 | 14 | return ( 15 |
16 |
17 | 21 |
22 | { totalUnitTests > 1 && 23 |
24 | deleteUnitTest(workspaceIndex) } 26 | message="deleteUnitTest" 27 | /> 28 |
29 | } 30 |
31 | ) 32 | } 33 | } 34 | 35 | MainBrickDetails.propTypes = { 36 | addUnitTest: PropTypes.func.isRequired, 37 | deleteUnitTest: PropTypes.func.isRequired, 38 | totalUnitTests: PropTypes.number.isRequired, 39 | workspaceIndex: PropTypes.number.isRequired 40 | } 41 | -------------------------------------------------------------------------------- /src/components/TestSummary.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react' 2 | 3 | import { 4 | getTestResultColor 5 | } from '../utils/unitTestUtils' 6 | 7 | const ulStyles = { 8 | display: 'inline-block', 9 | listStyleType: 'none', 10 | margin: 0, 11 | padding: 0 12 | } 13 | 14 | const liStyles = { 15 | border: '2px solid black', 16 | borderRadius: '3px', 17 | display: 'inline-block', 18 | height: '20px', 19 | marginRight: '5px', 20 | width: '20px' 21 | } 22 | 23 | export default class TestSummary extends Component { 24 | render() { 25 | const { 26 | unitTests 27 | } = this.props 28 | 29 | return ( 30 |
    31 | { unitTests.map((unitTest, index) => 32 |
  • 41 | )} 42 |
43 | ) 44 | } 45 | } 46 | 47 | TestSummary.propTypes = { 48 | unitTests: PropTypes.arrayOf( 49 | PropTypes.shape({ 50 | result: PropTypes.string.isRequired 51 | }).isRequired 52 | ).isRequired 53 | } 54 | -------------------------------------------------------------------------------- /src/utils/slotSelection.js: -------------------------------------------------------------------------------- 1 | import { isNotEmpty } from './index' 2 | import { MAIN_BRICK } from './componentNames' 3 | 4 | export const selectedSlots = (workspace) => { 5 | const { input, output } = workspace.selectionState.pipe 6 | const selectedSlots = [input.slotId, output.slotId] 7 | 8 | return selectedSlots.filter(element => element) 9 | } 10 | 11 | export const isSlotSelected = (selectedSlots, slotId) => { 12 | return selectedSlots.indexOf(slotId) !== -1 13 | } 14 | 15 | export const areSlotsInSameElement = (pipeSelectionState) => { 16 | const { input, output } = pipeSelectionState 17 | 18 | return input.elementId && input.elementId === output.elementId 19 | } 20 | 21 | export const isPipeNotAllowedInsideElement = (workspace, elementId) => { 22 | return !workspace.entities[elementId].innerIds 23 | } 24 | 25 | export const bothSlotsSelected = (pipeSelectionState) => { 26 | const { input, output } = pipeSelectionState 27 | 28 | return isNotEmpty(input) && isNotEmpty(output) 29 | } 30 | 31 | export const outputElementHasPipe = (workspace, output) => { 32 | const element = workspace.entities[output.elementId] 33 | const slots = element.componentName == MAIN_BRICK ? element.outputSlots : element.inputSlots 34 | 35 | return !!slots[output.slotId].value 36 | } 37 | -------------------------------------------------------------------------------- /src/components/Module.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react' 2 | 3 | export default class Module extends Component { 4 | render() { 5 | const { 6 | functions, 7 | label, 8 | name, 9 | onFunctionClick 10 | } = this.props 11 | 12 | return ( 13 |
14 |

15 | { label } 16 |

17 |
    18 | { functions.map((libraryFunction) => { 19 | return ( 20 |
  • onFunctionClick(name, libraryFunction) } 23 | title={ libraryFunction.title } 24 | > 25 | { libraryFunction.label } 26 |
  • 27 | ) 28 | } 29 | )} 30 |
31 |
32 | ) 33 | } 34 | } 35 | 36 | Module.propTypes = { 37 | label: PropTypes.string.isRequired, 38 | name: PropTypes.string.isRequired, 39 | functions: PropTypes.arrayOf( 40 | PropTypes.shape({ 41 | arity: PropTypes.number.isRequired, 42 | id: PropTypes.number.isRequired, 43 | label: PropTypes.string.isRequired, 44 | name: PropTypes.string.isRequired 45 | }).isRequired 46 | ).isRequired, 47 | onFunctionClick: PropTypes.func.isRequired 48 | } 49 | -------------------------------------------------------------------------------- /src/components/TestResult.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react' 2 | import Rectangle from 'react-art/lib/Rectangle.art' 3 | 4 | import { getTestResultColor } from '../utils/unitTestUtils' 5 | import { PositionPropTypes, SizePropTypes } from '../propTypes' 6 | 7 | import { getConstant } from './constants' 8 | import { TEST_RESULT } from '../utils/componentNames' 9 | 10 | class TestResult extends Component { 11 | render() { 12 | const { 13 | mainBrick, 14 | unitTest 15 | } = this.props 16 | 17 | const componentName = TEST_RESULT 18 | const slotHeight = getConstant(componentName, 'slotHeight') 19 | const delta = slotHeight * 5 / 3 20 | 21 | return ( 22 | 29 | ) 30 | } 31 | } 32 | 33 | TestResult.propTypes = { 34 | mainBrick: PropTypes.shape({ 35 | position: PositionPropTypes.isRequired, 36 | size: SizePropTypes.isRequired 37 | }).isRequired, 38 | unitTest: PropTypes.shape({ 39 | result: PropTypes.string.isRequired 40 | }).isRequired 41 | } 42 | 43 | export default TestResult 44 | -------------------------------------------------------------------------------- /src/reducers/workspace/evaluationReducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | BRICK, 3 | MAIN_BRICK, 4 | PRIMITIVE, 5 | SELECTABLE_PIPE 6 | } from '../../utils/componentNames' 7 | 8 | import { 9 | elementInputValueIds, 10 | evalPathValueIds, 11 | tryEvalPath 12 | } from '../../utils/evalUtils' 13 | 14 | export const evaluateAllWorkspaces = (workspace, elementId) => { 15 | const newUnitTests = workspace.unitTests.map((unitTest) => { 16 | return tryEvalPath(workspace, unitTest, elementId) 17 | }) 18 | 19 | return Object.assign({}, workspace, { 20 | ...workspace, 21 | unitTests: newUnitTests 22 | }) 23 | } 24 | 25 | export const unevaluate = (workspace, elementId) => { 26 | let element = workspace.entities[elementId] 27 | 28 | if(element.componentName == SELECTABLE_PIPE) { 29 | element = workspace.entities[element.output.elementId] 30 | } 31 | 32 | const valueIds = evalPathValueIds(workspace, element, []) 33 | 34 | const newUnitTests = workspace.unitTests.map((unitTest) => { 35 | let newUnitTest = { values: {} } 36 | 37 | for(let id in unitTest.values) { 38 | if(valueIds.indexOf(parseInt(id)) == -1) { 39 | newUnitTest.values[id] = unitTest.values[id] 40 | } 41 | } 42 | 43 | return newUnitTest 44 | }) 45 | 46 | return Object.assign({}, workspace, { 47 | unitTests: newUnitTests 48 | }) 49 | } 50 | -------------------------------------------------------------------------------- /src/actions/requestLibraryActions.js: -------------------------------------------------------------------------------- 1 | import fetch from 'isomorphic-fetch' 2 | 3 | export const REQUEST_LIBRARY = 'REQUEST_LIBRARY' 4 | export const RECEIVE_LIBRARY = 'RECEIVE_LIBRARY' 5 | export const INVALIDATE_LIBRARY = 'INVALIDATE_LIBRARY' 6 | 7 | export const invalidateLibrary = () => { 8 | return { 9 | type: INVALIDATE_LIBRARY, 10 | } 11 | } 12 | 13 | const requestLibrary = () => { 14 | return { 15 | type: REQUEST_LIBRARY, 16 | } 17 | } 18 | 19 | const receiveLibrary = (items) => { 20 | return { 21 | type: RECEIVE_LIBRARY, 22 | payload: { 23 | items 24 | } 25 | } 26 | } 27 | 28 | const fetchLibrary = (id) => { 29 | return dispatch => { 30 | dispatch(requestLibrary()) 31 | return fetch(`library/js/${id}.json`) 32 | .then(response => response.json()) 33 | .then(items => dispatch(receiveLibrary(items))) 34 | } 35 | } 36 | 37 | const shouldFetchLibrary = (state) => { 38 | // const posts = state.postsByReddit[reddit] 39 | // if (!posts) { 40 | // return true 41 | // } 42 | // if (posts.isFetching) { 43 | // return false 44 | // } 45 | // return posts.didInvalidate 46 | return true 47 | } 48 | 49 | export const fetchLibraryIfNeeded = (id) => { 50 | return (dispatch, getState) => { 51 | const { library } = getState() 52 | 53 | if (shouldFetchLibrary(library)) { 54 | return dispatch(fetchLibrary(id)) 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/components/Pipe.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react' 2 | import { Group, Path, Shape } from 'react-art' 3 | 4 | import { getConstant } from './constants' 5 | import { PIPE } from '../utils/componentNames' 6 | import { PositionPropTypes } from '../propTypes' 7 | 8 | class Pipe extends Component { 9 | render() { 10 | const { 11 | inputPosition, 12 | fillColor, 13 | strokeColor, 14 | outputPosition, 15 | type 16 | } = this.props 17 | const componentName = PIPE 18 | const slotHeight = getConstant(componentName, 'slotHeight') 19 | const slotWidth = getConstant(componentName, 'slotWidth') 20 | 21 | const deltaPosition = { 22 | x: outputPosition.x - inputPosition.x, 23 | y: outputPosition.y - (inputPosition.y + slotHeight) 24 | } 25 | const path = Path() 26 | 27 | path.move(inputPosition.x, inputPosition.y + slotHeight) 28 | path.line(slotWidth, 0) 29 | path.line(deltaPosition.x, deltaPosition.y) 30 | path.line(-slotWidth, 0) 31 | path.close() 32 | 33 | return ( 34 | 39 | ) 40 | } 41 | } 42 | 43 | Pipe.propTypes = { 44 | fillColor: PropTypes.string.isRequired, 45 | inputPosition: PositionPropTypes.isRequired, 46 | outputPosition: PositionPropTypes.isRequired, 47 | strokeColor: PropTypes.string.isRequired 48 | } 49 | 50 | export default Pipe 51 | -------------------------------------------------------------------------------- /src/components/ElementDetails/PrimitiveDetails.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react' 2 | 3 | import { 4 | PrimitivePropTypes 5 | } from '../../propTypes' 6 | 7 | import CustomValueInput from './CustomValueInput' 8 | import DialogButton from './DialogButton' 9 | 10 | export default class PrimitiveDetails extends Component { 11 | render() { 12 | const { 13 | changePrimitiveValue, 14 | closeDialog, 15 | deleteElement, 16 | id, 17 | primitives, 18 | type, 19 | value, 20 | workspaceIndex 21 | } = this.props 22 | 23 | return ( 24 |
25 | 34 |
35 | deleteElement(id) } 37 | message="delete" 38 | /> 39 |
40 | ) 41 | } 42 | } 43 | 44 | PrimitiveDetails.propTypes = { 45 | deleteElement: PropTypes.func.isRequired, 46 | changePrimitiveValue: PropTypes.func.isRequired, 47 | closeDialog: PropTypes.func.isRequired, 48 | id: PropTypes.number.isRequired, 49 | primitives: PropTypes.objectOf(PrimitivePropTypes).isRequired, 50 | type: PropTypes.string.isRequired, 51 | value: PropTypes.string, 52 | workspaceIndex: PropTypes.number 53 | } 54 | -------------------------------------------------------------------------------- /src/components/WorkspaceSurface.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react' 2 | import { Surface } from 'react-art' 3 | 4 | import MainBrick from '../containers/MainBrick' 5 | import TestResult from '../components/TestResult' 6 | 7 | const workspaceSurfaceStyles = { 8 | marginRight: '10px' 9 | } 10 | 11 | const surfaceStyles = { 12 | backgroundColor: 'white' 13 | } 14 | 15 | class WorkspaceSurface extends Component { 16 | render() { 17 | const { 18 | index, 19 | mainBrick, 20 | selectedSlots, 21 | unitTest 22 | } = this.props 23 | 24 | return ( 25 |
28 | 33 | 37 | 43 | 44 |
45 | ) 46 | } 47 | } 48 | 49 | WorkspaceSurface.propTypes = { 50 | index: PropTypes.number.isRequired, 51 | mainBrick: PropTypes.object.isRequired, 52 | selectedSlots: PropTypes.arrayOf(PropTypes.number).isRequired, 53 | unitTest: PropTypes.object.isRequired 54 | } 55 | 56 | export default WorkspaceSurface 57 | -------------------------------------------------------------------------------- /src/components/Slot.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react' 2 | import Rectangle from 'react-art/lib/Rectangle.art' 3 | 4 | import { getConstant } from './constants' 5 | import { isSlotSelected } from '../utils' 6 | import { SLOT } from '../utils/componentNames' 7 | 8 | class Slot extends Component { 9 | render() { 10 | const { 11 | fillColor: unselectedFillColor, 12 | id, 13 | parentId, 14 | selectedSlots, 15 | selectSlot, 16 | strokeColor, 17 | x, 18 | y 19 | } = this.props 20 | const componentName = SLOT 21 | 22 | const fillColor = isSlotSelected(selectedSlots, id) ? 23 | getConstant(componentName, 'fillColor') : 24 | unselectedFillColor 25 | 26 | return ( 27 | selectSlot(parentId, id) } 34 | cursor={ getConstant(componentName, 'cursor') } 35 | fill={ fillColor } 36 | stroke={ strokeColor } 37 | /> 38 | ) 39 | } 40 | } 41 | 42 | Slot.propTypes = { 43 | fillColor: PropTypes.string.isRequired, 44 | id: PropTypes.number.isRequired, 45 | parentId: PropTypes.number.isRequired, 46 | selectedSlots: PropTypes.arrayOf(PropTypes.number).isRequired, 47 | selectSlot: PropTypes.func.isRequired, 48 | strokeColor: PropTypes.string.isRequired, 49 | x: PropTypes.number.isRequired, 50 | y: PropTypes.number.isRequired 51 | } 52 | 53 | export default Slot 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lambda-bricks", 3 | "version": "0.1.0", 4 | "description": "Visual Programming Environment frontend", 5 | "author": { 6 | "name": "Daniel Garcia Carmona", 7 | "email": "daniel.garcia.carmona@gmail.com" 8 | }, 9 | "license": "AGPL-3.0", 10 | "repository": { 11 | "type": "git", 12 | "url": "github.com/lambdabricks/bricks-front-react" 13 | }, 14 | "browserify": { 15 | "transform": [ 16 | [ 17 | "babelify", 18 | { 19 | "presets": [ 20 | "es2015", 21 | "react" 22 | ], 23 | "plugins": [ 24 | "transform-object-rest-spread" 25 | ] 26 | } 27 | ] 28 | ] 29 | }, 30 | "dependencies": { 31 | "isomorphic-fetch": "^2.1.1", 32 | "lodash.throttle": "^4.0.1", 33 | "query-string": "^4.1.0", 34 | "react": "^0.14.0", 35 | "react-art": "^0.14.0", 36 | "react-dom": "^0.14.0", 37 | "react-joyride": "^1.3.4", 38 | "react-redux": "^3.1.0", 39 | "redux": "^3.0.0", 40 | "redux-batched-subscribe": "^0.1.4", 41 | "redux-thunk": "^0.1.0" 42 | }, 43 | "devDependencies": { 44 | "babel-core": "^6.3.0", 45 | "babel-plugin-transform-object-rest-spread": "^6.3.13", 46 | "babel-preset-es2015": "^6.3.13", 47 | "babel-preset-react": "^6.3.13", 48 | "babelify": "^7.2.0", 49 | "browserify": "^13.0.0", 50 | "http-server": "^0.8.5", 51 | "watchify": "^3.3.1" 52 | }, 53 | "scripts": { 54 | "start": "watchify --extension=js -o docs/app.js src/index.js & watchify --extension=js -o docs/tutorial.js src/tutorial-index.js & http-server docs" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/components/ElementDetails/TypesSelect.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react' 2 | 3 | import { 4 | PrimitivePropTypes 5 | } from '../../propTypes' 6 | 7 | import Translate from '../Translate' 8 | 9 | export default class TypesSelect extends Component { 10 | render() { 11 | const { 12 | handleChange, 13 | id, 14 | primitives, 15 | type, 16 | workspaceIndex 17 | } = this.props 18 | 19 | return ( 20 |
21 | 25 | 50 |
51 | ) 52 | 53 | } 54 | } 55 | 56 | TypesSelect.propTypes = { 57 | handleChange: PropTypes.func.isRequired, 58 | id: PropTypes.number.isRequired, 59 | primitives: PropTypes.objectOf(PrimitivePropTypes).isRequired, 60 | type: PropTypes.string.isRequired, 61 | workspaceIndex: PropTypes.number 62 | } 63 | -------------------------------------------------------------------------------- /src/components/ElementDetails/TestNodeDetails.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react' 2 | 3 | import { 4 | PrimitivePropTypes 5 | } from '../../propTypes' 6 | 7 | import CustomValueInput from './CustomValueInput' 8 | import TypesSelect from './TypesSelect' 9 | 10 | export default class TestNodeDetails extends Component { 11 | render() { 12 | const { 13 | changeTestNodeType, 14 | changeTestNodeValue, 15 | closeDialog, 16 | id, 17 | primitives, 18 | value, 19 | type, 20 | workspaceIndex 21 | } = this.props 22 | 23 | return ( 24 |
25 | 32 | { type != "null" && 33 |
34 | 43 |
44 | } 45 |
46 | ) 47 | } 48 | } 49 | 50 | TestNodeDetails.propTypes = { 51 | changeTestNodeType: PropTypes.func.isRequired, 52 | changeTestNodeValue: PropTypes.func.isRequired, 53 | closeDialog: PropTypes.func.isRequired, 54 | id: PropTypes.number.isRequired, 55 | primitives: PropTypes.objectOf(PrimitivePropTypes).isRequired, 56 | value: PropTypes.string, 57 | type: PropTypes.string.isRequired, 58 | workspaceIndex: PropTypes.number 59 | } 60 | -------------------------------------------------------------------------------- /src/components/SelectedElementDialog.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react' 2 | 3 | import { 4 | getDetailsComponent 5 | } from './ElementDetails/ElementDetailsFactory' 6 | 7 | import { 8 | PositionPropTypes, 9 | PrimitivePropTypes 10 | } from '../propTypes' 11 | 12 | export default class SelectedElementDialog extends Component { 13 | render() { 14 | const { 15 | closeDialog, 16 | componentName, 17 | mousePosition 18 | } = this.props 19 | 20 | const ReactComponent = getDetailsComponent(componentName) 21 | 22 | return ( 23 |
32 | 37 | x 38 | 39 | 40 |
41 | ) 42 | } 43 | } 44 | 45 | SelectedElementDialog.propTypes = { 46 | addUnitTest: PropTypes.func.isRequired, 47 | changePrimitiveValue: PropTypes.func.isRequired, 48 | changeTestNodeType: PropTypes.func.isRequired, 49 | changeTestNodeValue: PropTypes.func.isRequired, 50 | closeDialog: PropTypes.func.isRequired, 51 | componentName: PropTypes.string.isRequired, 52 | deleteElement: PropTypes.func.isRequired, 53 | id: PropTypes.number.isRequired, 54 | mousePosition: PositionPropTypes.isRequired, 55 | primitives: PropTypes.objectOf(PrimitivePropTypes).isRequired, 56 | value: PropTypes.string, 57 | totalUnitTests: PropTypes.number.isRequired, 58 | type: PropTypes.string, 59 | workspaceIndex: PropTypes.number 60 | } 61 | -------------------------------------------------------------------------------- /src/components/SelectablePipe.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react' 2 | import { Group, Text } from 'react-art' 3 | 4 | import { getConstant } from './constants' 5 | import { BindingPropTypes, PositionPropTypes } from '../propTypes' 6 | import { getFillColor } from '../utils' 7 | import Pipe from './Pipe' 8 | 9 | class SelectablePipe extends Component { 10 | render() { 11 | const { 12 | componentName, 13 | binding, 14 | handleClick, 15 | id, 16 | inputPosition, 17 | outputPosition 18 | } = this.props 19 | 20 | const fillColor = getFillColor(binding.type, binding.value) 21 | 22 | return ( 23 | handleClick(id, e) } 25 | > 26 | 32 | { binding.value && 33 | 40 | { binding.value } 41 | 42 | } 43 | 44 | ) 45 | } 46 | } 47 | 48 | SelectablePipe.propTypes = { 49 | componentName: PropTypes.string.isRequired, 50 | binding: BindingPropTypes.isRequired, 51 | handleClick: PropTypes.func.isRequired, 52 | id: PropTypes.number.isRequired, 53 | inputPosition: PositionPropTypes.isRequired, 54 | outputPosition: PositionPropTypes.isRequired 55 | } 56 | 57 | export default SelectablePipe 58 | -------------------------------------------------------------------------------- /docs/library/js/4.json: -------------------------------------------------------------------------------- 1 | { 2 | "modules": 3 | [ 4 | { 5 | "label": "Arithmetic", 6 | "name": "arithmetic", 7 | "functions": 8 | [ 9 | { "id" : 41, "label": "+", "name" : "+", "arity" : 2, "title" : "Inputs:\n • number\n • number\nOutput: number" }, 10 | { "id" : 42, "label": "-", "name" : "-", "arity" : 2, "title" : "Inputs:\n • number\n • number\nOutput: number" }, 11 | { "id" : 43, "label": "*", "name" : "*", "arity" : 2, "title" : "Inputs:\n • number\n • number\nOutput: number" }, 12 | { "id" : 44, "label": "/", "name" : "/", "arity" : 2, "title" : "Inputs:\n • number\n • number\nOutput: number" } 13 | ] 14 | }, 15 | { 16 | "label": "Convertion", 17 | "name": "parse", 18 | "functions": 19 | [ 20 | { "id" : 51, "label": "toNumber", "name" : "toNumber", "arity" : 1, "title" : "Input: string\nOutput: number" }, 21 | { "id" : 52, "label": "toString", "name" : "toString", "arity" : 1, "title" : "Input: number\nOutput: string" } 22 | ] 23 | }, 24 | { 25 | "label": "String", 26 | "name": "string", 27 | "functions": 28 | [ 29 | { "id" : 21, "label": "concat", "name" : "concat", "arity" : 2, "title" : "Inputs:\n • string\n • string\nOutput: string" }, 30 | { "id" : 22, "label": "length", "name" : "length", "arity" : 1, "title" : "Input: string\nOutput: number" }, 31 | { "id" : 23, "label": "reverse", "name" : "reverse", "arity" : 1, "title" : "Input: string\nOutput: string" } 32 | ] 33 | } 34 | ], 35 | "primitives": 36 | { 37 | "number": 38 | { 39 | "id": 102, 40 | "label": "Number", 41 | "type": "number" 42 | }, 43 | "string": 44 | { 45 | "id": 103, 46 | "label": "String", 47 | "type": "string" 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/containers/SelectedElementDialog.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | 3 | import { 4 | addUnitTest, 5 | changePrimitiveValue, 6 | changeTestNodeType, 7 | changeTestNodeValue, 8 | removeElement, 9 | removeSelectedElement, 10 | removeUnitTest 11 | } from '../actions' 12 | import SelectedElementDialog from '../components/SelectedElementDialog' 13 | 14 | const mapStateToProps = (state, ownProps) => { 15 | const { entities, unitTests } = state.workspace 16 | const { id, workspaceIndex } = ownProps 17 | const { primitives } = state.library.items 18 | 19 | return { 20 | ...entities[id], 21 | ...unitTests[workspaceIndex || 0].values[id], 22 | primitives, 23 | totalUnitTests: unitTests.length 24 | } 25 | } 26 | 27 | const mapDispatchToProps = (dispatch) => { 28 | return { 29 | addUnitTest: () => { 30 | dispatch(addUnitTest()) 31 | }, 32 | deleteElement: (elementId) => { 33 | dispatch(removeElement(elementId)) 34 | }, 35 | deleteUnitTest: (workspaceIndex) => { 36 | dispatch(removeUnitTest(workspaceIndex)) 37 | }, 38 | changePrimitiveValue: (elementId, changeEvent, workspaceIndex) => { 39 | dispatch( 40 | changePrimitiveValue(elementId, changeEvent.target.value, workspaceIndex) 41 | ) 42 | }, 43 | changeTestNodeType: (elementId, changeEvent, workspaceIndex) => { 44 | dispatch( 45 | changeTestNodeType(elementId, changeEvent.target.value, workspaceIndex) 46 | ) 47 | }, 48 | changeTestNodeValue: (elementId, changeEvent, workspaceIndex) => { 49 | dispatch( 50 | changeTestNodeValue(elementId, changeEvent.target.value, workspaceIndex) 51 | ) 52 | }, 53 | closeDialog: () => { 54 | dispatch(removeSelectedElement()) 55 | } 56 | } 57 | } 58 | 59 | export default connect(mapStateToProps, mapDispatchToProps)(SelectedElementDialog) 60 | -------------------------------------------------------------------------------- /docs/library/js/1.json: -------------------------------------------------------------------------------- 1 | { 2 | "modules": 3 | [ 4 | { 5 | "label": "Arithmetic", 6 | "name": "arithmetic", 7 | "functions": 8 | [ 9 | { "id" : 41, "label": "+", "name" : "+", "arity" : 2, "title" : "+(term1: number, term2: number) : number" }, 10 | { "id" : 42, "label": "-", "name" : "-", "arity" : 2 }, 11 | { "id" : 43, "label": "*", "name" : "*", "arity" : 2 }, 12 | { "id" : 44, "label": "/", "name" : "/", "arity" : 2 } 13 | ] 14 | }, 15 | { 16 | "label": "Boolean Logic", 17 | "name": "boolean", 18 | "functions": 19 | [ 20 | { "id" : 31, "label": "&&", "name" : "&&", "arity" : 2 }, 21 | { "id" : 32, "label": "||", "name" : "||", "arity" : 2 }, 22 | { "id" : 33, "label": "!", "name" : "!", "arity" : 1 } 23 | ] 24 | }, 25 | { 26 | "label": "Collection", 27 | "name": "hof", 28 | "functions": 29 | [ 30 | { "id" : 5, "label": "join", "name" : "join", "arity" : 1 }, 31 | { "id" : 4, "label": "sort", "name" : "sort", "arity" : 1 } 32 | ] 33 | }, 34 | { 35 | "label": "Comparison", 36 | "name": "eq", 37 | "functions": 38 | [ 39 | { "id": 11, "label": "==", "name": "==", "arity" : 2 } 40 | ] 41 | }, 42 | { 43 | "label": "String", 44 | "name": "string", 45 | "functions": 46 | [ 47 | { "id" : 21, "label": "concat", "name" : "concat", "arity" : 2 }, 48 | { "id" : 22, "label": "length", "name" : "length", "arity" : 1 }, 49 | { "id" : 23, "label": "reverse", "name" : "reverse", "arity" : 1 }, 50 | { "id" : 24, "label": "split", "name" : "split", "arity" : 1 } 51 | ] 52 | } 53 | ], 54 | "primitives": 55 | { 56 | "boolean": 57 | { 58 | "id": 101, 59 | "label": "Boolean", 60 | "type": "boolean", 61 | "values": [ 62 | { "label": "false", "name": "false" }, 63 | { "label": "true", "name": "true" } 64 | ] 65 | }, 66 | "number": 67 | { 68 | "id": 102, 69 | "label": "Number", 70 | "type": "number" 71 | }, 72 | "string": 73 | { 74 | "id": 103, 75 | "label": "String", 76 | "type": "string" 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/reducers/workspace/selectionReducer.js: -------------------------------------------------------------------------------- 1 | import * as Utils from '../../utils' 2 | 3 | export const updateSlotSelectionStateInWorkspace = (state, payload) => { 4 | const { elementId, slotId, type } = payload 5 | 6 | const selectedSlots = Utils.selectedSlots(state) 7 | let pipeSelectionState = Object.assign({}, state.selectionState.pipe) 8 | 9 | // Toggle selection, deselect slot if it is already selected. 10 | if(!Utils.isSlotSelected(selectedSlots, slotId)) { 11 | pipeSelectionState[type] = { 12 | elementId, 13 | slotId 14 | } 15 | } else { 16 | pipeSelectionState[type] = { } 17 | } 18 | 19 | // Only allow pipes inside mainBrick element 20 | if((Utils.areSlotsInSameElement(pipeSelectionState) && 21 | Utils.isPipeNotAllowedInsideElement(state, elementId)) || 22 | (Utils.bothSlotsSelected(pipeSelectionState) && 23 | Utils.outputElementHasPipe(state, pipeSelectionState.output)) 24 | ) { 25 | pipeSelectionState = { 26 | input: { }, 27 | output: { } 28 | } 29 | 30 | pipeSelectionState[type] = { 31 | elementId, 32 | slotId 33 | } 34 | } 35 | 36 | return Object.assign({}, state, { 37 | selectionState: { 38 | ...state.selectionState, 39 | pipe: pipeSelectionState 40 | } 41 | }) 42 | } 43 | 44 | export const removeSlotSelectionState = (state) => { 45 | return Object.assign({}, state, { 46 | selectionState: { 47 | ...state.selectionState, 48 | pipe: { 49 | input: { }, 50 | output: { } 51 | } 52 | } 53 | }) 54 | } 55 | 56 | export const addSelectedElementToWorkspace = (state, payload) => { 57 | const { 58 | elementId, 59 | mousePosition, 60 | workspaceIndex 61 | } = payload 62 | 63 | return Object.assign({}, state, { 64 | selectionState: { 65 | ...state.selectionState, 66 | element: { 67 | id: elementId, 68 | mousePosition, 69 | workspaceIndex 70 | } 71 | } 72 | }) 73 | } 74 | 75 | export const removeSelectedElementFromWorkspace = (state) => { 76 | return Object.assign({}, state, { 77 | selectionState: { 78 | ...state.selectionState, 79 | element: { } 80 | } 81 | }) 82 | } 83 | -------------------------------------------------------------------------------- /src/components/SlotGroup.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react' 2 | import { Group } from 'react-art' 3 | import Rectangle from 'react-art/lib/Rectangle.art' 4 | 5 | import { getConstant } from './constants' 6 | import { SlotPropTypes } from '../propTypes' 7 | 8 | import Slot from './Slot' 9 | 10 | class SlotGroup extends Component { 11 | render() { 12 | const { 13 | componentName, 14 | parentId, 15 | parentWidth, 16 | selectedSlots, 17 | selectSlot, 18 | slots, 19 | y 20 | } = this.props 21 | 22 | const slotOffset = getConstant(componentName, 'slotOffset') 23 | const slotAndOffset = getConstant(componentName, 'slotAndOffset') 24 | 25 | const width = this.slotGroupWidth(slots, slotOffset, slotAndOffset) 26 | const xOffset = (parentWidth - width) / 2 27 | 28 | return ( 29 | 30 | { Object.keys(slots).map((slotId, index) => { 31 | const slot = slots[slotId] 32 | const x = slotOffset + (slot.index * slotAndOffset) 33 | 34 | return ( 35 | 46 | ) 47 | }) 48 | } 49 | 50 | ) 51 | } 52 | 53 | slotGroupWidth(slots, slotOffset, slotAndOffset) { 54 | const totalSlots = Object.keys(slots).length 55 | 56 | return slotOffset + (totalSlots * slotAndOffset) 57 | } 58 | } 59 | 60 | SlotGroup.propTypes = { 61 | componentName: PropTypes.string.isRequired, 62 | parentId: PropTypes.number.isRequired, 63 | parentWidth: PropTypes.number.isRequired, 64 | selectedSlots: PropTypes.arrayOf(PropTypes.number).isRequired, 65 | selectSlot: PropTypes.func.isRequired, 66 | slots: SlotPropTypes.isRequired, 67 | y: PropTypes.number.isRequired 68 | } 69 | 70 | export default SlotGroup 71 | -------------------------------------------------------------------------------- /src/components/composeBrick.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react' 2 | import { Group } from 'react-art' 3 | import Rectangle from 'react-art/lib/Rectangle.art' 4 | 5 | import { getConstant } from './constants' 6 | import { PositionPropTypes, SizePropTypes, SlotPropTypes } from '../propTypes' 7 | 8 | import SlotGroup from './SlotGroup' 9 | 10 | export default function composeBrick(InnerComponent) { 11 | class AbstractBrick extends Component { 12 | render() { 13 | const { 14 | componentName, 15 | id, 16 | inputSlots, 17 | outputSlots, 18 | position, 19 | selectBrickInputSlot, 20 | selectBrickOutputSlot, 21 | selectedSlots, 22 | size 23 | } = this.props 24 | const slotHeight = getConstant(componentName, 'slotHeight') 25 | 26 | return ( 27 | 28 | 29 | 38 | 47 | 48 | ) 49 | } 50 | } 51 | 52 | AbstractBrick.propTypes = { 53 | componentName: PropTypes.string.isRequired, 54 | inputSlots: SlotPropTypes.isRequired, 55 | outputSlots: SlotPropTypes.isRequired, 56 | position: PositionPropTypes.isRequired, 57 | selectBrickInputSlot: PropTypes.func.isRequired, 58 | selectBrickOutputSlot: PropTypes.func.isRequired, 59 | selectedSlots: PropTypes.arrayOf(PropTypes.number).isRequired, 60 | size: SizePropTypes.isRequired 61 | } 62 | 63 | AbstractBrick.displayName = InnerComponent.name 64 | 65 | return AbstractBrick 66 | } 67 | -------------------------------------------------------------------------------- /src/reducers/workspace/dragReducer.js: -------------------------------------------------------------------------------- 1 | import { bound } from '../../utils' 2 | import { getConstant } from '../../components/constants' 3 | 4 | export const updateElementInWorkspace = (workspace, payload) => { 5 | const { selectionState } = workspace 6 | 7 | if(!selectionState.dragStarted) { 8 | return workspace 9 | } 10 | 11 | const { currentMousePosition } = payload 12 | const { element } = selectionState 13 | const originalElement = workspace.entities[element.id] 14 | const parentElement = workspace.entities[originalElement.parentId] 15 | const margin = getConstant(parentElement.componentName, 'margin') 16 | 17 | const x = element.startPosition.x + currentMousePosition.x - element.mouseDownPosition.x 18 | const y = element.startPosition.y + currentMousePosition.y - element.mouseDownPosition.y 19 | 20 | return Object.assign({}, workspace, { 21 | ...workspace, 22 | entities: { 23 | ...workspace.entities, 24 | [element.id]: { 25 | ...originalElement, 26 | position: { 27 | x: bound( 28 | x, 29 | margin, 30 | parentElement.size.width - originalElement.size.width - margin 31 | ), 32 | y: bound( 33 | y, 34 | margin, 35 | parentElement.size.height - originalElement.size.height - margin 36 | ), 37 | } 38 | } 39 | } 40 | }) 41 | } 42 | 43 | export const addDragStartedToWorkspace = (workspace, payload) => { 44 | const { 45 | elementId, 46 | elementPosition, 47 | mousePosition 48 | } = payload 49 | 50 | return setDragStateToWorkspace( 51 | workspace, 52 | { 53 | dragStarted: true, 54 | element: { 55 | id: elementId, 56 | mouseDownPosition: mousePosition, 57 | startPosition: elementPosition 58 | } 59 | } 60 | ) 61 | } 62 | 63 | export const addDragStoppedToWorkspace = (workspace, payload) => { 64 | return setDragStateToWorkspace( 65 | workspace, 66 | { 67 | dragStarted: false 68 | } 69 | ) 70 | } 71 | 72 | export const setDragStateToWorkspace = (workspace, dragState) => { 73 | return Object.assign({}, workspace, { 74 | ...workspace, 75 | selectionState: { 76 | ...workspace.selectionState, 77 | ...dragState 78 | } 79 | }) 80 | } 81 | -------------------------------------------------------------------------------- /src/containers/Workspace.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | 3 | import { 4 | initWorkspace, 5 | moveElement, 6 | selectElementOrStopDrag 7 | } from '../actions' 8 | 9 | import { 10 | SELECTABLE_PIPE 11 | } from '../utils/componentNames' 12 | 13 | import { 14 | inputSlotPosition, 15 | outputSlotPosition, 16 | selectedSlots 17 | } from '../utils' 18 | 19 | import { 20 | getTestResult 21 | } from '../utils/unitTestUtils' 22 | 23 | import Workspace from '../components/Workspace' 24 | 25 | const mapStateToProps = (state) => { 26 | const { entities, selectionState } = state.workspace 27 | const mainBrick = entities[state.workspace.mainBrickId] 28 | 29 | let selectedElement = {} 30 | 31 | if(!selectionState.dragStarted) 32 | selectedElement = selectionState.element 33 | 34 | return { 35 | dragStarted: selectionState.dragStarted, 36 | mainBrick: { 37 | ...mainBrick, 38 | inner: mainBrick.innerIds.map((elementId) => { 39 | const element = entities[elementId] 40 | 41 | if(element.componentName == SELECTABLE_PIPE) { 42 | return { 43 | ...element, 44 | inputPosition: inputSlotPosition( 45 | entities[element.input.elementId], 46 | element.input.slotId 47 | ), 48 | outputPosition: outputSlotPosition( 49 | entities[element.output.elementId], 50 | element.output.slotId 51 | ) 52 | } 53 | } else { 54 | return element 55 | } 56 | }), 57 | testInputs: mainBrick.testInputIds.map((elementId) => entities[elementId]), 58 | testOutputs: mainBrick.testOutputIds.map((elementId) => entities[elementId]) 59 | }, 60 | selectedElement, 61 | selectedSlots: selectedSlots(state.workspace), 62 | unitTests: state.workspace.unitTests.map((unitTest) => { 63 | return { 64 | ...unitTest, 65 | result: getTestResult(mainBrick, unitTest) 66 | } 67 | }) 68 | } 69 | } 70 | 71 | const mapDispatchToProps = (dispatch) => { 72 | return { 73 | initWorkspace: (type) => { 74 | dispatch(initWorkspace(type)) 75 | }, 76 | moveElement: (position) => { 77 | dispatch(moveElement(position)) 78 | }, 79 | selectElementOrStopDrag: (mousePosition) => { 80 | dispatch(selectElementOrStopDrag(mousePosition)) 81 | } 82 | } 83 | } 84 | 85 | export default connect(mapStateToProps, mapDispatchToProps)(Workspace) 86 | -------------------------------------------------------------------------------- /src/components/TestInput.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react' 2 | import { Group, Text } from 'react-art' 3 | 4 | import { getConstant } from './constants' 5 | import Ellipse from './Ellipse' 6 | import { getFillColor } from '../utils' 7 | import Pipe from './Pipe' 8 | import { BindingPropTypes, PositionPropTypes } from '../propTypes' 9 | 10 | class TestInput extends Component { 11 | render() { 12 | const { 13 | componentName, 14 | binding, 15 | handleClick, 16 | id, 17 | slotPosition, 18 | size, 19 | workspaceIndex 20 | } = this.props 21 | const slotWidth = getConstant(componentName, 'slotWidth') 22 | const slotHeight = getConstant(componentName, 'slotHeight') 23 | 24 | const fillColor = getFillColor(binding.type, binding.value) 25 | const position = { 26 | x: slotPosition.x - ((size.width - slotWidth) / 2), 27 | y: slotPosition.y - getConstant(componentName, 'yOffset') 28 | } 29 | const inputPipePosition = { 30 | x: (size.width - slotWidth) / 2, 31 | y: size.height - slotHeight 32 | } 33 | const outputPipePosition = { 34 | x: (size.width - slotWidth) / 2, 35 | y: getConstant(componentName, 'yOffset') 36 | } 37 | 38 | return ( 39 | handleClick(id, e, workspaceIndex) } 41 | x={ position.x } 42 | y={ position.y } 43 | > 44 | 48 | 55 | { binding.value === undefined ? "" : binding.value } 56 | 57 | 63 | 64 | ) 65 | } 66 | } 67 | 68 | TestInput.propTypes = { 69 | componentName: PropTypes.string.isRequired, 70 | binding: BindingPropTypes, 71 | handleClick: PropTypes.func.isRequired, 72 | id: PropTypes.number.isRequired, 73 | slotPosition: PositionPropTypes.isRequired, 74 | workspaceIndex: PropTypes.number.isRequired 75 | } 76 | 77 | export default TestInput 78 | -------------------------------------------------------------------------------- /src/utils/slotPosition.js: -------------------------------------------------------------------------------- 1 | import { getConstant } from '../components/constants' 2 | 3 | import { 4 | BRICK, 5 | MAIN_BRICK, 6 | PRIMITIVE 7 | } from './componentNames' 8 | 9 | export const inputSlotPosition = (element, slotId) => { 10 | const { componentName, position, size } = element 11 | const slotHeight = getConstant(componentName, 'slotHeight') 12 | 13 | switch(componentName) { 14 | case BRICK: 15 | const { outputSlots } = element 16 | 17 | return { 18 | x: position.x + 19 | slotXPosition(outputSlots, slotId, size.width, componentName), 20 | y: position.y + size.height + slotHeight 21 | } 22 | case MAIN_BRICK: 23 | const { inputSlots } = element 24 | 25 | return { 26 | x: slotXPosition(inputSlots, slotId, size.width, componentName), 27 | y: -slotHeight 28 | } 29 | case PRIMITIVE: 30 | const slotPosition = centeredSlotPosition(size, componentName) 31 | 32 | return { 33 | x: position.x + slotPosition.x, 34 | y: position.y + slotPosition.y 35 | } 36 | } 37 | } 38 | 39 | export const outputSlotPosition = (element, slotId) => { 40 | const { componentName, position, size } = element 41 | 42 | switch(componentName) { 43 | case BRICK: 44 | const { inputSlots } = element 45 | return { 46 | x: position.x + 47 | slotXPosition(inputSlots, slotId, size.width, componentName), 48 | y: position.y 49 | } 50 | case MAIN_BRICK: 51 | const { outputSlots } = element 52 | 53 | return { 54 | x: slotXPosition(outputSlots, slotId, size.width, componentName), 55 | y: size.height 56 | } 57 | } 58 | } 59 | 60 | const slotXPosition = (slots, slotId, width, componentName) => { 61 | const slot = slots[slotId] 62 | const xOffset = (width - slotGroupWidth(slots, componentName)) / 2 63 | 64 | return xOffset + 65 | (getConstant(componentName, 'slotOffset') + 66 | (slot.index * getConstant(componentName, 'slotAndOffset')) 67 | ) 68 | } 69 | 70 | const slotGroupWidth = (slots, componentName) => { 71 | const totalSlots = Object.keys(slots).length 72 | 73 | return getConstant(componentName, 'slotOffset') + 74 | (totalSlots * getConstant(componentName, 'slotAndOffset')) 75 | } 76 | 77 | export const centeredSlotPosition = (size, componentName) => { 78 | const slotWidth = getConstant(componentName, 'slotWidth') 79 | 80 | return { 81 | x: (size.width - slotWidth) / 2, 82 | y: size.height 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/components/TestOutput.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react' 2 | import { Group, Text } from 'react-art' 3 | 4 | import { getConstant } from './constants' 5 | import Ellipse from './Ellipse' 6 | import { getFillColor } from '../utils' 7 | import Pipe from './Pipe' 8 | import { BindingPropTypes, PositionPropTypes } from '../propTypes' 9 | 10 | class TestOutput extends Component { 11 | render() { 12 | const { 13 | componentName, 14 | binding, 15 | handleClick, 16 | id, 17 | slotPosition, 18 | size, 19 | workspaceIndex 20 | } = this.props 21 | 22 | const fillColor = getFillColor(binding.type, binding.value) 23 | const position = { 24 | x: - ((size.width - getConstant(componentName, 'slotWidth')) / 2), 25 | y: getConstant(componentName, 'slotHeight') + 26 | getConstant(componentName, 'yOffset') - size.height 27 | } 28 | const inputPipePosition = { 29 | x: 0, 30 | y: 0 31 | } 32 | const outputPipePosition = { 33 | x: 0, 34 | y: getConstant(componentName, 'yOffset') - (size.height / 2) 35 | } 36 | 37 | return ( 38 | handleClick(id, e, workspaceIndex) } 40 | x={ slotPosition.x } 41 | y={ slotPosition.y } 42 | > 43 | 47 | 51 | 58 | { binding.value === undefined ? "" : binding.value } 59 | 60 | 61 | 67 | 68 | ) 69 | } 70 | } 71 | 72 | TestOutput.propTypes = { 73 | componentName: PropTypes.string.isRequired, 74 | binding: BindingPropTypes, 75 | handleClick: PropTypes.func.isRequired, 76 | id: PropTypes.number.isRequired, 77 | slotPosition: PositionPropTypes.isRequired, 78 | workspaceIndex: PropTypes.number.isRequired 79 | } 80 | 81 | export default TestOutput 82 | -------------------------------------------------------------------------------- /src/components/Library.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react' 2 | 3 | import Module from './Module' 4 | import { PrimitivePropTypes } from '../propTypes' 5 | import Translate from './Translate' 6 | 7 | export default class Library extends Component { 8 | componentDidMount() { 9 | const { fetchLibrary, id } = this.props 10 | fetchLibrary(id) 11 | } 12 | 13 | render() { 14 | const { 15 | isFetching, 16 | items, 17 | onFunctionClick, 18 | onPrimitiveClick 19 | } = this.props 20 | 21 | return ( 22 | 72 | ) 73 | } 74 | } 75 | 76 | Library.propTypes = { 77 | fetchLibrary: PropTypes.func.isRequired, 78 | id: PropTypes.string.isRequired, 79 | isFetching: PropTypes.bool.isRequired, 80 | items: PropTypes.shape({ 81 | modules: PropTypes.array, 82 | primitives: PropTypes.objectOf(PrimitivePropTypes) 83 | }).isRequired, 84 | onFunctionClick: PropTypes.func.isRequired, 85 | onPrimitiveClick: PropTypes.func.isRequired 86 | } 87 | -------------------------------------------------------------------------------- /src/components/Primitive.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react' 2 | import { Group, Text } from 'react-art' 3 | 4 | import { getConstant } from './constants' 5 | import { getFillColor, centeredSlotPosition } from '../utils' 6 | import { BindingPropTypes, PositionPropTypes } from '../propTypes' 7 | 8 | import Ellipse from './Ellipse' 9 | import Slot from './Slot' 10 | 11 | class Primitive extends Component { 12 | constructor(props) { 13 | super(props) 14 | 15 | this.startDrag = this.startDrag.bind(this) 16 | } 17 | 18 | startDrag(mouseEvent) { 19 | const { handleMouseDown, id, position } = this.props 20 | 21 | handleMouseDown(id, mouseEvent, position) 22 | } 23 | 24 | render() { 25 | const { 26 | componentName, 27 | binding, 28 | id, 29 | handleMouseDown, 30 | position, 31 | selectedSlots, 32 | selectSlot, 33 | size, 34 | } = this.props 35 | 36 | const fillColor = getFillColor(binding.type, binding.value) 37 | const slotPosition = centeredSlotPosition(size, componentName) 38 | 39 | return ( 40 | 41 | 44 | 48 | 55 | { binding.value === undefined ? "" : binding.value } 56 | 57 | 58 | 69 | 70 | ) 71 | } 72 | } 73 | 74 | Primitive.propTypes = { 75 | componentName: PropTypes.string.isRequired, 76 | binding: BindingPropTypes.isRequired, 77 | id: PropTypes.number.isRequired, 78 | handleMouseDown: PropTypes.func.isRequired, 79 | position: PositionPropTypes.isRequired, 80 | selectedSlots: PropTypes.arrayOf(PropTypes.number).isRequired, 81 | selectSlot: PropTypes.func.isRequired 82 | } 83 | 84 | Primitive.displayName = Primitive.name 85 | 86 | export default Primitive 87 | -------------------------------------------------------------------------------- /src/components/constants.js: -------------------------------------------------------------------------------- 1 | import { 2 | BRICK, 3 | MAIN_BRICK, 4 | PIPE, 5 | PRIMITIVE, 6 | SELECTABLE_PIPE, 7 | SLOT, 8 | SURFACE, 9 | TEST_INPUT, 10 | TEST_OUTPUT, 11 | TEST_RESULT 12 | } from '../utils/componentNames' 13 | 14 | import { ERROR } from '../utils/evalUtils' 15 | 16 | export const colors = { 17 | boolean: "#8A2BE2", 18 | [ERROR]: '#D80000', 19 | false: "#E44B37", 20 | list: "#E96859", 21 | null: "#BFBFBF", 22 | number: "#E9E759", 23 | string: "#E9A559", 24 | true: "#358DE2", 25 | undefined: "#d8d8d8" 26 | } 27 | 28 | export const testColors = { 29 | failing: '#d87777', 30 | passing: '#77d877', 31 | pending: 'white' 32 | } 33 | 34 | export const LEFT = 0 35 | 36 | export const CLEAN = '1' 37 | export const FUNCTION = '2' 38 | export const UNIT_TEST = '3' 39 | 40 | const brickSlotOffset = 20 41 | const font = '15px monospace' 42 | const mainBrickSlotOffset = 100 43 | const slotHeight = 15 44 | const slotWidth = slotHeight 45 | 46 | const TestNode = { 47 | alignment: 'center', 48 | font, 49 | slotHeight: slotHeight, 50 | slotWidth: slotWidth, 51 | textColor: 'black', 52 | yOffset: 60 53 | } 54 | 55 | const Constants = { 56 | [BRICK]: { 57 | alignment: 'center', 58 | fillColor: '#ADD8E6', 59 | font: font, 60 | outputFont: 'bold ' + font, 61 | slotAndOffset: brickSlotOffset + slotWidth, 62 | slotHeight: slotHeight, 63 | slotOffset: brickSlotOffset, 64 | slotWidth: slotWidth, 65 | strokeColor: 'black', 66 | textColor: 'black' 67 | }, 68 | [MAIN_BRICK]: { 69 | fillColor: 'rgba(119, 136, 153, 0.7)', 70 | hoverFillColor: '#385661', 71 | margin: 20, 72 | slotAndOffset: mainBrickSlotOffset + slotWidth, 73 | slotHeight: slotHeight, 74 | slotOffset: mainBrickSlotOffset, 75 | slotWidth: slotWidth, 76 | strokeColor: 'black' 77 | }, 78 | [PIPE]: { 79 | slotHeight: slotHeight, 80 | slotWidth: slotWidth 81 | }, 82 | [PRIMITIVE]: { 83 | alignment: 'center', 84 | font: font, 85 | slotWidth: slotWidth, 86 | textColor: 'black' 87 | }, 88 | [SELECTABLE_PIPE]: { 89 | alignment: 'center', 90 | font: font, 91 | strokeColor: 'black', 92 | textColor: 'black' 93 | }, 94 | [SLOT]: { 95 | cursor: 'pointer', 96 | height: 15, 97 | width: slotWidth, 98 | fillColor: '#00FA9A' 99 | }, 100 | [TEST_INPUT]: TestNode, 101 | [TEST_OUTPUT]: TestNode, 102 | [TEST_RESULT]: { 103 | slotHeight: slotHeight 104 | } 105 | } 106 | 107 | export const getConstant = (componentName, key) => { 108 | return Constants[componentName][key] 109 | } 110 | -------------------------------------------------------------------------------- /src/components/Brick.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react' 2 | import { Group, Text } from 'react-art' 3 | import Rectangle from 'react-art/lib/Rectangle.art' 4 | 5 | import composeBrick from './composeBrick' 6 | import { getConstant } from './constants' 7 | import { getFillColor } from '../utils' 8 | import { 9 | BindingPropTypes, 10 | PositionPropTypes, 11 | SizePropTypes, 12 | SlotPropTypes 13 | } from '../propTypes' 14 | 15 | import { ERROR } from '../utils/evalUtils' 16 | 17 | class Brick extends Component { 18 | constructor(props) { 19 | super(props) 20 | 21 | this.startDrag = this.startDrag.bind(this) 22 | } 23 | 24 | startDrag(mouseEvent) { 25 | const { handleMouseDown, id, position } = this.props 26 | 27 | handleMouseDown(id, mouseEvent, position) 28 | } 29 | 30 | render() { 31 | const { 32 | componentName, 33 | binding, 34 | name, 35 | outputSlots, 36 | size 37 | } = this.props 38 | 39 | const midHeight = size.height / 2 - 7 40 | const outputSlotId = Object.keys(outputSlots)[0] 41 | const { outputElementIds } = outputSlots[outputSlotId] 42 | const slotHeight = getConstant(componentName, 'slotHeight') 43 | const slotWidth = getConstant(componentName, 'slotWidth') 44 | 45 | return ( 46 | 50 | 56 | 63 | { name } 64 | 65 | { outputElementIds.length == 0 && binding.type && 66 | 72 | { binding.value } 73 | 74 | } 75 | 76 | ) 77 | } 78 | } 79 | 80 | Brick.propTypes = { 81 | componentName: PropTypes.string.isRequired, 82 | binding: BindingPropTypes, 83 | id: PropTypes.number.isRequired, 84 | name: PropTypes.string.isRequired, 85 | position: PositionPropTypes.isRequired, 86 | outputSlots: SlotPropTypes.isRequired, 87 | size: SizePropTypes.isRequired 88 | } 89 | 90 | export default composeBrick(Brick) 91 | -------------------------------------------------------------------------------- /src/components/ElementDetails/CustomValueInput.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react' 2 | import ReactDOM from 'react-dom' 3 | 4 | import { 5 | PrimitivePropTypes 6 | } from '../../propTypes' 7 | 8 | import { colors } from '../constants' 9 | import Translate from '../Translate' 10 | 11 | export default class CustomValueInput extends Component { 12 | constructor(props) { 13 | super(props) 14 | 15 | this.closeOnEnterKeyDown = this.closeOnEnterKeyDown.bind(this) 16 | this.customInput = this.customInput.bind(this) 17 | } 18 | 19 | componentDidMount() { 20 | if(this.refs.valueInput) { 21 | ReactDOM.findDOMNode(this.refs.valueInput).focus() 22 | } 23 | } 24 | 25 | closeOnEnterKeyDown(e) { 26 | if(e.key == 'Enter') { 27 | this.props.closeDialog() 28 | } 29 | } 30 | 31 | customInput() { 32 | const { 33 | handleChange, 34 | id, 35 | primitives, 36 | type, 37 | value, 38 | workspaceIndex 39 | } = this.props 40 | const primitive = primitives[type] 41 | 42 | if(primitive && primitive.values) { 43 | return ( 44 | 45 | { primitive.values.map((primitiveValue) => 46 | 47 | handleChange(id, e, workspaceIndex) } 49 | id={ primitiveValue.name } 50 | name="inputWithOptions" 51 | type="radio" 52 | value={ primitiveValue.name } 53 | checked={ primitiveValue.name == value } 54 | /> 55 | 61 | 62 | )} 63 | 64 | ) 65 | } else { 66 | const inputType = type == "number" ? type : "text" 67 | 68 | return ( 69 | handleChange(id, e, workspaceIndex) } 71 | onKeyDown={ this.closeOnEnterKeyDown } 72 | ref="valueInput" 73 | type={ inputType } 74 | value={ value } 75 | /> 76 | ) 77 | } 78 | } 79 | 80 | render() { 81 | return ( 82 |
83 | 87 | { this.customInput() } 88 |
89 | ) 90 | } 91 | } 92 | 93 | CustomValueInput.propTypes = { 94 | closeDialog: PropTypes.func.isRequired, 95 | handleChange: PropTypes.func.isRequired, 96 | id: PropTypes.number.isRequired, 97 | primitives: PropTypes.objectOf(PrimitivePropTypes).isRequired, 98 | type: PropTypes.string.isRequired, 99 | value: PropTypes.string, 100 | workspaceIndex: PropTypes.number 101 | } 102 | -------------------------------------------------------------------------------- /src/components/MainBrick.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react' 2 | import { Group } from 'react-art' 3 | import Rectangle from 'react-art/lib/Rectangle.art' 4 | 5 | import composeBrick from './composeBrick' 6 | import { getConstant } from './constants' 7 | import { getComponent } from '../utils/ComponentFactory' 8 | import { PositionPropTypes, SizePropTypes } from '../propTypes' 9 | 10 | import TestInput from '../containers/TestInput' 11 | import TestOutput from '../containers/TestOutput' 12 | 13 | class MainBrick extends Component { 14 | render() { 15 | const { 16 | componentName, 17 | handleClick, 18 | id, 19 | inner, 20 | selectedSlots, 21 | size, 22 | testInputs, 23 | testOutputs, 24 | unitTest, 25 | workspaceIndex 26 | } = this.props 27 | const slotHeight = getConstant(componentName, 'slotHeight') 28 | 29 | return ( 30 | 31 | { testInputs.map((element) => { 32 | return ( 33 | 39 | ) 40 | }) 41 | } 42 | handleClick(id, e, workspaceIndex) } 48 | /> 49 | { inner.map((element) => { 50 | const ReactComponent = getComponent(element.componentName) 51 | 52 | return ( 53 | 59 | ) 60 | }) 61 | } 62 | { testOutputs.map((element) => { 63 | return ( 64 | 70 | ) 71 | }) 72 | } 73 | 74 | ) 75 | } 76 | } 77 | 78 | MainBrick.propTypes = { 79 | componentName: PropTypes.string.isRequired, 80 | handleClick: PropTypes.func.isRequired, 81 | id: PropTypes.number.isRequired, 82 | inner: PropTypes.arrayOf( 83 | PropTypes.shape({ 84 | componentName: PropTypes.string.isRequired, 85 | id: PropTypes.number.isRequired, 86 | name: PropTypes.string, 87 | position: PositionPropTypes, 88 | value: PropTypes.any 89 | }) 90 | ).isRequired, 91 | selectedSlots: PropTypes.arrayOf(PropTypes.number).isRequired, 92 | size: SizePropTypes.isRequired, 93 | testInputs: PropTypes.arrayOf(PropTypes.object).isRequired, 94 | testOutputs: PropTypes.arrayOf(PropTypes.object).isRequired, 95 | unitTest: PropTypes.object.isRequired, 96 | workspaceIndex: PropTypes.number.isRequired 97 | } 98 | 99 | export default composeBrick(MainBrick) 100 | -------------------------------------------------------------------------------- /src/components/Workspace.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react' 2 | import throttle from 'lodash.throttle' 3 | 4 | import { UNIT_TEST } from './constants' 5 | import { isNotEmpty } from '../utils' 6 | import { PositionPropTypes } from '../propTypes' 7 | 8 | import SelectedElementDialog from '../containers/SelectedElementDialog' 9 | import TestSummary from './TestSummary' 10 | import Translate from './Translate' 11 | import WorkspaceSurface from './WorkspaceSurface' 12 | 13 | const styles = { 14 | WebkitUserSelect: 'none' 15 | } 16 | 17 | const workspaceHeaderStyles = { 18 | display: 'inline-block', 19 | marginRight: '30px' 20 | } 21 | 22 | const workspacesStyles = { 23 | display: 'flex' 24 | } 25 | 26 | export default class Workspace extends Component { 27 | componentDidMount() { 28 | const { 29 | initWorkspace, 30 | type 31 | } = this.props 32 | 33 | initWorkspace(type) 34 | } 35 | 36 | render() { 37 | const { 38 | dragStarted, 39 | mainBrick, 40 | moveElement, 41 | selectedElement, 42 | selectedSlots, 43 | selectElementOrStopDrag, 44 | type, 45 | unitTests 46 | } = this.props 47 | let handleMouseMove, handleMouseUp 48 | 49 | if(dragStarted) { 50 | handleMouseMove = (e) => { 51 | moveElement({ x: e.pageX, y: e.pageY }) 52 | } 53 | handleMouseUp = (e) => { 54 | selectElementOrStopDrag({ x: e.pageX, y: e.pageY }) 55 | } 56 | } 57 | else { 58 | handleMouseMove = () => {} 59 | handleMouseUp = () => {} 60 | } 61 | 62 | return ( 63 |
69 |
70 | 75 | { type == UNIT_TEST && } 76 | 77 | 81 | 82 |
83 |
84 | { unitTests.map((unitTest, index) => { 85 | return ( 86 | 93 | ) 94 | }) 95 | } 96 |
97 | { isNotEmpty(selectedElement) && 98 | 99 | } 100 |
101 | ) 102 | } 103 | } 104 | 105 | Workspace.propTypes = { 106 | dragStarted: PropTypes.bool.isRequired, 107 | initWorkspace: PropTypes.func.isRequired, 108 | mainBrick: PropTypes.object.isRequired, 109 | moveElement: PropTypes.func.isRequired, 110 | selectedElement: PropTypes.object.isRequired, 111 | selectedSlots: PropTypes.arrayOf(PropTypes.number).isRequired, 112 | selectElementOrStopDrag: PropTypes.func.isRequired, 113 | type: PropTypes.string.isRequired, 114 | unitTests: PropTypes.array.isRequired 115 | } 116 | -------------------------------------------------------------------------------- /src/reducers/workspaceReducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | ADD_BRICK, 3 | ADD_PIPE, 4 | ADD_PRIMITIVE, 5 | ADD_UNIT_TEST, 6 | CHANGE_PRIMITIVE_VALUE, 7 | CHANGE_TEST_NODE_TYPE, 8 | CHANGE_TEST_NODE_VALUE, 9 | CLEAR_SLOT_SELECTION, 10 | EVALUATE, 11 | INIT_WORKSPACE, 12 | LINK_SLOTS, 13 | MOVE_ELEMENT, 14 | REMOVE_ELEMENT, 15 | REMOVE_SELECTED_ELEMENT, 16 | REMOVE_UNIT_TEST, 17 | SELECT_ELEMENT, 18 | SELECT_SLOT, 19 | START_DRAG, 20 | STOP_DRAG, 21 | UNEVALUATE, 22 | UNLINK_SLOTS, 23 | } from '../actions' 24 | 25 | import { 26 | addBrickToWorkspace, 27 | addPipeToWorkspace, 28 | addPrimitiveToWorkspace, 29 | addUnitTestToWorkspace, 30 | initWorkspace, 31 | removeElementInWorkspace, 32 | removeUnitTest 33 | } from './workspace/addRemoveElementReducer' 34 | 35 | import { 36 | addDragStartedToWorkspace, 37 | addDragStoppedToWorkspace, 38 | updateElementInWorkspace 39 | } from './workspace/dragReducer' 40 | 41 | import { 42 | evaluateAllWorkspaces, 43 | unevaluate 44 | } from './workspace/evaluationReducer' 45 | 46 | import { 47 | addSelectedElementToWorkspace, 48 | removeSlotSelectionState, 49 | removeSelectedElementFromWorkspace, 50 | updateSlotSelectionStateInWorkspace 51 | } from './workspace/selectionReducer' 52 | 53 | import { 54 | changePrimitiveValue, 55 | changeTestNodeType, 56 | changeTestNodeValue, 57 | linkSlots, 58 | unlinkSlots 59 | } from './workspace/updateElementReducer' 60 | 61 | const initialWorkspace = { 62 | entities: { 63 | 1: { 64 | innerIds: [], 65 | testInputIds: [], 66 | testOutputIds: [] 67 | } 68 | }, 69 | mainBrickId: 1, 70 | selectionState: { 71 | dragStarted: false, 72 | element: { }, 73 | pipe: { 74 | input: { }, 75 | output: { } 76 | } 77 | }, 78 | unitTests: [] 79 | } 80 | 81 | export const workspace = (state = initialWorkspace, action) => { 82 | const { payload, type } = action 83 | 84 | switch (type) { 85 | case ADD_BRICK: 86 | return addBrickToWorkspace(state, payload) 87 | case ADD_PIPE: 88 | return addPipeToWorkspace(state, payload) 89 | case ADD_PRIMITIVE: 90 | return addPrimitiveToWorkspace(state, payload) 91 | case ADD_UNIT_TEST: 92 | return addUnitTestToWorkspace(state) 93 | case CHANGE_PRIMITIVE_VALUE: 94 | return changePrimitiveValue(state, payload) 95 | case CHANGE_TEST_NODE_TYPE: 96 | return changeTestNodeType(state, payload) 97 | case CHANGE_TEST_NODE_VALUE: 98 | return changeTestNodeValue(state, payload) 99 | case CLEAR_SLOT_SELECTION: 100 | return removeSlotSelectionState(state) 101 | case EVALUATE: 102 | return evaluateAllWorkspaces(state, payload) 103 | case INIT_WORKSPACE: 104 | return initWorkspace(state, payload) 105 | case LINK_SLOTS: 106 | return linkSlots(state, payload) 107 | case MOVE_ELEMENT: 108 | return updateElementInWorkspace(state, payload) 109 | case REMOVE_ELEMENT: 110 | return removeElementInWorkspace(state, payload) 111 | case REMOVE_SELECTED_ELEMENT: 112 | return removeSelectedElementFromWorkspace(state) 113 | case REMOVE_UNIT_TEST: 114 | return removeUnitTest(state, payload) 115 | case SELECT_ELEMENT: 116 | return addSelectedElementToWorkspace(state, payload) 117 | case SELECT_SLOT: 118 | return updateSlotSelectionStateInWorkspace(state, payload) 119 | case START_DRAG: 120 | return addDragStartedToWorkspace(state, payload) 121 | case STOP_DRAG: 122 | return addDragStoppedToWorkspace(state, payload) 123 | case UNEVALUATE: 124 | return unevaluate(state, payload) 125 | case UNLINK_SLOTS: 126 | return unlinkSlots(state, payload) 127 | default: 128 | return state 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/components/Tutorial.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import JoyRide from 'react-joyride' 3 | 4 | import { getMessage } from '../utils/translationUtils' 5 | import { 6 | getTutorialConfig, 7 | totalSteps 8 | } from './tutorialSteps' 9 | 10 | import Library from '../containers/Library' 11 | import Logo from './Logo' 12 | import Translate from './Translate' 13 | import Workspace from '../containers/Workspace' 14 | 15 | export default class Tutorial extends Component { 16 | constructor(props) { 17 | super(props) 18 | 19 | this.state = { 20 | joyrideOverlay: true, 21 | joyrideType: 'continuous', 22 | ready: false, 23 | showStepsProgress: false, 24 | joyrideSteps: [ 25 | { 26 | text: getMessage(props.locale, 'joyrideSteps.window'), 27 | selector: '#window', 28 | position: 'top' 29 | }, 30 | { 31 | text: getMessage(props.locale, 'joyrideSteps.library'), 32 | selector: '#library', 33 | position: 'right' 34 | }, 35 | { 36 | text: getMessage(props.locale, 'joyrideSteps.constants'), 37 | selector: '#constants', 38 | position: 'right' 39 | }, 40 | { 41 | text: getMessage(props.locale, 'joyrideSteps.functions'), 42 | selector: '#functions', 43 | position: 'right' 44 | }, 45 | { 46 | text: getMessage(props.locale, 'joyrideSteps.workspace'), 47 | selector: '#workspace', 48 | position: 'left' 49 | }, 50 | { 51 | text: getMessage(props.locale, 'joyrideSteps.tutorial'), 52 | selector: '#instructions', 53 | position: 'right' 54 | } 55 | ] 56 | } 57 | } 58 | 59 | componentDidMount() { 60 | this.setState({ 61 | ready: getTutorialConfig(this.props.step).openSiteTourAtStart 62 | }) 63 | } 64 | 65 | componentDidUpdate(prevProps, prevState) { 66 | if(!prevState.ready && this.state.ready) { 67 | this.refs.joyride.start(true) 68 | } 69 | } 70 | 71 | render() { 72 | const { locale, step } = this.props 73 | const nextStep = step + 1 74 | const tutorialConfig = getTutorialConfig(step) 75 | const state = this.state 76 | 77 | return ( 78 |
79 | 80 |
81 | 85 |

{ step } / { totalSteps() }

86 |
91 | { getMessage(locale, `tutorialSteps.${nextStep}`) && } 96 |
97 | 98 | 99 | 108 |
109 | ) 110 | } 111 | 112 | getChildContext() { 113 | return { 114 | locale: this.props.locale 115 | } 116 | } 117 | } 118 | 119 | Tutorial.childContextTypes = { 120 | locale: PropTypes.string.isRequired 121 | } 122 | 123 | Tutorial.defaultProps = { 124 | step: 1 125 | } 126 | 127 | Tutorial.PropTypes = { 128 | locale: PropTypes.string.isRequired, 129 | step: PropTypes.number.isRequired 130 | } 131 | -------------------------------------------------------------------------------- /src/reducers/workspace/addRemoveElementReducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | PRIMITIVE 3 | } from '../../utils/componentNames' 4 | 5 | import { 6 | newBrick, 7 | newPipe, 8 | newPrimitive, 9 | newUnitTest, 10 | newWorkspace, 11 | pipeConnectedToElement 12 | } from './workspaceReducerUtils' 13 | 14 | export const initWorkspace = (workspace, type) => 15 | newWorkspace(type) 16 | 17 | export const addBrickToWorkspace = (workspace, attributes) => 18 | addToWorkspace(workspace, newBrick(attributes, workspace.mainBrickId)) 19 | 20 | export const addPipeToWorkspace = (workspace, attributes) => 21 | addToWorkspace(workspace, newPipe(attributes)) 22 | 23 | export const addPrimitiveToWorkspace = (workspace, primitiveType) => { 24 | const primitive = newPrimitive(primitiveType, workspace.mainBrickId) 25 | const newWorkspace = addToWorkspace(workspace, primitive) 26 | 27 | newWorkspace.unitTests = newWorkspace.unitTests.map((unitTest) => { 28 | return Object.assign({}, unitTest, { 29 | values: { 30 | ...unitTest.values, 31 | [primitive.id]: { 32 | componentName: PRIMITIVE, 33 | type: primitiveType 34 | } 35 | } 36 | }) 37 | }) 38 | 39 | return newWorkspace 40 | } 41 | 42 | export const addUnitTestToWorkspace = (workspace) => { 43 | const values = workspace.unitTests[0].values 44 | let newValues = {} 45 | 46 | for(let elementId in values) { 47 | if(values[elementId].componentName == PRIMITIVE) 48 | newValues[elementId] = values[elementId] 49 | } 50 | 51 | return Object.assign({}, workspace, { 52 | ...workspace, 53 | unitTests: [ 54 | ...workspace.unitTests, 55 | { 56 | values: newValues 57 | } 58 | ] 59 | }) 60 | } 61 | 62 | const addToWorkspace = (workspace, element) => { 63 | const mainBrickId = workspace.mainBrickId 64 | const mainBrick = workspace.entities[mainBrickId] 65 | 66 | return Object.assign({}, workspace, { 67 | ...workspace, 68 | entities: { 69 | ...workspace.entities, 70 | [mainBrickId]: { 71 | ...mainBrick, 72 | innerIds: [ 73 | ...mainBrick.innerIds, 74 | element.id 75 | ] 76 | }, 77 | [element.id]: element 78 | } 79 | }) 80 | } 81 | 82 | export const removeElementInWorkspace = (workspace, payload) => { 83 | const { elementId } = payload 84 | const { entities, mainBrickId } = workspace 85 | const mainBrick = entities[mainBrickId] 86 | 87 | let newEntities = {} 88 | let removedKeys = [] 89 | 90 | for(let key in entities) { 91 | const element = entities[key] 92 | 93 | // Skip if key is the elementId or is a pipe connected to this elementId 94 | if(!(key == elementId || pipeConnectedToElement(element, elementId))) { 95 | newEntities[key] = element 96 | } else { 97 | removedKeys.push(key) 98 | } 99 | } 100 | 101 | const newUnitTests = workspace.unitTests.map((unitTest) => { 102 | let newValues = {} 103 | for(let key in unitTest.values) { 104 | if(removedKeys.indexOf(key) === -1) 105 | newValues[key] = unitTest.values[key] 106 | } 107 | return { 108 | ...unitTest, 109 | values: newValues 110 | } 111 | }) 112 | 113 | return Object.assign({}, workspace, { 114 | ...workspace, 115 | entities: { 116 | ...newEntities, 117 | [mainBrickId]: { 118 | ...mainBrick, 119 | // Remove from mainBrick.innerIds all the entities removed by 120 | // the loop above 121 | innerIds: mainBrick.innerIds.filter((id) => newEntities[id]) 122 | } 123 | }, 124 | unitTests: newUnitTests 125 | }) 126 | } 127 | 128 | export const removeUnitTest = (workspace, workspaceIndex) => { 129 | const { unitTests } = workspace 130 | 131 | return Object.assign({}, workspace, { 132 | unitTests: [ 133 | ...unitTests.slice(0, workspaceIndex), 134 | ...unitTests.slice(workspaceIndex + 1) 135 | ] 136 | }) 137 | } 138 | -------------------------------------------------------------------------------- /docs/styles.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | color: #4f5f6f; 3 | font-family: 'Open Sans', sans-serif; 4 | height: 100%; 5 | margin: 0; 6 | min-height: 100%; 7 | overflow-x: hidden; 8 | padding: 0; 9 | } 10 | 11 | body { 12 | font-size: 14px; 13 | line-height: 1.5; 14 | } 15 | 16 | h1, h2, h3, h4, h5 { 17 | font-weight: normal; 18 | } 19 | 20 | h2, h3 { 21 | font-size: 14px; 22 | } 23 | 24 | a { 25 | color: #59c2e6; 26 | text-decoration: none; 27 | transition: color 0.13s; 28 | } 29 | 30 | a:hover { 31 | color: #0b98ca; 32 | } 33 | 34 | ol, ul { 35 | padding-left: 15px; 36 | } 37 | 38 | button { 39 | background-color: #0b98ca; 40 | border: 0; 41 | color: white; 42 | cursor: pointer; 43 | padding: 6px 12px; 44 | transition: background-color 0.13s; 45 | } 46 | 47 | button:hover { 48 | background-color: #219FCA; 49 | } 50 | 51 | label { 52 | display: inline-block; 53 | width: 45px; 54 | } 55 | 56 | select, input { 57 | background-color: white; 58 | border: 0; 59 | font-size: 14px; 60 | height: 26px; 61 | padding: 3px 3px; 62 | width: 200px; 63 | } 64 | 65 | hr { 66 | border-style: groove; 67 | } 68 | 69 | ::-webkit-scrollbar { 70 | height: 7px; 71 | width: 7px; 72 | } 73 | 74 | ::-webkit-scrollbar-thumb { 75 | background: #767D86; 76 | border-radius: 0; 77 | } 78 | 79 | ::-webkit-scrollbar-track { 80 | background-color: #2d363f; 81 | border-radius: 0; 82 | } 83 | 84 | #main { 85 | height: 100%; 86 | overflow-x: hidden; 87 | overflow-y: auto; 88 | position: absolute; 89 | width: 100%; 90 | } 91 | 92 | .app { 93 | background-color: #f0f3f6; 94 | box-sizing: border-box; 95 | /*box-shadow: 0 0 3px #ccc;*/ 96 | left: 0; 97 | /*margin: 0 auto;*/ 98 | min-height: 100vh; 99 | padding: 10px 10px 10px 240px; 100 | position: relative; 101 | transition: left 0.3s ease, padding-left 0.3s ease; 102 | width: 100%; 103 | } 104 | 105 | #logo { 106 | background-color: #3a4651; 107 | box-sizing: border-box; 108 | color: white; 109 | font-size: 16px; 110 | left: 0; 111 | line-height: 70px; 112 | padding-left: 15px; 113 | position: fixed; 114 | top: 0; 115 | width: 230px; 116 | } 117 | 118 | .brick { 119 | float: left; 120 | height: 40px; 121 | padding-top: 18px; 122 | width: 50px; 123 | } 124 | 125 | .brick rect { 126 | fill: #79D1EF; 127 | } 128 | 129 | #library { 130 | background-color: #3a4651; 131 | bottom: 0; 132 | color: white; 133 | font-size: 14px; 134 | left: 0; 135 | overflow-y: auto; 136 | position: fixed; 137 | top: 70px; 138 | transition: left 0.3s ease; 139 | width: 230px; 140 | } 141 | 142 | #library h2 { 143 | padding-left: 20px; 144 | } 145 | 146 | .nav h3, .nav h4 { 147 | color: white; 148 | font-size: 14px; 149 | margin: 0; 150 | } 151 | 152 | .nav h3 { 153 | background-color: #0b98ca; 154 | padding: 15px 10px 15px 20px; 155 | } 156 | 157 | .nav h4 { 158 | background-color: #2d363f; 159 | padding: 15px 10px 15px 40px; 160 | } 161 | 162 | .nav ul { 163 | list-style: none; 164 | margin: 0; 165 | padding-left: 0; 166 | } 167 | 168 | .nav li { 169 | color: rgba(255, 255, 255, 0.5); 170 | cursor: pointer; 171 | margin: 0; 172 | padding: 10px 0 10px 40px; 173 | transition: color 0.13s; 174 | } 175 | 176 | .nav li:hover { 177 | color: white; 178 | background-color: #2d363f; 179 | } 180 | 181 | #instructions { 182 | background-color: #2d363f; 183 | bottom: 0; 184 | box-sizing: border-box; 185 | color: rgba(255, 255, 255, 0.6); 186 | left: 0; 187 | overflow-y: auto; 188 | padding: 10px; 189 | position: fixed; 190 | top: 70px; 191 | width: 250px; 192 | } 193 | 194 | .tutorial.app { 195 | padding-left: 490px; 196 | } 197 | 198 | .tutorial #logo { 199 | width: 480px; 200 | } 201 | 202 | .tutorial #library { 203 | left: 250px; 204 | } 205 | 206 | .selectedElementDialog { 207 | background-color: rgba(58, 70, 81, 0.9); 208 | border-radius: 4px; 209 | color: white; 210 | padding: 20px 14px 10px; 211 | position: absolute; 212 | } 213 | 214 | #customValueInput input[type=radio] { 215 | display: none; 216 | } 217 | 218 | #customValueInput input[type=radio] + label { 219 | border: 3px solid rgba(0, 0, 0, 0); 220 | color: white; 221 | display: inline-block; 222 | font-weight: bold; 223 | height: 20px; 224 | margin-right: 3px; 225 | padding-left: 3px; 226 | } 227 | 228 | #customValueInput input[type=radio]:checked + label { 229 | border: 3px solid orange; 230 | } 231 | 232 | .closeBtn { 233 | color: white; 234 | font-size: 15px; 235 | line-height: 10px; 236 | position: absolute; 237 | right: 6px; 238 | text-decoration: none; 239 | top: 6px; 240 | } 241 | 242 | .closeBtn:hover, .closeBtn:focus, .closeBtn:active { 243 | color: white; 244 | text-decoration: none; 245 | } 246 | 247 | .topMargin { 248 | margin-top: 5px; 249 | } 250 | 251 | .code { 252 | color: rgba(255, 255, 255, 0.9); 253 | font-weight: bold; 254 | } 255 | 256 | .github-logo { 257 | width: 32px; 258 | float: right; 259 | filter: opacity(80%); 260 | -webkit-filter: opacity(80%); 261 | transition: filter 0.13s, -webkit-filter 0.13s; 262 | } 263 | 264 | .github-logo:hover { 265 | filter: opacity(90%); 266 | -webkit-filter: opacity(90%); 267 | } 268 | -------------------------------------------------------------------------------- /src/utils/evalUtils.js: -------------------------------------------------------------------------------- 1 | import { 2 | BRICK, 3 | MAIN_BRICK 4 | } from './componentNames' 5 | 6 | export const ERROR = 'error' 7 | 8 | export const doesAllInputsHaveValues = (element, valueIds, unitTest) => { 9 | const numberOfInputs = Object.keys(element.inputSlots).length 10 | 11 | return (numberOfInputs == valueIds.length) && 12 | (unitTestValues(valueIds, unitTest).length == numberOfInputs) 13 | } 14 | 15 | export const elementInputValueIds = (element) => { 16 | let valueIds = [] 17 | 18 | for(let id in element.inputSlots) { 19 | const inputSlot = element.inputSlots[id] 20 | 21 | if(inputSlot.value) { 22 | valueIds[inputSlot.index] = inputSlot.value.slotId 23 | } 24 | } 25 | 26 | return valueIds 27 | } 28 | 29 | const unitTestValues = (valueIds, unitTest) => { 30 | let values = [] 31 | 32 | for(let id in valueIds) { 33 | const valueId = valueIds[id] 34 | const element = unitTest.values[valueId] 35 | 36 | if(element && element.type !== ERROR && element.value) { 37 | values.push({ 38 | id: valueId, 39 | type: element.type, 40 | value: element.value 41 | }) 42 | } 43 | } 44 | 45 | return values 46 | } 47 | 48 | const nativeBricks = { 49 | arithmetic: { 50 | "+": (a, b) => { 51 | if(assert([a, b], ["number", "number"])) 52 | return a + b 53 | throw "Invalid parameters" 54 | }, 55 | "-": (a, b) => { 56 | if(assert([a, b], ["number", "number"])) 57 | return a - b 58 | throw "Invalid parameters" 59 | }, 60 | "*": (a, b) => { 61 | if(assert([a, b], ["number", "number"])) 62 | return a * b 63 | throw "Invalid parameters" 64 | }, 65 | "/": (a, b) => { 66 | if(assert([a, b], ["number", "number"])) 67 | return a / b 68 | throw "Invalid parameters" 69 | } 70 | }, 71 | boolean: { 72 | "&&": (a, b) => a && b, 73 | "||": (a, b) => a || b, 74 | "!": (a) => !a 75 | }, 76 | eq: { 77 | "==": (a, b) => a === b, 78 | }, 79 | hof: { 80 | "join": (a) => a.join(''), 81 | "sort": (a) => a.slice(0).sort() 82 | }, 83 | parse: { 84 | "toNumber": (a) => { 85 | if(assert([a], ["string"])) 86 | return parseFloat(a) 87 | throw "Invalid parameters" 88 | }, 89 | "toString": (a) => { 90 | if(assert([a], ["number"])) 91 | return a.toString() 92 | throw "Invalid parameters" 93 | }, 94 | }, 95 | string: { 96 | "concat": (a, b) => { 97 | if(assert([a, b], ["string", "string"])) 98 | return a.concat(b) 99 | throw "Invalid parameters" 100 | }, 101 | "length": (a) => { 102 | if(assert([a], ["string"])) 103 | return a.length 104 | throw "Invalid parameters" 105 | }, 106 | "reverse": (a) => { 107 | if(assert([a], ["string"])) 108 | return a.split('').reverse().join('') 109 | throw "Invalid parameters" 110 | }, 111 | "split": (a) => { 112 | if(assert([a], ["string"])) 113 | return a.split('') 114 | throw "Invalid parameters" 115 | }, 116 | } 117 | } 118 | 119 | const assert = (parameters, types) => { 120 | for(var i = 0; i < parameters.length; i++) { 121 | if(typeof parameters[i] !== types[i]) 122 | return false 123 | } 124 | 125 | return true 126 | } 127 | 128 | const parsers = { 129 | boolean: (value) => value.toLowerCase() == "true", 130 | list: (value) => value.split(','), 131 | number: (value) => parseFloat(value), 132 | string: (value) => value 133 | } 134 | 135 | const _evalBrick = (brick, args) => { 136 | const { moduleName, name } = brick 137 | let brickOutput 138 | 139 | try { 140 | brickOutput = nativeBricks[moduleName][name].apply(null, args) 141 | } catch(error) { 142 | } 143 | 144 | if(brickOutput === undefined) { 145 | return { 146 | componentName: BRICK, 147 | type: ERROR, 148 | value: 'error' 149 | } 150 | } 151 | 152 | return { 153 | componentName: BRICK, 154 | type: _getOutputType(brickOutput), 155 | value: brickOutput.toString() 156 | } 157 | } 158 | 159 | export const tryEvalPath = (workspace, unitTest, elementId) => { 160 | let newUnitTest = Object.assign({}, unitTest) 161 | 162 | return _tryEvalPath(workspace, newUnitTest, elementId) 163 | } 164 | 165 | const _tryEvalPath = (workspace, unitTest, elementId) => { 166 | const brick = workspace.entities[elementId] 167 | let args = [] 168 | 169 | if(brick.componentName != BRICK) { 170 | return unitTest 171 | } 172 | 173 | for(var id in brick.inputSlots) { 174 | const { value } = brick.inputSlots[id] 175 | const slotValue = unitTest.values[value.slotId] 176 | 177 | if(slotValue && slotValue.type && slotValue.value) { 178 | args.push(parsers[slotValue.type](slotValue.value)) 179 | } else { 180 | return unitTest 181 | } 182 | } 183 | 184 | const outputSlotId = Object.keys(brick.outputSlots)[0] 185 | const outputSlot = brick.outputSlots[outputSlotId] 186 | 187 | unitTest.values[outputSlotId] = _evalBrick(brick, args) 188 | 189 | outputSlot.outputElementIds.forEach((id) => 190 | _tryEvalPath(workspace, unitTest, id) 191 | ) 192 | 193 | return unitTest 194 | } 195 | 196 | export const evalPathValueIds = (workspace, element, valueIds) => { 197 | if(element.componentName != MAIN_BRICK) { 198 | const outputSlotId = Object.keys(element.outputSlots)[0] 199 | const outputSlot = element.outputSlots[outputSlotId] 200 | 201 | valueIds.push(outputSlot.id) 202 | 203 | if(outputSlot.outputElementIds) { 204 | outputSlot.outputElementIds.forEach((id) => { 205 | evalPathValueIds(workspace, workspace.entities[id], valueIds) 206 | }) 207 | } 208 | } 209 | 210 | return valueIds 211 | } 212 | 213 | const _getOutputType = (output) => { 214 | if(Array.isArray(output)) { 215 | return 'list' 216 | } else { 217 | return typeof output 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WIP 2 | 3 | # Lambda Bricks 4 | 5 | What will happen if we separate a programming language in front-end and back-end? 6 | The back-end will be the programming language as we know it. 7 | But in the front-end we could add new features to it, for example: 8 | - Translations 9 | - Multiple ways of reading and writing the code. 10 | The code could behave as data, and we could have multiple views of it. 11 | - etc. 12 | 13 | This project is an experiment around this question. 14 | 15 | [![Alpha status](docs/images/video-preview.png)](http://www.youtube.com/watch?v=bf7bJLPxivc) 16 | 17 | - [How this project started](#how-this-project-started) 18 | - [Benefits](#benefits) 19 | - [Paradigms](#paradigms) 20 | - [Differences with similar projects](#differences-with-similar-projects) 21 | - [Roadmap](#roadmap) 22 | - [Further work](#further-work) 23 | - [License](#license) 24 | - [Development](#development) 25 | - [Tasks](#tasks) 26 | 27 | 28 | ## How this project started 29 | 30 | I was working in a bootcamp as a teacher, 31 | and realized that there is a lot of incidental complexity 32 | when someone is learning to program. 33 | 34 | We could use the ideas above to think of a programming environment 35 | that makes learning to program easier. 36 | Here are some ways in which applying this ideas could help: 37 | - Having translations or a visual representation could help non-English speakers. 38 | - Avoid the need to know all the syntax of a programing language 39 | before starting to program. You don't need to know where all the commas, 40 | parenthesis, brackets, keywords, etc. are placed. 41 | - Teach programming concepts and not a programming language. 42 | General concepts could be applied to different programming languages later. 43 | - All you need in one place: documentation, usage examples, tests, etc. 44 | 45 | The project currently have the constraint that can only represent 46 | functional languages that have some kind of pattern matching or guards. 47 | For example: Haskell, Erlang, Elixir. 48 | 49 | > Anything is easy if you can assimilate it to your collection of models. 50 | > If you can't, anything can be painfully difficult. 51 | Seymour Papert, Mindstorms. 52 | 53 | 54 | ## Benefits 55 | 56 | It is not a new programming language. 57 | 58 | 59 | ## Paradigms 60 | 61 | The project takes a lot of inspiration from the following paradigms: 62 | - Dataflow programming 63 | - Visual programming 64 | - Functional programming 65 | - Live programming 66 | 67 | 68 | ## Differences with similar projects 69 | 70 | This project differentiates by 3 main ideas of other visual programming environments: 71 | - Uses Functional Programming 72 | - The connections between blocks are as important as the blocks. 73 | In most of the other projects the connections are just lines. 74 | - The execution flow is from top to bottom, 75 | not from left to right like in most projects. 76 | I think flow from top to bottom is more natural, 77 | an example in the real world is gravity. 78 | 79 | 80 | ## Roadmap 81 | 82 | ### Evaluate technologies 83 | 84 | The first step is to evaluate 85 | if the project will continue to be developed with react/redux/canvas. 86 | Using it in Firefox feels slow. 87 | I don't know if it is because my limited knowledge of react/redux, 88 | or because the project is not suited for using these technologies. 89 | Please open an issue with your OS, browser, pc specs 90 | if the project feels slow in your machine. 91 | 92 | ### ~~Test output~~ DONE 93 | 94 | The same way as you select the values/types of the test inputs, 95 | you will select the value/type of the test output. 96 | If the test output is the same as the brick output 97 | there will be visual cue that the test is passing. 98 | 99 | ### ~~Custom constant value input depending on type~~ DONE 100 | 101 | ![Custom boolean value input](docs/images/custom-boolean-value-input.png) 102 | ![Custom number value input](docs/images/custom-number-value-input.png) 103 | 104 | ### Pattern matching/Guards 105 | 106 | The only way to do control flow will be with multiple brick heads. 107 | 108 | ![Guards](docs/images/guards.jpg) 109 | 110 | ### Collapse/Expand bricks 111 | 112 | This way you could see the dataflow inside other bricks. 113 | Also expand bricks implemented by other people to see what 114 | is happening inside. 115 | 116 | ### Keyboard shortcuts 117 | 118 | Just examples, the actual shortcuts will be defined later. 119 | - B Add new brick 120 | - C Add new constant 121 | - L Link slots 122 | - E Select element 123 | 124 | I think that with just this shortcuts and some autocompletion 125 | you could program with fewer keystrokes than even inside an IDE. 126 | This is because with each shortcut you are narrowing a lot 127 | the potential following actions. 128 | 129 | ### Higher order functions 130 | 131 | Wrap bricks inside a funnel, so it can flow through a pipe. 132 | 133 | ### Search bar for defined bricks 134 | 135 | Add a search bar for bricks with autocompletion. 136 | 137 | ![Search](docs/images/search-brick.png) 138 | 139 | ### Change locale/programming language 140 | 141 | There will be the need to implement backends in different programming languages 142 | that can evaluate Lambda Bricks state. 143 | 144 | 145 | ## Further work 146 | 147 | 1. Do physical open-hardware toys that could teach programming to kids. 148 | 2. Translate between code (in different languages) and brick visualization. 149 | 3. Apply this ideas for tools in: 150 | - Debugging 151 | - Refactoring 152 | - Navigating code 153 | - Visualization of a project architecture 154 | 155 | 156 | ## License 157 | 158 | This project is licensed AGPL for non-commercial use. 159 | A donation to the project will be required for commercial use. 160 | 161 | 162 | ## Development 163 | 164 | You need npm & browserify installed on your machine. 165 | 166 | ``` 167 | $ git clone https://github.com/lambdabricks/bricks-front-react.git 168 | $ cd bricks-front-react 169 | $ npm install 170 | $ npm start 171 | ``` 172 | Open `localhost:8080` in your browser. 173 | 174 | 175 | ## Tasks 176 | 177 | 178 | TODO: Add description of the components and state of the application. 179 | -------------------------------------------------------------------------------- /src/utils/translationUtils.js: -------------------------------------------------------------------------------- 1 | const translations = { 2 | en: { 3 | addUnitTest: 'Add unit test', 4 | constants: 'Constants', 5 | delete: 'Delete', 6 | deleteUnitTest: 'Delete unit test', 7 | empty: 'None', 8 | functions: 'Functions', 9 | tutorial: 'Tutorial', 10 | library: 'Library', 11 | loading: 'Loading...', 12 | next: 'Next', 13 | noValue: '', 14 | primitives: 'Primitives', 15 | type: 'Type: ', 16 | value: 'Value: ', 17 | workspace: 'Workspace', 18 | 'joyrideSteps.window': 'The window has 3 sections.', 19 | 'joyrideSteps.library': 20 | '1. Library\ 21 |

The library has 2 components:

  • Constants
  • Functions
  • ', 22 | 'joyrideSteps.constants': 23 | '

    Clicking on "Number" will add a ballon to the workspace.

    \ 24 |

    This ballon can hold a number.

    ', 25 | 'joyrideSteps.functions': 'Clicking on a math operation will add a brick to the workspace.', 26 | 'joyrideSteps.workspace': 27 | '2. Workspace\ 28 |

    The workspace is the playground where you can connect ballons and bricks.

    \ 29 |

    Clicking on an element will show a dialog where you can change its properties.

    \ 30 |

    Move the elements by drag & drop.

    \ 31 |

    The elements are connected through pipes. To create a pipe click on an\ 32 | input and an output slot.', 33 | 'joyrideSteps.tutorial': 34 | '3. Tutorial\ 35 |

    The instructions for following the tutorial

    ', 36 | 'tutorialSteps.1': 37 | "

    This is a small tutorial for learning the basics of programming.

    \ 38 |

    All programs are composed of two primary components.\ 39 | It doesn't matter if it's a smartphone app, a web site, a game, or anything else.\ 40 | The two components are: constants and functions.

    \ 41 |

    You have already used this components in your math lessons of elementary school.

    \ 42 |
    • The numbers are constants.
    • \ 43 |
    • And the math operators are functions.
    \ 44 |

    In general functions process one or more inputs to give a result.\ 45 | For example in the sum 3 + 4, the function + has two inputs: the numbers\ 46 | 3 and 4. And it gives the result of 7.

    \ 47 |

    In the workspace you can do this kind of operations by connecting the constants\ 48 | as inputs to the functions.

    \ 49 |

    Try doing a couple of operations now\ 50 | (Here is a video showing\ 51 | how to do it if you have any doubts).

    \ 52 |

    When you finish doing some operations click the button Next to continue.

    ", 53 | 'tutorialSteps.2': 54 | "

    We can manipulate not only numbers, we can also manipulate letters.\ 55 | Letters are usually referred as String in programming.\ 56 | And as well as the math operations, there are functions to transform them.

    \ 57 |

    Try to guess what the functions in the right do. Then do some operations\ 58 | on the workspace to see if the results of the functions are what you expected.

    \ 59 |

    Some things you can try:

    \ 60 |
    • concat a String\ 61 | with your first name and a String with your last name.
    • \ 62 |
    • reverse your name.
    • \ 63 |
    • Get the length of your name.
    • \ 64 |
    \ 65 |

    When you finish click the button Next to continue.

    ", 66 | 'tutorialSteps.3': 67 | "

    We have seen two types of constants so far: Numbers & Letters (Strings).\ 68 | Most programs are composed by more than one type of data. And we can mix them\ 69 | according to what we want to achieve. We can even convert from one to the\ 70 | other in case we need it.

    \ 71 |

    Have you noticed that constants have different colors depending on their type?\ 72 | Numbers are yellow and strings are orange.

    \ 73 |

    The functions have definitions that tell us which type of data they need as input.\ 74 | Hover on the functions of the library and you will see this information.

    \ 75 |

    As usual try doing a couple of operations now. See what happens when you connect\ 76 | an input of the wrong type to a function.

    \ 77 |

    Try using the functions toNumber and\ 78 | toString to convert a constant\ 79 | from one type to another.

    \ 80 |

    When you finish click the button Next to continue.

    ", 81 | 'tutorialSteps.4': 82 | "

    What we have done so far don't seem really useful. But one thing we haven't\ 83 | done, is using the output of a function as input for another function.

    \ 84 |

    All programs are composed by series of transformations with a specific order.\ 85 | For example, to calculate the average of 2 numbers, first we need to sum the\ 86 | numbers and then divide the result by 2.

    \ 87 |

    Try using the output of functions as input to other ones. Some ideas of\ 88 | things you can try:

    \ 89 |
    • Calculate the average of 2 numbers.
    • \ 90 |
    • Concat 3 strings.
    • \ 91 |
    • Can you figure out which functions you need to connect to calculate the\ 92 | number of digits in a number?
    \ 93 |

    We are near the end of the tutorial there is just one more step.

    \ 94 |

    When you finish doing some of the exercises click the button Next.

    ", 95 | 'tutorialSteps.5': 96 | "

    Imagine we have a program that needs to calculate a lot of averages.\ 97 | It will be really boring to do all the connections every time.\ 98 | Luckily there is a way to avoid this. We can create our own functions!

    \ 99 |

    The functions that we create are exactly the same as the functions we have used\ 100 | in the previous steps. Our functions will have inputs and outputs.\ 101 | We can receive data in the inputs, and we return a result as output.

    \ 102 |

    There are two separate steps in creating a function:

    \ 103 |
    1. First we need to add other functions & constants inside the function\ 104 | we are creating, and connect them with pipes. This is known in programming as\ 105 | defining the function.
    2. \ 106 |
    3. After we have defined the function, someone else (or ourselves) can use it\ 107 | by giving values to every input. This is known in programming as calling\ 108 | the function.
    \ 109 |

    Try to create your own function now. You can create a function that calculates\ 110 | the average of 2 numbers that we saw in the previous step, or any other function\ 111 | you want. You can call your function by giving values to the constants that are\ 112 | outside of it.

    \ 113 |

    This is the end of this really short tutorial on the basics of programming.\ 114 | Hope you find it useful as a first step in your path to learn to program.

    " 115 | } 116 | } 117 | 118 | export const getMessage = (locale, key) => { 119 | return translations[locale][key] 120 | } 121 | -------------------------------------------------------------------------------- /src/reducers/workspace/updateElementReducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | BRICK, 3 | MAIN_BRICK, 4 | PRIMITIVE, 5 | TEST_INPUT 6 | } from '../../utils/componentNames' 7 | 8 | const updateElementInAllUnitTests = (workspace, elementId, newProps) => { 9 | const { unitTests } = workspace 10 | 11 | return Object.assign({}, workspace, { 12 | ...workspace, 13 | unitTests: unitTests.map((unitTest) => 14 | updateElementValues(unitTest, elementId, newProps) 15 | ) 16 | }) 17 | } 18 | 19 | const updateElementInUnitTest = (workspace, elementId, workspaceIndex, newProps) => { 20 | const { unitTests } = workspace 21 | 22 | return Object.assign({}, workspace, { 23 | ...workspace, 24 | unitTests: [ 25 | ...unitTests.slice(0, workspaceIndex), 26 | updateElementValues(unitTests[workspaceIndex], elementId, newProps), 27 | ...unitTests.slice(workspaceIndex + 1) 28 | ] 29 | }) 30 | } 31 | 32 | const updateElementValues = (unitTest, elementId, newProps) => { 33 | return Object.assign({}, unitTest, { 34 | ...unitTest, 35 | values: { 36 | ...unitTest.values, 37 | [elementId]: { 38 | ...unitTest.values[elementId], 39 | ...newProps 40 | } 41 | } 42 | }) 43 | } 44 | 45 | export const changePrimitiveValue = (workspace, payload) => { 46 | const { 47 | elementId, 48 | newValue 49 | } = payload 50 | 51 | return updateElementInAllUnitTests( 52 | workspace, 53 | elementId, 54 | { 55 | componentName: PRIMITIVE, 56 | value: newValue 57 | } 58 | ) 59 | } 60 | 61 | export const changeTestNodeType = (workspace, payload) => { 62 | const { 63 | componentName, 64 | elementId, 65 | newType, 66 | workspaceIndex 67 | } = payload 68 | 69 | return updateElementInUnitTest( 70 | workspace, 71 | elementId, 72 | workspaceIndex, 73 | { 74 | componentName, 75 | type: newType 76 | } 77 | ) 78 | } 79 | 80 | export const changeTestNodeValue = (workspace, payload) => { 81 | const { 82 | componentName, 83 | elementId, 84 | newValue, 85 | workspaceIndex 86 | } = payload 87 | 88 | return updateElementInUnitTest( 89 | workspace, 90 | elementId, 91 | workspaceIndex, 92 | { 93 | componentName, 94 | value: newValue 95 | } 96 | ) 97 | } 98 | 99 | export const linkSlots = (workspace, payload) => { 100 | const { input, output } = payload 101 | const outputElement = workspace.entities[output.elementId] 102 | let newOutputElementSlots = {} 103 | let inputElement = workspace.entities[input.elementId] 104 | 105 | if(inputElement.componentName == MAIN_BRICK) { 106 | inputElement = workspace.entities[input.slotId] 107 | } 108 | 109 | const outputSlot = inputElement.outputSlots[input.slotId] 110 | 111 | const newInputElementSlots = { 112 | outputSlots: _addPropsToSlot( 113 | inputElement.outputSlots, 114 | input.slotId, 115 | { 116 | outputElementIds: [ 117 | ...outputSlot.outputElementIds, 118 | outputElement.id 119 | ] 120 | } 121 | ) 122 | } 123 | 124 | if(outputElement.componentName == BRICK) { 125 | const { inputSlots } = outputElement 126 | 127 | newOutputElementSlots = { 128 | inputSlots: addValueToSlots(inputSlots, input, output) 129 | } 130 | } 131 | if(outputElement.componentName == MAIN_BRICK) { 132 | const { outputSlots } = outputElement 133 | 134 | newOutputElementSlots = { 135 | outputSlots: addValueToSlots(outputSlots, input, output) 136 | } 137 | } 138 | 139 | return Object.assign({}, workspace, { 140 | entities: { 141 | ...workspace.entities, 142 | [outputElement.id]: { 143 | ...outputElement, 144 | ...newOutputElementSlots 145 | }, 146 | [inputElement.id]: { 147 | ...inputElement, 148 | ...newInputElementSlots 149 | } 150 | } 151 | }) 152 | } 153 | 154 | const addValueToSlots = (slots, input, output) => { 155 | return _addPropsToSlot(slots, output.slotId, { 156 | value: { 157 | slotId: input.slotId, 158 | elementId: input.elementId 159 | } 160 | }) 161 | } 162 | 163 | const _addPropsToSlot = (slots, slotId, newProps) => { 164 | return Object.assign({}, slots, { 165 | [slotId]: { 166 | ...slots[slotId], 167 | ...newProps 168 | } 169 | }) 170 | } 171 | 172 | export const unlinkSlots = (workspace, payload) => { 173 | const { input, output } = payload 174 | let inputElement = workspace.entities[input.elementId] 175 | 176 | if(inputElement.componentName == MAIN_BRICK) { 177 | inputElement = workspace.entities[input.slotId] 178 | } 179 | 180 | const outputSlot = inputElement.outputSlots[input.slotId] 181 | const index = outputSlot.outputElementIds.indexOf(output.elementId) 182 | 183 | const outputElement = workspace.entities[output.elementId] 184 | const newOutputElement = _removeSlotValue(outputElement, output) 185 | 186 | return Object.assign({}, workspace, { 187 | entities: { 188 | ...workspace.entities, 189 | [outputElement.id]: newOutputElement, 190 | [inputElement.id]: { 191 | ...inputElement, 192 | outputSlots: { 193 | ...inputElement.outputSlots, 194 | [input.slotId]: { 195 | ...outputSlot, 196 | outputElementIds: [ 197 | ...outputSlot.outputElementIds.slice(0, index), 198 | ...outputSlot.outputElementIds.slice(index + 1) 199 | ] 200 | } 201 | } 202 | } 203 | } 204 | }) 205 | } 206 | 207 | const _removeSlotValue = (element, output) => { 208 | var slots = {} 209 | 210 | if(output.slotId) { 211 | if(element.componentName == MAIN_BRICK) { 212 | Object.assign(slots, { outputSlots: element.outputSlots }) 213 | 214 | delete slots.outputSlots[output.slotId]['value'] 215 | } else { 216 | Object.assign(slots, { inputSlots: element.inputSlots }) 217 | 218 | delete slots.inputSlots[output.slotId]['value'] 219 | } 220 | } else if(output.sourceElementId) { 221 | if(element.componentName == MAIN_BRICK) { 222 | Object.assign(slots, { outputSlots: element.outputSlots }) 223 | 224 | Object.keys(element.outputSlots).forEach((slotId) => { 225 | const slot = element.outputSlots[slotId] 226 | 227 | if(slot.value && slot.value.elementId == output.sourceElementId) { 228 | delete slots.outputSlots[slotId]['value'] 229 | } 230 | }) 231 | } else { 232 | Object.assign(slots, { inputSlots: element.inputSlots }) 233 | 234 | Object.keys(element.inputSlots).forEach((slotId) => { 235 | const slot = element.inputSlots[slotId] 236 | 237 | if(slot.value && slot.value.elementId == output.sourceElementId) { 238 | delete slots.inputSlots[slotId]['value'] 239 | } 240 | }) 241 | } 242 | } 243 | 244 | return Object.assign(element, { ...slots }) 245 | } 246 | -------------------------------------------------------------------------------- /src/reducers/workspace/workspaceReducerUtils.js: -------------------------------------------------------------------------------- 1 | import { 2 | CLEAN, 3 | FUNCTION, 4 | UNIT_TEST 5 | } from '../../components/constants' 6 | 7 | import { 8 | inputSlotPosition, 9 | outputSlotPosition 10 | } from '../../utils' 11 | 12 | import { 13 | BRICK, 14 | MAIN_BRICK, 15 | PRIMITIVE, 16 | SELECTABLE_PIPE, 17 | TEST_INPUT, 18 | TEST_OUTPUT 19 | } from '../../utils/componentNames' 20 | 21 | const TestNodeDefaults = { 22 | size: { 23 | height: 30, 24 | width: 60 25 | } 26 | } 27 | 28 | const MainBrickDefaults = { 29 | position: { 30 | x: 50, 31 | y: 80 32 | }, 33 | size: { 34 | height: 400, 35 | width: 350 36 | } 37 | } 38 | 39 | const Defaults = { 40 | [BRICK]: { 41 | size: { 42 | height: 40, 43 | width: 100 44 | } 45 | }, 46 | [MAIN_BRICK]: { 47 | position: { 48 | [CLEAN]: { 49 | x: 50, 50 | y: 35 51 | }, 52 | [FUNCTION]: MainBrickDefaults.position, 53 | [UNIT_TEST]: MainBrickDefaults.position 54 | }, 55 | size: { 56 | [CLEAN]: { 57 | height: 500, 58 | width: 600 59 | }, 60 | [FUNCTION]: MainBrickDefaults.size, 61 | [UNIT_TEST]: MainBrickDefaults.size 62 | } 63 | }, 64 | [PRIMITIVE]: { 65 | size: { 66 | height: 30, 67 | width: 60 68 | } 69 | }, 70 | [TEST_INPUT]: { 71 | size: TestNodeDefaults.size 72 | }, 73 | [TEST_OUTPUT]: { 74 | size: TestNodeDefaults.size 75 | } 76 | } 77 | 78 | let id = 1 79 | // TODO: Generate id's with an UID function ?? 80 | const nextId = () => id++ 81 | 82 | export const newBrick = (brick, parentId) => { 83 | const { arity, moduleName, name } = brick 84 | let inputSlots = {} 85 | const elementId = nextId() 86 | const outputSlotId = nextId() 87 | 88 | for(var i=0; i < arity; i++) { 89 | const id = nextId() 90 | 91 | inputSlots[id] = { 92 | id, 93 | index: i 94 | } 95 | } 96 | 97 | return { 98 | componentName: BRICK, 99 | id: elementId, 100 | inputSlots, 101 | moduleName, 102 | name, 103 | outputSlots: { 104 | [outputSlotId]: { 105 | id: outputSlotId, 106 | index: 0, 107 | outputElementIds: [], 108 | value: { 109 | slotId: outputSlotId 110 | } 111 | } 112 | }, 113 | parentId, 114 | position: _shiftPosition(elementId), 115 | size: Defaults[BRICK].size, 116 | valueId: outputSlotId 117 | } 118 | } 119 | 120 | const newMainBrick = (mainBrickId, workspaceType = UNIT_TEST) => { 121 | let mainBrick = { 122 | componentName: MAIN_BRICK, 123 | id: mainBrickId, 124 | innerIds: [], 125 | inputSlots: { }, 126 | outputSlots: { }, 127 | position: Defaults[MAIN_BRICK].position[workspaceType], 128 | size: Defaults[MAIN_BRICK].size[workspaceType], 129 | workspaceType 130 | } 131 | 132 | if(workspaceType != CLEAN) { 133 | const inputSlotIds = [nextId(), nextId()] 134 | const outputSlotId = nextId() 135 | 136 | mainBrick.inputSlots = { 137 | [inputSlotIds[0]]: { 138 | id: inputSlotIds[0], 139 | index: 0 140 | }, 141 | [inputSlotIds[1]]: { 142 | id: inputSlotIds[1], 143 | index: 1 144 | } 145 | } 146 | 147 | mainBrick.outputSlots = { 148 | [outputSlotId]: { 149 | id: outputSlotId, 150 | index: 0 151 | } 152 | } 153 | } 154 | 155 | return mainBrick 156 | } 157 | 158 | export const newPrimitive = (type, parentId) => { 159 | const elementId = nextId() 160 | 161 | return { 162 | componentName: PRIMITIVE, 163 | id: elementId, 164 | outputSlots: { 165 | [elementId]: { 166 | id: elementId, 167 | index: 0, 168 | outputElementIds: [], 169 | valueId: elementId 170 | } 171 | }, 172 | parentId, 173 | position: _shiftPosition(elementId), 174 | size: Defaults[PRIMITIVE].size, 175 | valueId: elementId 176 | } 177 | } 178 | 179 | const _shiftPosition = (id) => { 180 | const xMultiplier = (id % 5) + 1 181 | const yMultiplier = Math.floor(id / 5) + 1 182 | 183 | return { 184 | x: 50 * xMultiplier, 185 | y: 30 * yMultiplier 186 | } 187 | } 188 | 189 | export const newPipe = (payload) => { 190 | const { input, output } = payload 191 | 192 | return { 193 | componentName: SELECTABLE_PIPE, 194 | id: nextId(), 195 | input, 196 | output, 197 | type: "null", 198 | valueId: input.slotId 199 | } 200 | } 201 | 202 | export const newTestInputs = (mainBrick) => { 203 | let testInputs = _newTestNodes( 204 | mainBrick, 205 | TEST_INPUT, 206 | mainBrick.inputSlots, 207 | inputSlotPosition 208 | ) 209 | 210 | for(var id in testInputs) { 211 | const testInput = testInputs[id] 212 | 213 | testInput.outputSlots = { 214 | [testInput.id]: { 215 | id: testInput.id, 216 | index: 0, 217 | outputElementIds: [] 218 | } 219 | } 220 | } 221 | 222 | return testInputs 223 | } 224 | 225 | export const newTestOutputs = (mainBrick) => { 226 | return _newTestNodes( 227 | mainBrick, 228 | TEST_OUTPUT, 229 | mainBrick.outputSlots, 230 | outputSlotPosition 231 | ) 232 | } 233 | 234 | const _newTestNodes = (mainBrick, componentName, slots, slotPosition) => { 235 | let testNodes = {} 236 | 237 | for(var id in slots) { 238 | const slot = slots[id] 239 | 240 | testNodes[id] = { 241 | componentName, 242 | id: slot.id, 243 | slotPosition: slotPosition(mainBrick, slot.id), 244 | size: Defaults[componentName].size, 245 | type: "null", 246 | valueId: slot.id 247 | } 248 | } 249 | 250 | return testNodes 251 | } 252 | 253 | export const newWorkspace = (type) => { 254 | const mainBrickId = nextId() 255 | const mainBrick = newMainBrick(mainBrickId, type) 256 | const testInputs = newTestInputs(mainBrick) 257 | const testOutputs = type == UNIT_TEST ? newTestOutputs(mainBrick) : [] 258 | 259 | const testInputIds = Object.keys(testInputs).map((testInput) => { 260 | return testInputs[testInput].id 261 | }) 262 | 263 | const testOutputIds = Object.keys(testOutputs).map((testOutput) => { 264 | return testOutputs[testOutput].id 265 | }) 266 | 267 | return { 268 | entities: { 269 | [mainBrickId]: { 270 | ...mainBrick, 271 | testInputIds, 272 | testOutputIds 273 | }, 274 | ...testInputs, 275 | ...testOutputs 276 | }, 277 | mainBrickId: mainBrickId, 278 | selectionState: { 279 | dragStarted: false, 280 | element: { }, 281 | pipe: { 282 | input: { }, 283 | output: { } 284 | } 285 | }, 286 | unitTests: [ 287 | { 288 | values: { } 289 | } 290 | ] 291 | } 292 | } 293 | 294 | export const pipeConnectedToElement = (element, elementId) => { 295 | return element.componentName == SELECTABLE_PIPE && 296 | (element.input.elementId == elementId || 297 | element.output.elementId == elementId) 298 | } 299 | -------------------------------------------------------------------------------- /docs/react-joyride-compiled.css: -------------------------------------------------------------------------------- 1 | .joyride-beacon { 2 | display: inline-block; 3 | height: 36px; 4 | position: relative; 5 | width: 36px; 6 | z-index: 1500; } 7 | .joyride-beacon__inner { 8 | -webkit-animation: joyride-beacon-inner 1.2s infinite ease-in-out; 9 | animation: joyride-beacon-inner 1.2s infinite ease-in-out; 10 | background-color: #404040; 11 | border-radius: 50%; 12 | display: block; 13 | height: 50%; 14 | left: 50%; 15 | opacity: 0.7; 16 | position: relative; 17 | top: 50%; 18 | -webkit-transform: translate(-50%, -50%); 19 | transform: translate(-50%, -50%); 20 | width: 50%; } 21 | .joyride-beacon__outer { 22 | -webkit-animation: joyride-beacon-outer 1.2s infinite ease-in-out; 23 | animation: joyride-beacon-outer 1.2s infinite ease-in-out; 24 | background-color: rgba(255, 0, 68, 0.2); 25 | border: 2px solid #404040; 26 | border-radius: 50%; 27 | display: block; 28 | height: 100%; 29 | left: 0; 30 | opacity: 0.9; 31 | position: absolute; 32 | top: 0; 33 | -webkit-transform: translateY(-50%); 34 | transform: translateY(-50%); 35 | -webkit-transform-origin: center; 36 | transform-origin: center; 37 | width: 100%; } 38 | 39 | .joyride-overlay { 40 | bottom: 0; 41 | cursor: pointer; 42 | left: 0; 43 | position: absolute; 44 | right: 0; 45 | top: 0; 46 | z-index: 1500; } 47 | 48 | .joyride-hole { 49 | border-radius: 4px; 50 | box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.5), 0 0 15px rgba(0, 0, 0, 0.5); 51 | position: absolute; } 52 | .joyride-hole.safari { 53 | box-shadow: 0 0 999px 9999px rgba(0, 0, 0, 0.5), 0 0 15px rgba(0, 0, 0, 0.5); } 54 | 55 | .joyride-tooltip { 56 | background-color: #fff; 57 | border-radius: 4px; 58 | color: #333; 59 | cursor: default; 60 | -webkit-filter: drop-shadow(-1px -2px 3px rgba(0, 0, 0, 0.3)) drop-shadow(1px 2px 3px rgba(0, 0, 0, 0.3)); 61 | filter: drop-shadow(-1px -2px 3px rgba(0, 0, 0, 0.3)) drop-shadow(1px 2px 3px rgba(0, 0, 0, 0.3)); 62 | opacity: 0; 63 | padding: 20px; 64 | -webkit-transform: translate3d(0, 0, 0); 65 | transform: translate3d(0, 0, 0); 66 | width: 290px; 67 | z-index: 1510; } 68 | @media screen and (min-width: 480px) { 69 | .joyride-tooltip { 70 | width: 360px; } } 71 | @media screen and (min-width: 960px) { 72 | .joyride-tooltip { 73 | width: 450px; } } 74 | .joyride-tooltip--animate { 75 | -webkit-animation: joyride-tooltip 0.4s forwards; 76 | animation: joyride-tooltip 0.4s forwards; 77 | -webkit-animation-timing-function: cubic-bezier(0, 1.05, 0.55, 1.18); 78 | animation-timing-function: cubic-bezier(0, 1.05, 0.55, 1.18); } 79 | .joyride-tooltip__triangle { 80 | background-repeat: no-repeat; 81 | overflow: hidden; 82 | position: absolute; } 83 | .joyride-tooltip.bottom, .joyride-tooltip.bottom-left, .joyride-tooltip.bottom-right { 84 | margin-top: 18px; } 85 | .joyride-tooltip.bottom .joyride-tooltip__triangle, .joyride-tooltip.bottom-left .joyride-tooltip__triangle, .joyride-tooltip.bottom-right .joyride-tooltip__triangle { 86 | background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2236px%22%20height%3D%2218px%22%20version%3D%221.1%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpolygon%20points%3D%220%2C%200%208%2C%208%2016%2C0%22%20fill%3D%22%23fff%22%20transform%3D%22scale%282.25%29%20rotate%28180 8 4%29%22%3E%3C%2Fpolygon%3E%3C%2Fsvg%3E"); 87 | height: 18px; 88 | left: 50%; 89 | top: -16px; 90 | -webkit-transform: translateX(-50%); 91 | transform: translateX(-50%); 92 | width: 36px; } 93 | .joyride-tooltip.top, .joyride-tooltip.top-left, .joyride-tooltip.top-right { 94 | margin-bottom: 18px; } 95 | .joyride-tooltip.top .joyride-tooltip__triangle, .joyride-tooltip.top-left .joyride-tooltip__triangle, .joyride-tooltip.top-right .joyride-tooltip__triangle { 96 | background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2236px%22%20height%3D%2218px%22%20version%3D%221.1%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpolygon%20points%3D%220%2C%200%208%2C%208%2016%2C0%22%20fill%3D%22%23fff%22%20transform%3D%22scale%282.25%29%20rotate%280%29%22%3E%3C%2Fpolygon%3E%3C%2Fsvg%3E"); 97 | bottom: -16px; 98 | height: 18px; 99 | left: 50%; 100 | -webkit-transform: translateX(-50%); 101 | transform: translateX(-50%); 102 | width: 36px; } 103 | .joyride-tooltip.bottom-left .joyride-tooltip__triangle, .joyride-tooltip.top-left .joyride-tooltip__triangle { 104 | left: 3%; 105 | -webkit-transform: translateX(0); 106 | transform: translateX(0); } 107 | @media screen and (min-width: 480px) { 108 | .joyride-tooltip.bottom-left .joyride-tooltip__triangle, .joyride-tooltip.top-left .joyride-tooltip__triangle { 109 | left: 2%; } } 110 | .joyride-tooltip.bottom-right .joyride-tooltip__triangle, .joyride-tooltip.top-right .joyride-tooltip__triangle { 111 | left: auto; 112 | right: 3%; 113 | -webkit-transform: translateX(0); 114 | transform: translateX(0); } 115 | @media screen and (min-width: 480px) { 116 | .joyride-tooltip.bottom-right .joyride-tooltip__triangle, .joyride-tooltip.top-right .joyride-tooltip__triangle { 117 | right: 2%; } } 118 | .joyride-tooltip.left { 119 | margin-right: 18px; } 120 | .joyride-tooltip.left .joyride-tooltip__triangle { 121 | background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2218px%22%20height%3D%2236px%22%20version%3D%221.1%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpolygon%20points%3D%220%2C%200%208%2C%208%2016%2C0%22%20fill%3D%22%23fff%22%20transform%3D%22scale%282.25%29%20rotate%28270 8 8%29%22%3E%3C%2Fpolygon%3E%3C%2Fsvg%3E"); 122 | height: 36px; 123 | right: -16px; 124 | width: 18px; } 125 | .joyride-tooltip.right { 126 | margin-left: 18px; } 127 | .joyride-tooltip.right .joyride-tooltip__triangle { 128 | background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2218px%22%20height%3D%2236px%22%20version%3D%221.1%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpolygon%20points%3D%220%2C%200%208%2C%208%2016%2C0%22%20fill%3D%22%23fff%22%20transform%3D%22scale%282.25%29%20rotate%2890 4 4%29%22%3E%3C%2Fpolygon%3E%3C%2Fsvg%3E"); 129 | height: 36px; 130 | left: -16px; 131 | width: 18px; } 132 | .joyride-tooltip__close { 133 | color: rgba(85, 85, 85, 0.5); 134 | font-size: 30px; 135 | line-height: 12px; 136 | position: absolute; 137 | right: 10px; 138 | text-decoration: none; 139 | top: 10px; 140 | z-index: 10; 141 | display: block; } 142 | .joyride-tooltip__close:hover, .joyride-tooltip__close:focus { 143 | color: rgba(60, 60, 60, 0.5); 144 | outline: none; 145 | text-decoration: none; } 146 | .joyride-tooltip__close--header { 147 | right: 20px; 148 | top: 20px; } 149 | .joyride-tooltip__header { 150 | border-bottom: 1px solid #404040; 151 | color: #555; 152 | font-size: 20px; 153 | padding-bottom: 6px; 154 | position: relative; } 155 | .joyride-tooltip__main { 156 | font-size: 16px; 157 | padding: 12px 0 18px; } 158 | .joyride-tooltip__footer { 159 | text-align: right; } 160 | .joyride-tooltip__button--primary { 161 | background-color: #404040; 162 | border-radius: 4px; 163 | color: #fff; 164 | outline: none; 165 | padding: 6px 12px; 166 | text-decoration: none; 167 | -webkit-transition: background-color 0.2s ease-in-out; 168 | transition: background-color 0.2s ease-in-out; } 169 | .joyride-tooltip__button--primary:active, .joyride-tooltip__button--primary:focus, .joyride-tooltip__button--primary:hover { 170 | background-color: #666666; 171 | color: #fff; 172 | text-decoration: none; } 173 | .joyride-tooltip__button--secondary { 174 | color: #404040; 175 | margin-right: 10px; 176 | outline: none; } 177 | .joyride-tooltip__button--skip { 178 | color: #ccc; 179 | float: left; 180 | margin-right: 10px; } 181 | .joyride-tooltip--standalone .joyride-tooltip__main { 182 | padding-bottom: 0; } 183 | .joyride-tooltip--standalone .joyride-tooltip__footer { 184 | display: none; } 185 | 186 | @-webkit-keyframes joyride-tooltip { 187 | 0% { 188 | -webkit-transform: scale(0.1); 189 | transform: scale(0.1); } 190 | 100% { 191 | opacity: 1; 192 | -webkit-transform: perspective(1px) scale(1); 193 | transform: perspective(1px) scale(1); } } 194 | 195 | @keyframes joyride-tooltip { 196 | 0% { 197 | -webkit-transform: scale(0.1); 198 | transform: scale(0.1); } 199 | 100% { 200 | opacity: 1; 201 | -webkit-transform: perspective(1px) scale(1); 202 | transform: perspective(1px) scale(1); } } 203 | 204 | @-webkit-keyframes joyride-beacon-inner { 205 | 20% { 206 | opacity: 0.9; } 207 | 90% { 208 | opacity: 0.7; } } 209 | 210 | @keyframes joyride-beacon-inner { 211 | 20% { 212 | opacity: 0.9; } 213 | 90% { 214 | opacity: 0.7; } } 215 | 216 | @-webkit-keyframes joyride-beacon-outer { 217 | 0% { 218 | -webkit-transform: scale(1); 219 | transform: scale(1); } 220 | 45% { 221 | opacity: 0.7; 222 | -webkit-transform: scale(0.75); 223 | transform: scale(0.75); } 224 | 100% { 225 | opacity: 0.9; 226 | -webkit-transform: scale(1); 227 | transform: scale(1); } } 228 | 229 | @keyframes joyride-beacon-outer { 230 | 0% { 231 | -webkit-transform: scale(1); 232 | transform: scale(1); } 233 | 45% { 234 | opacity: 0.7; 235 | -webkit-transform: scale(0.75); 236 | transform: scale(0.75); } 237 | 100% { 238 | opacity: 0.9; 239 | -webkit-transform: scale(1); 240 | transform: scale(1); } } 241 | -------------------------------------------------------------------------------- /src/actions/workspaceActions.js: -------------------------------------------------------------------------------- 1 | import { bothSlotsSelected } from '../utils' 2 | 3 | import { 4 | BRICK, 5 | PRIMITIVE, 6 | SELECTABLE_PIPE 7 | } from '../utils/componentNames' 8 | 9 | import { 10 | unique 11 | } from '../utils' 12 | 13 | import { 14 | doesAllInputsHaveValues, 15 | elementInputValueIds 16 | } from '../utils/evalUtils' 17 | 18 | export const ADD_BRICK = 'ADD_BRICK' 19 | export const ADD_PIPE = 'ADD_PIPE' 20 | export const ADD_PRIMITIVE = 'ADD_PRIMITIVE' 21 | export const ADD_UNIT_TEST = 'ADD_UNIT_TEST' 22 | export const CHANGE_PRIMITIVE_VALUE = 'CHANGE_PRIMITIVE_VALUE' 23 | export const CHANGE_TEST_NODE_TYPE = 'CHANGE_TEST_NODE_TYPE' 24 | export const CHANGE_TEST_NODE_VALUE = 'CHANGE_TEST_NODE_VALUE' 25 | export const CLEAR_SLOT_SELECTION = 'CLEAR_SLOT_SELECTION' 26 | export const EVALUATE = 'EVALUATE' 27 | export const INIT_WORKSPACE = 'INIT_WORKSPACE' 28 | export const LINK_SLOTS = 'LINK_SLOTS' 29 | export const MOVE_ELEMENT = 'MOVE_ELEMENT' 30 | export const REMOVE_ELEMENT = 'REMOVE_ELEMENT' 31 | export const REMOVE_SELECTED_ELEMENT = 'REMOVE_SELECTED_ELEMENT' 32 | export const REMOVE_UNIT_TEST = 'REMOVE_UNIT_TEST' 33 | export const START_DRAG = 'START_DRAG' 34 | export const STOP_DRAG = 'STOP_DRAG' 35 | export const SELECT_ELEMENT = 'SELECT_ELEMENT' 36 | export const SELECT_SLOT = 'SELECT_SLOT' 37 | export const UNEVALUATE = 'UNEVALUATE' 38 | export const UNLINK_SLOTS = 'UNLINK_SLOTS' 39 | 40 | export const initWorkspace = (type) => { 41 | return { 42 | type: INIT_WORKSPACE, 43 | payload: type 44 | } 45 | } 46 | 47 | export const addBrick = (brick) => { 48 | return (dispatch, getState) => { 49 | dispatch(removeSelectedElement()) 50 | dispatch(_addBrick(brick)) 51 | } 52 | } 53 | 54 | const _addBrick = (brick) => { 55 | return { 56 | type: ADD_BRICK, 57 | payload: brick 58 | } 59 | } 60 | 61 | export const addPrimitive = (type) => { 62 | return (dispatch, getState) => { 63 | dispatch(removeSelectedElement()) 64 | dispatch(_addPrimitive(type)) 65 | } 66 | } 67 | 68 | const _addPrimitive = (type) => { 69 | return { 70 | type: ADD_PRIMITIVE, 71 | payload: type 72 | } 73 | } 74 | 75 | export const startDrag = (elementId, mousePosition, elementPosition) => { 76 | return { 77 | type: START_DRAG, 78 | payload: { 79 | elementId, 80 | mousePosition, 81 | elementPosition 82 | } 83 | } 84 | } 85 | 86 | export const selectElementOrStopDrag = (mousePosition) => { 87 | return (dispatch, getState) => { 88 | const { element } = getState().workspace.selectionState 89 | 90 | dispatch(stopDrag()) 91 | 92 | if(element.mouseDownPosition.x === mousePosition.x && 93 | element.mouseDownPosition.y === mousePosition.y ) { 94 | dispatch(selectElement(element.id, mousePosition)) 95 | } else { 96 | dispatch(removeSelectedElement()) 97 | } 98 | } 99 | } 100 | 101 | export const stopDrag = () => { 102 | return { 103 | type: STOP_DRAG 104 | } 105 | } 106 | 107 | export const moveElement = (currentMousePosition) => { 108 | return { 109 | type: MOVE_ELEMENT, 110 | payload: { 111 | currentMousePosition 112 | } 113 | } 114 | } 115 | 116 | export const addPipeOrSelectSlot = (type, elementId, slotId) => { 117 | return (dispatch, getState) => { 118 | dispatch(removeSelectedElement()) 119 | dispatch(selectSlot(type, elementId, slotId)) 120 | dispatch(_addPipeIfBothSlotsSelected()) 121 | } 122 | } 123 | 124 | // type: 'input' or 'output' slot 125 | export const selectSlot = (type, elementId, slotId) => { 126 | return { 127 | type: SELECT_SLOT, 128 | payload: { 129 | elementId, 130 | slotId, 131 | type 132 | } 133 | } 134 | } 135 | 136 | const _addPipeIfBothSlotsSelected = () => { 137 | return (dispatch, getState) => { 138 | const { workspace } = getState() 139 | const { pipe } = workspace.selectionState 140 | 141 | if(bothSlotsSelected(pipe)) { 142 | dispatch(_addPipe(pipe)) 143 | dispatch(_clearSlotSelection()) 144 | dispatch(_linkSlots(pipe)) 145 | dispatch(_evalAllWorkspacesIfNeeded(pipe.output.elementId)) 146 | } 147 | } 148 | } 149 | 150 | const _evalAllWorkspacesIfNeeded = (elementId) => { 151 | return (dispatch, getState) => { 152 | const { workspace } = getState() 153 | 154 | if(_shouldEval(elementId, workspace)) { 155 | return dispatch(_evalWorkspaces(elementId)) 156 | } 157 | } 158 | } 159 | 160 | const _shouldEval = (elementId, workspace) => { 161 | const element = workspace.entities[elementId] 162 | 163 | if(element.componentName != BRICK) { 164 | return false 165 | } 166 | 167 | const valueIds = elementInputValueIds(element) 168 | const shouldEvalWorkspaces = workspace.unitTests.map((unitTest) => 169 | doesAllInputsHaveValues(element, valueIds, unitTest) 170 | ) 171 | 172 | return shouldEvalWorkspaces.filter((shouldEval) => shouldEval).length > 0 173 | } 174 | 175 | const _evalWorkspaces = (elementId) => { 176 | return { 177 | type: EVALUATE, 178 | payload: elementId 179 | } 180 | } 181 | 182 | const _addPipe = (pipe) => { 183 | return { 184 | type: ADD_PIPE, 185 | payload: pipe 186 | } 187 | } 188 | 189 | const _linkSlots = (pipe) => { 190 | return { 191 | type: LINK_SLOTS, 192 | payload: pipe 193 | } 194 | } 195 | 196 | const _clearSlotSelection = () => { 197 | return { 198 | type: CLEAR_SLOT_SELECTION 199 | } 200 | } 201 | 202 | export const selectElement = (elementId, mousePosition, workspaceIndex) => { 203 | return { 204 | type: SELECT_ELEMENT, 205 | payload: { 206 | elementId, 207 | mousePosition, 208 | workspaceIndex 209 | } 210 | } 211 | } 212 | 213 | export const removeElement = (elementId) => { 214 | return (dispatch, getState) => { 215 | const { workspace } = getState() 216 | const element = workspace.entities[elementId] 217 | 218 | if(element.componentName == SELECTABLE_PIPE) { 219 | dispatch(_unlinkSlots(element)) 220 | } 221 | 222 | if(element.componentName == PRIMITIVE || element.componentName == BRICK) { 223 | const slotId = Object.keys(element.outputSlots)[0] 224 | const slot = element.outputSlots[slotId] 225 | 226 | slot.outputElementIds.forEach((outputElementId) => { 227 | dispatch( 228 | _unlinkSlots({ 229 | input: { 230 | elementId: element.id, 231 | slotId 232 | }, 233 | output: { 234 | elementId: outputElementId, 235 | sourceElementId: element.id 236 | } 237 | }) 238 | ) 239 | }) 240 | } 241 | 242 | if(element.componentName == BRICK) { 243 | Object.keys(element.inputSlots).forEach((slotId) => { 244 | const slot = element.inputSlots[slotId] 245 | 246 | if(slot.value) { 247 | dispatch( 248 | _unlinkSlots({ 249 | input: slot.value, 250 | output: { 251 | elementId: element.id, 252 | slotId: slot.id 253 | }, 254 | }) 255 | ) 256 | } 257 | }) 258 | } 259 | 260 | dispatch(removeSelectedElement()) 261 | dispatch(_uneval(elementId)) 262 | dispatch(_removeElement(elementId)) 263 | } 264 | } 265 | 266 | const _removeElement = (elementId) => { 267 | return { 268 | type: REMOVE_ELEMENT, 269 | payload: { 270 | elementId 271 | } 272 | } 273 | } 274 | 275 | const _uneval = (elementId) => { 276 | return { 277 | type: UNEVALUATE, 278 | payload: elementId 279 | } 280 | } 281 | 282 | const _unlinkSlots = (element) => { 283 | return { 284 | type: UNLINK_SLOTS, 285 | payload: { 286 | input: element.input, 287 | output: element.output 288 | } 289 | } 290 | } 291 | 292 | export const removeSelectedElement = () => { 293 | return { 294 | type: REMOVE_SELECTED_ELEMENT 295 | } 296 | } 297 | 298 | export const addUnitTest = () => { 299 | return (dispatch, getState) => { 300 | dispatch(removeSelectedElement()) 301 | dispatch(_addUnitTest()) 302 | } 303 | } 304 | 305 | const _addUnitTest = () => { 306 | return { 307 | type: ADD_UNIT_TEST 308 | } 309 | } 310 | 311 | export const changePrimitiveValue = (elementId, newValue) => { 312 | return (dispatch, getState) => { 313 | const { workspace } = getState() 314 | const primitive = workspace.entities[elementId] 315 | const { outputElementIds } = primitive.outputSlots[elementId] 316 | 317 | dispatch(_changePrimitiveValue(elementId, newValue)) 318 | 319 | unique(outputElementIds).forEach((outputElementId) => 320 | dispatch(_evalAllWorkspacesIfNeeded(outputElementId)) 321 | ) 322 | } 323 | } 324 | 325 | const _changePrimitiveValue = (elementId, newValue) => { 326 | return { 327 | type: CHANGE_PRIMITIVE_VALUE, 328 | payload: { 329 | elementId, 330 | newValue 331 | } 332 | } 333 | } 334 | 335 | export const changeTestNodeType = (elementId, newType, workspaceIndex) => { 336 | return (dispatch, getState) => { 337 | const { workspace } = getState() 338 | const testNode = workspace.entities[elementId] 339 | 340 | dispatch(_changeTestNodeType(testNode, newType, workspaceIndex)) 341 | } 342 | } 343 | 344 | const _changeTestNodeType = (testNode, newType, workspaceIndex) => { 345 | return { 346 | type: CHANGE_TEST_NODE_TYPE, 347 | payload: { 348 | componentName: testNode.componentName, 349 | elementId: testNode.id, 350 | newType, 351 | workspaceIndex 352 | } 353 | } 354 | } 355 | 356 | export const changeTestNodeValue = (elementId, newValue, workspaceIndex) => { 357 | return (dispatch, getState) => { 358 | const { workspace } = getState() 359 | const testNode = workspace.entities[elementId] 360 | 361 | dispatch(_changeTestNodeValue(testNode, newValue, workspaceIndex)) 362 | 363 | if(testNode.outputSlots){ 364 | const { outputElementIds } = testNode.outputSlots[elementId] 365 | 366 | unique(outputElementIds).forEach((outputElementId) => 367 | dispatch(_evalAllWorkspacesIfNeeded(outputElementId)) 368 | ) 369 | } 370 | } 371 | } 372 | 373 | const _changeTestNodeValue = (testNode, newValue, workspaceIndex) => { 374 | return { 375 | type: CHANGE_TEST_NODE_VALUE, 376 | payload: { 377 | componentName: testNode.componentName, 378 | elementId: testNode.id, 379 | newValue, 380 | workspaceIndex 381 | } 382 | } 383 | } 384 | 385 | export const removeUnitTest = (workspaceIndex) => { 386 | return (dispatch, getState) => { 387 | dispatch(removeSelectedElement()) 388 | dispatch(_removeUnitTest(workspaceIndex)) 389 | } 390 | } 391 | 392 | const _removeUnitTest = (workspaceIndex) => { 393 | return { 394 | type: REMOVE_UNIT_TEST, 395 | payload: workspaceIndex 396 | } 397 | } 398 | -------------------------------------------------------------------------------- /LICENSE-AGPL: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these terms. 626 | 627 | To do so, attach the following notices to the program. It is safest 628 | to attach them to the start of each source file to most effectively 629 | state the exclusion of warranty; and each file should have at least 630 | the "copyright" line and a pointer to where the full notice is found. 631 | 632 | 633 | Copyright (C) 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU Affero General Public License as published by 637 | the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU Affero General Public License for more details. 644 | 645 | You should have received a copy of the GNU Affero General Public License 646 | along with this program. If not, see . 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If your software can interact with users remotely through a computer 651 | network, you should also make sure that it provides a way for users to 652 | get its source. For example, if your program is a web application, its 653 | interface could display a "Source" link that leads users to an archive 654 | of the code. There are many ways you could offer source, and different 655 | solutions will be better for different programs; see section 13 for the 656 | specific requirements. 657 | 658 | You should also get your employer (if you work as a programmer) or school, 659 | if any, to sign a "copyright disclaimer" for the program, if necessary. 660 | For more information on this, and how to apply and follow the GNU AGPL, see 661 | . 662 | --------------------------------------------------------------------------------