├── .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 |The library has 2 components:
', 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 |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 |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 |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 |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.