├── .babelrc ├── .gitignore ├── .gitmodules ├── README.md ├── config ├── aws-config-cognito.js ├── webpack.config.base.js ├── webpack.config.development.js └── webpack.config.production.js ├── deploy.cmd ├── docs └── app strcuture.png ├── library ├── bundleJspsych.cmd ├── closure-compiler.jar ├── migrate-to-mui-v1-hepler.py └── updateJspsych.cmd ├── package-lock.json ├── package.json ├── public ├── index.html ├── jsPsych │ ├── jspsych-favicon.png │ ├── jspsych-logo-readme.jpg │ ├── jspsych.css │ └── jspsych.min.js ├── static │ ├── bundle.js │ └── bundle.js.map ├── style.css └── template.js └── src ├── client └── index.js ├── cloud ├── auth.js ├── aws-cognito-config.js ├── dynamodb.js ├── index.js └── s3.js ├── common ├── actions │ ├── editorActions.js │ ├── experimentSettingActions.js │ ├── organizerActions.js │ └── userActions.js ├── backend │ └── deploy │ │ └── index.js ├── components │ ├── App.js │ ├── Appbar │ │ ├── Appbar.jsx │ │ ├── CloudDeploymentManager │ │ │ ├── CloudDeploymentManager.jsx │ │ │ └── index.js │ │ ├── DIYDeploymentManager │ │ │ ├── DIYDeploymentManager.jsx │ │ │ └── index.js │ │ ├── UserMenu │ │ │ ├── ExperimentList │ │ │ │ ├── ExperimentList.jsx │ │ │ │ └── index.js │ │ │ ├── Profile │ │ │ │ └── index.js │ │ │ ├── UserMenu.jsx │ │ │ └── index.js │ │ ├── index.js │ │ ├── jsPsychInitEditor │ │ │ └── index.js │ │ └── theme.js │ ├── ArrayEditor │ │ └── index.js │ ├── Authentications │ │ ├── Authentications.js │ │ ├── ForgotPasswordWindow.js │ │ ├── RegisterWindow.js │ │ ├── SignInWindow.js │ │ ├── VerificationWindow.js │ │ └── index.js │ ├── CodeEditor │ │ └── index.js │ ├── KeyboardSelector │ │ └── index.js │ ├── MediaManager │ │ ├── MediaManager.jsx │ │ └── index.js │ ├── Notifications │ │ ├── Notifications.jsx │ │ └── index.js │ ├── ObjectEditor │ │ └── index.js │ ├── PreviewWindow │ │ ├── ZoomBar.js │ │ └── index.js │ ├── TimelineNodeEditor │ │ ├── CommonComponents │ │ │ ├── CommonComponents.jsx │ │ │ └── index.js │ │ ├── TimelineForm │ │ │ ├── TimelineForm.jsx │ │ │ ├── TimelineVariableTable.js │ │ │ └── index.js │ │ ├── TimelineNodeEditor.css │ │ ├── TimelineNodeEditor.jsx │ │ ├── TrialForm │ │ │ ├── TimelineVariableSelector.js │ │ │ ├── TrialFormItem │ │ │ │ ├── TrialFormItem.jsx │ │ │ │ ├── index.js │ │ │ │ └── utils.js │ │ │ └── index.js │ │ └── index.js │ ├── TimelineNodeOrganizer │ │ ├── SortableTreeMenu │ │ │ ├── NestedContextMenus.js │ │ │ ├── TimelineItem.js │ │ │ ├── Tree.js │ │ │ ├── TreeNode.js │ │ │ ├── TrialItem.js │ │ │ ├── index.js │ │ │ └── theme.js │ │ ├── TimelineNodeOrganizer.jsx │ │ └── index.js │ ├── gadgets │ │ └── index.js │ └── theme.js ├── constants │ ├── ActionTypes.js │ ├── Errors.js │ ├── core.js │ ├── enumerators.js │ └── theme.js ├── containers │ ├── AppContainer.js │ ├── Appbar │ │ ├── AppbarContainer.js │ │ ├── CloudDeploymentManager │ │ │ ├── CloudDeploymentManagerContainer.js │ │ │ └── index.js │ │ ├── DIYDeploymentManager │ │ │ ├── DIYDeploymentManagerContainer.js │ │ │ └── index.js │ │ ├── UserMenu │ │ │ ├── ExperimentList │ │ │ │ ├── ExperimentListContainer.js │ │ │ │ └── index.js │ │ │ ├── Profile │ │ │ │ └── index.js │ │ │ ├── UserMenuContainer.js │ │ │ └── index.js │ │ ├── index.js │ │ └── jsPsychInitEditor │ │ │ └── index.js │ ├── ArrayEditor │ │ └── index.js │ ├── Authentications │ │ ├── AuthenticationsContainer.js │ │ └── index.js │ ├── MediaManager │ │ ├── MediaManagerContainer.js │ │ └── index.js │ ├── Notifications │ │ ├── NotificationsContainer.js │ │ └── index.js │ ├── ObjectEditor │ │ └── index.js │ ├── PreviewWindow │ │ └── index.js │ ├── TimelineNodeEditor │ │ ├── TimelineForm │ │ │ ├── TimelineFormContainer.js │ │ │ ├── TimelineVariableTableContainer.js │ │ │ └── index.js │ │ ├── TimelineNodeEditorContainer.js │ │ ├── TrialForm │ │ │ ├── TimelineVariableSelectorContainer.js │ │ │ ├── TrialFormItemContainer.js │ │ │ └── index.js │ │ └── index.js │ ├── TimelineNodeOrganizer │ │ ├── SortableTreeMenu │ │ │ ├── TimelineItemContainer.js │ │ │ ├── TreeContainer.js │ │ │ ├── TreeNodeContainer.js │ │ │ ├── TrialItemContainer.js │ │ │ └── index.js │ │ ├── TimelineNodeOrganizerContainer.js │ │ └── index.js │ └── commonFlows.js ├── reducers │ ├── Authentications │ │ ├── authenticationsReducer.js │ │ └── index.js │ ├── Experiment │ │ ├── editor.js │ │ ├── index.js │ │ ├── jsPsychInit.js │ │ ├── organizer.js │ │ ├── tests │ │ │ ├── editor.test.js │ │ │ ├── initSetting.test.js │ │ │ ├── jspsych.js │ │ │ └── organizer.test.js │ │ └── utils │ │ │ └── index.js │ ├── Notifications │ │ └── index.js │ ├── User │ │ ├── index.js │ │ └── tests │ │ │ └── user.test.js │ └── index.js └── utils │ └── index.js ├── index.js └── server └── dev-server.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react", "env", "stage-2"] 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | build 31 | node_modules 32 | jspm_packages 33 | 34 | # Optional npm cache directory 35 | .npm 36 | 37 | # Optional REPL history 38 | .node_repl_history 39 | 40 | *.duplicate.* 41 | 42 | # Windows thumbnail cache files 43 | Thumbs.db 44 | ehthumbs.db 45 | ehthumbs_vista.db 46 | 47 | # Dump file 48 | *.stackdump 49 | 50 | # Folder config file 51 | [Dd]esktop.ini 52 | 53 | # Recycle Bin used on file shares 54 | $RECYCLE.BIN/ 55 | 56 | # Windows Installer files 57 | *.cab 58 | *.msi 59 | *.msm 60 | *.msp 61 | 62 | # Windows shortcuts 63 | *.lnk -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "library/jsPsych"] 2 | path = library/jsPsych 3 | url = https://github.com/jspsych/jsPsych 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jsPsych-Redux-GUI 2 | A browser-based interface for creating experiments with jsPsych 3 | http://builder.jspsych.org/ 4 | 5 | Install - npm install
6 | Development Server - npm run dev
7 | Production Build - npm run build
8 | -------------------------------------------------------------------------------- /config/aws-config-cognito.js: -------------------------------------------------------------------------------- 1 | exports.cognitoConfig = { 2 | region: 'us-east-2', 3 | IdentityPoolId: 'us-east-2:03654ec9-25fb-421c-b08b-e824354f9b6f', 4 | UserPoolId: 'us-east-2_1Lk3mA2UO', 5 | ClientId: '35jh4lt7qr6u84k64e7b4mlqfq', 6 | } 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /config/webpack.config.base.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | module.exports = { 5 | entry: [ 6 | "babel-polyfill" 7 | ], 8 | resolve: { 9 | alias: { 10 | utils: path.resolve(__dirname, '../src/common/utils/index.js'), 11 | enums: path.resolve(__dirname, '../src/common/constants/enumerators.js'), 12 | actions: path.resolve(__dirname, '../src/common/constants/ActionTypes.js'), 13 | theme: path.resolve(__dirname, '../src/common/constants/theme.js'), 14 | core: path.resolve(__dirname, '../src/common/constants/core.js'), 15 | myaws: path.resolve(__dirname, '../src/cloud/index.js'), 16 | errors: path.resolve(__dirname, '../src/common/constants/Errors.js'), 17 | } 18 | }, 19 | plugins: [ 20 | new webpack.ProvidePlugin({ 21 | utils: 'utils', 22 | enums: 'enums', 23 | actions: 'actions', 24 | theme: 'theme', 25 | core: 'core', 26 | myaws: 'myaws', 27 | errors: 'errors' 28 | }) 29 | ], 30 | module: { 31 | rules: [{ 32 | test: /\.css$/, 33 | use: ['style-loader', 'css-loader'] 34 | }, { 35 | test: /\.jsx?$/, 36 | loader: 'babel-loader', 37 | exclude: /node_modules/, 38 | include: path.resolve(__dirname, '../'), 39 | query: { 40 | presets: ['env', 'react', 'stage-2'] 41 | } 42 | }, { 43 | test: /\.json$/, 44 | loader: "json-loader" 45 | }] 46 | }, 47 | node: { 48 | fs: "empty", 49 | module: "empty", 50 | net: "empty" 51 | } 52 | } -------------------------------------------------------------------------------- /config/webpack.config.development.js: -------------------------------------------------------------------------------- 1 | var merge = require('webpack-merge'); 2 | var baseConfig = require("./webpack.config.base"); 3 | var path = require('path'); 4 | var webpack = require('webpack'); 5 | 6 | module.exports = merge(baseConfig, { 7 | devtool: 'eval', 8 | entry: [ 9 | 'webpack-hot-middleware/client', 10 | path.resolve(__dirname, '../src/client/index.js') 11 | ], 12 | output: { 13 | path: path.resolve(__dirname, 'dist'), 14 | filename: 'bundle.js', 15 | publicPath: '/static/' 16 | }, 17 | plugins: [ 18 | new webpack.HotModuleReplacementPlugin(), 19 | ], 20 | }) 21 | 22 | -------------------------------------------------------------------------------- /config/webpack.config.production.js: -------------------------------------------------------------------------------- 1 | var merge = require('webpack-merge'); 2 | var baseConfig = require("./webpack.config.base"); 3 | var path = require('path'); 4 | var webpack = require('webpack'); 5 | var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; 6 | 7 | module.exports = merge(baseConfig, { 8 | devtool: 'cheap-module-source-map', 9 | entry: [ 10 | path.resolve(__dirname, '../src/client/index.js'), 11 | ], 12 | output: { 13 | path: path.resolve(__dirname, '../public/static'), 14 | filename: 'bundle.js', 15 | publicPath: '/static/' 16 | }, 17 | plugins: [ 18 | // new BundleAnalyzerPlugin(), 19 | new webpack.DefinePlugin({ 20 | 'process.env.NODE_ENV': JSON.stringify('production') 21 | }), 22 | new webpack.optimize.UglifyJsPlugin({ 23 | sourceMap: true, 24 | mangle: { except: ['exports'] }, 25 | compress: { 26 | warnings: false, // Suppress uglification warnings 27 | pure_getters: true, 28 | unsafe: true, 29 | unsafe_comps: true, 30 | screw_ie8: true 31 | }, 32 | output: { 33 | comments: false, 34 | }, 35 | exclude: [/\.min\.js$/gi] // skip pre-minified libs 36 | }), 37 | new webpack.optimize.AggressiveMergingPlugin(), 38 | new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/) 39 | ] 40 | }) 41 | -------------------------------------------------------------------------------- /deploy.cmd: -------------------------------------------------------------------------------- 1 | @cmd /c "cd library && updateJspsych.cmd && exit" 2 | @aws s3 sync .\public s3://builder.jspsych.org 3 | @echo. 4 | @echo done... -------------------------------------------------------------------------------- /docs/app strcuture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jspsych/jsPsych-Redux-GUI/f6fee6f3f1678b15f244404645335f5ea1d7b6b4/docs/app strcuture.png -------------------------------------------------------------------------------- /library/bundleJspsych.cmd: -------------------------------------------------------------------------------- 1 | @del ..\public\jsPsych\jspsych.min.js 2 | @del ..\public\jsPsych\jspsych.css 3 | @java -jar closure-compiler.jar --js ./jspsych/jspsych.js ./jspsych/plugins/*.js --js_output_file ../public/jsPsych/jspsych.min.js 4 | @echo Built "../public/jsPsych/jspsych.min.js" 5 | copy /y .\jspsych\css\jspsych.css ..\public\jsPsych\ 6 | @echo. 7 | @echo Finish bundling jsPsych... -------------------------------------------------------------------------------- /library/closure-compiler.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jspsych/jsPsych-Redux-GUI/f6fee6f3f1678b15f244404645335f5ea1d7b6b4/library/closure-compiler.jar -------------------------------------------------------------------------------- /library/migrate-to-mui-v1-hepler.py: -------------------------------------------------------------------------------- 1 | import os, re 2 | 3 | def getJSFiles(d): 4 | d = os.path.abspath(d) 5 | res = [] 6 | for root, dirs, files in os.walk(d): 7 | for name in files: 8 | if name.endswith(('.js', 'jsx')): 9 | res.append(os.path.join(root, name)) 10 | return res 11 | 12 | def myStr(s): 13 | s = s.split('-') 14 | return ''.join([a.capitalize() for a in s]) 15 | 16 | def resolveIconPath(files): 17 | for f in files: 18 | src = open(f).read() 19 | pairs = [(item[0]+item[1], 'material-ui-icons/' + myStr(item[1])) for item in re.findall("(material-ui/svg-icons/.*?/)(.*?)('|\")", src)] 20 | for p in pairs: 21 | src = src.replace(p[0], p[1]) 22 | wrt = open(f, 'w') 23 | wrt.write(src) 24 | wrt.close() 25 | 26 | def main(): 27 | files = getJSFiles('../src/common/components') 28 | resolveIconPath(files) 29 | 30 | 31 | if __name__ == '__main__': 32 | main() 33 | -------------------------------------------------------------------------------- /library/updateJspsych.cmd: -------------------------------------------------------------------------------- 1 | @echo git submodule update --init --force --remote 2 | @git submodule update --init --force --remote 3 | @echo. 4 | @echo Finish fetching jsPsych... 5 | @echo. 6 | @cmd /c "bundleJspsych.cmd && exit" -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jsPsych-Redux-GUI", 3 | "version": "0.1.0", 4 | "babel": {}, 5 | "private": true, 6 | "jest": { 7 | "testPathIgnorePatterns": [ 8 | "/node_modules/", 9 | "/jsPsych/" 10 | ] 11 | }, 12 | "devDependencies": { 13 | "babel-cli": "^6.26.0", 14 | "babel-core": "^6.26.3", 15 | "babel-loader": "^7.1.1", 16 | "babel-preset-env": "^1.7.0", 17 | "babel-preset-react": "^6.24.1", 18 | "babel-preset-stage-2": "^6.24.1", 19 | "css-loader": "^2.1.1", 20 | "express": "^4.16.2", 21 | "jest": "^24.5.0", 22 | "json-loader": "^0.5.4", 23 | "react-scripts": "^2.1.8", 24 | "style-loader": "^0.18.2", 25 | "webpack": "^3.10.0", 26 | "webpack-bundle-analyzer": "^2.9.2", 27 | "webpack-merge": "^4.1.1", 28 | "webpack-preset": "^0.2.0" 29 | }, 30 | "dependencies": { 31 | "aws-amplify": "^0.4.1", 32 | "aws-sdk": "^2.430.0", 33 | "babel-polyfill": "^6.26.0", 34 | "bluebird": "^3.5.0", 35 | "copy-to-clipboard": "^3.0.6", 36 | "file-type": "^5.2.0", 37 | "filesaver.js-npm": "^1.0.1", 38 | "inline-style-prefixer": "^4.0.0", 39 | "jszip": "^3.1.3", 40 | "lodash": "^4.17.11", 41 | "material-ui": "^0.20.0", 42 | "react": "^16.3.2", 43 | "react-codemirror": "^1.0.0", 44 | "react-contextmenu": "^2.6.5", 45 | "react-dnd": "^2.4.0", 46 | "react-dnd-html5-backend": "^2.4.1", 47 | "react-dom": "^16.3.2", 48 | "react-dropzone": "^3.13.3", 49 | "react-redux": "^5.0.2", 50 | "react-speed-dial": "^0.4.7", 51 | "redux": "^3.6.0", 52 | "redux-thunk": "^2.2.0", 53 | "short-uuid": "^2.3.3" 54 | }, 55 | "scripts": { 56 | "dev": "babel-node ./src/server/dev-server", 57 | "start": "react-scripts start", 58 | "build": "webpack -p --config config/webpack.config.production.js", 59 | "deploy": "deploy.cmd", 60 | "test": "jest", 61 | "test:watch": "npm test -- --watch", 62 | "updateJspsych": "./library/updateJspsych.cmd" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | jsPsych Experiment Builder 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 18 | 19 | -------------------------------------------------------------------------------- /public/jsPsych/jspsych-favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jspsych/jsPsych-Redux-GUI/f6fee6f3f1678b15f244404645335f5ea1d7b6b4/public/jsPsych/jspsych-favicon.png -------------------------------------------------------------------------------- /public/jsPsych/jspsych-logo-readme.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jspsych/jsPsych-Redux-GUI/f6fee6f3f1678b15f244404645335f5ea1d7b6b4/public/jsPsych/jspsych-logo-readme.jpg -------------------------------------------------------------------------------- /public/jsPsych/jspsych.css: -------------------------------------------------------------------------------- 1 | /* 2 | * CSS for jsPsych experiments. 3 | * 4 | * This stylesheet provides minimal styling to make jsPsych 5 | * experiments look polished without any additional styles. 6 | */ 7 | 8 | @import url(https://fonts.googleapis.com/css?family=Open+Sans:400italic,700italic,400,700); 9 | 10 | /* Container holding jsPsych content */ 11 | 12 | .jspsych-display-element { 13 | display: flex; 14 | flex-direction: column; 15 | overflow-y: auto; 16 | } 17 | 18 | .jspsych-display-element:focus { 19 | outline: none; 20 | } 21 | 22 | .jspsych-content-wrapper { 23 | display: flex; 24 | margin: auto; 25 | flex: 1 1 100%; 26 | width: 100%; 27 | } 28 | 29 | .jspsych-content { 30 | max-width: 95%; /* this is mainly an IE 10-11 fix */ 31 | text-align: center; 32 | margin: auto; /* this is for overflowing content */ 33 | } 34 | 35 | .jspsych-top { 36 | align-items: flex-start; 37 | } 38 | 39 | .jspsych-middle { 40 | align-items: center; 41 | } 42 | 43 | /* fonts and type */ 44 | 45 | .jspsych-display-element { 46 | font-family: 'Open Sans', 'Arial', sans-serif; 47 | font-size: 18px; 48 | line-height: 1.6em; 49 | } 50 | 51 | /* Form elements like input fields and buttons */ 52 | 53 | .jspsych-display-element input[type="text"] { 54 | font-family: 'Open Sans', 'Arial', sans-serif; 55 | font-size: 14px; 56 | } 57 | 58 | /* borrowing Bootstrap style for btn elements, but combining styles a bit */ 59 | .jspsych-btn { 60 | display: inline-block; 61 | padding: 6px 12px; 62 | margin: 0px; 63 | font-size: 14px; 64 | font-weight: 400; 65 | font-family: 'Open Sans', 'Arial', sans-serif; 66 | cursor: pointer; 67 | line-height: 1.4; 68 | text-align: center; 69 | white-space: nowrap; 70 | vertical-align: middle; 71 | background-image: none; 72 | border: 1px solid transparent; 73 | border-radius: 4px; 74 | color: #333; 75 | background-color: #fff; 76 | border-color: #ccc; 77 | } 78 | 79 | .jspsych-btn:hover { 80 | background-color: #ddd; 81 | border-color: #aaa; 82 | } 83 | 84 | .jspsych-btn:disabled { 85 | background-color: #eee; 86 | color: #aaa; 87 | border-color: #ccc; 88 | cursor: not-allowed; 89 | } 90 | 91 | /* jsPsych progress bar */ 92 | 93 | #jspsych-progressbar-container { 94 | color: #555; 95 | border-bottom: 1px solid #dedede; 96 | background-color: #f9f9f9; 97 | margin-bottom: 1em; 98 | text-align: center; 99 | padding: 8px 0px; 100 | width: 100%; 101 | line-height: 1em; 102 | } 103 | #jspsych-progressbar-container span { 104 | font-size: 14px; 105 | padding-right: 14px; 106 | } 107 | #jspsych-progressbar-outer { 108 | background-color: #eee; 109 | width: 50%; 110 | margin: auto; 111 | height: 14px; 112 | display: inline-block; 113 | vertical-align: middle; 114 | box-shadow: inset 0 1px 2px rgba(0,0,0,0.1); 115 | } 116 | #jspsych-progressbar-inner { 117 | background-color: #aaa; 118 | width: 0%; 119 | height: 100%; 120 | } 121 | 122 | /* Control appearance of jsPsych.data.displayData() */ 123 | #jspsych-data-display { 124 | text-align: left; 125 | } 126 | -------------------------------------------------------------------------------- /public/static/bundle.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"bundle.js","sources":["webpack:///bundle.js"],"mappings":"AAAA","sourceRoot":""} -------------------------------------------------------------------------------- /public/style.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | height: 100%; 3 | overflow: hidden; 4 | } 5 | 6 | #container { 7 | width: 100%; 8 | height: 100%; 9 | } 10 | 11 | .react-context-menu { 12 | min-width: 160px; 13 | outline: none; 14 | background-color: rgb(255, 255, 255); 15 | color: rgba(0, 0, 0, 0.87); 16 | -webkit-transition: opacity 250ms cubic-bezier(0.23, 1, 0.32, 1) 0ms, -webkit-transform 250ms cubic-bezier(0.23, 1, 0.32, 1) 0ms; 17 | transition: opacity 250ms cubic-bezier(0.23, 1, 0.32, 1) 0ms, -webkit-transform 250ms cubic-bezier(0.23, 1, 0.32, 1) 0ms; 18 | -o-transition: transform 250ms cubic-bezier(0.23, 1, 0.32, 1) 0ms, opacity 250ms cubic-bezier(0.23, 1, 0.32, 1) 0ms; 19 | transition: transform 250ms cubic-bezier(0.23, 1, 0.32, 1) 0ms, opacity 250ms cubic-bezier(0.23, 1, 0.32, 1) 0ms; 20 | transition: transform 250ms cubic-bezier(0.23, 1, 0.32, 1) 0ms, opacity 250ms cubic-bezier(0.23, 1, 0.32, 1) 0ms, -webkit-transform 250ms cubic-bezier(0.23, 1, 0.32, 1) 0ms; 21 | -webkit-box-sizing: border-box; 22 | box-sizing: border-box; 23 | -webkit-box-shadow: rgba(0, 0, 0, 0.12) 0px 1px 6px, rgba(0, 0, 0, 0.12) 0px 1px 4px; 24 | box-shadow: rgba(0, 0, 0, 0.12) 0px 1px 6px, rgba(0, 0, 0, 0.12) 0px 1px 4px; 25 | border-radius: 2px; 26 | max-height: 425px; 27 | overflow-y: auto; 28 | } 29 | 30 | .truncate-long-string { 31 | white-space: nowrap; 32 | overflow: hidden; 33 | -o-text-overflow: ellipsis; 34 | text-overflow: ellipsis; 35 | } -------------------------------------------------------------------------------- /public/template.js: -------------------------------------------------------------------------------- 1 | export default function(preloadedState) { 2 | return ` 3 | 4 | 5 | 6 | 7 | 8 | jsPsych Experiment Builder 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 21 | 22 | 23 | ` 24 | } 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/client/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | import { createStore , applyMiddleware } from 'redux'; 5 | import thunk from 'redux-thunk'; 6 | import rootReducer from '../common/reducers'; 7 | import App from '../common/containers/AppContainer'; 8 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'; 9 | 10 | const store = createStore(rootReducer, applyMiddleware(thunk)); 11 | 12 | window.addEventListener('load', () => { 13 | utils.commonFlows.load({dispatch: store.dispatch}); 14 | }); 15 | 16 | window.addEventListener('beforeunload', (e) => { 17 | let { experimentState } = store.getState(); 18 | if (utils.commonFlows.hasExperimentChanged(experimentState)) { 19 | e.returnValue = true; 20 | return true; 21 | } 22 | }); 23 | 24 | 25 | ReactDOM.render( 26 | 27 | 28 | 29 | 30 | , 31 | document.getElementById('container') 32 | ); 33 | -------------------------------------------------------------------------------- /src/cloud/auth.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file This file defines the wrapped methods of AWS-Amplify AuthClass. 3 | */ 4 | 5 | import { Cognito_Config } from './aws-cognito-config.js'; 6 | import { AWS } from './index.js'; 7 | import Amplify, { Auth } from 'aws-amplify'; 8 | 9 | Amplify.configure({ 10 | Auth: { 11 | ...Cognito_Config 12 | } 13 | }); 14 | 15 | /** 16 | * Wrapped Auth.signIn function 17 | * @param {string} username - username 18 | * @param {string} password - password 19 | * @return {Promise} - A promise that resolves to current user info if successful 20 | */ 21 | export const signIn = ({username, password}) => { 22 | return Auth.signIn(username, password).then(myaws.Auth.setCredentials).then(myaws.Auth.getCurrentUserInfo); 23 | } 24 | 25 | /** 26 | * Wrapped Auth.essentailCredential function that also sets credentials of the AWS sdk object 27 | * @return {Promise} - A promise that resolves to signed in user's credential 28 | */ 29 | export const setCredentials = () => { 30 | return Auth.currentCredentials().then(credentials => { 31 | let essentialCredentials = Auth.essentialCredentials(credentials); 32 | AWS.config.credentials = essentialCredentials; 33 | 34 | return essentialCredentials; 35 | }).catch(err => { 36 | // not signed in 37 | if (err.code === 'NotAuthorizedException') { 38 | console.log(err.message); 39 | return Promise.resolve(null); 40 | } 41 | throw err; 42 | }); 43 | } 44 | 45 | /** 46 | * Wrapped Auth.signUp function 47 | * @param {string} username - username 48 | * @param {string} password - password 49 | * @param {Object} attributes - user attributes 50 | * @param {Array} validationData - user's validation data 51 | * @return {Promise} - A promise that resolves if success 52 | */ 53 | export const signUp = ({username, password, attributes={}, validationData=[]}) => { 54 | return Auth.signUp({ 55 | username: username, 56 | password: password, 57 | attributes: attributes, 58 | validationData: validationData 59 | }); 60 | } 61 | 62 | /** 63 | * Wrapped Auth.signOut function 64 | * @return {Promise} - A promise that resolves if success 65 | */ 66 | export const signOut = () => { 67 | return Auth.signOut(); 68 | } 69 | 70 | /** 71 | * Wrapped Auth.forgotPassword function 72 | * @param {string} username - username 73 | * @return {Promise} - A promise that resolves if success 74 | */ 75 | export const forgotPassword = ({username}) => { 76 | return Auth.forgotPassword(username); 77 | } 78 | 79 | /** 80 | * Wrapped Auth.forgotPasswordSubmit function 81 | * @param {string} username - username 82 | * @param {string} code - verification code 83 | * @param {string} new_password - new password 84 | * @return {Promise} - A promise that resolves if success 85 | */ 86 | export const forgotPasswordSubmit = ({username, code, new_password}) => { 87 | return Auth.forgotPasswordSubmit(username, code, new_password); 88 | } 89 | 90 | /** 91 | * Wrapped Auth.resendSignUp function 92 | * @param {string} username - username 93 | * @return {Promise} - A promise that resolves if success 94 | */ 95 | export const resendVerification = ({username}) => { 96 | return Auth.resendSignUp(username) 97 | } 98 | 99 | /** 100 | * Wrapped Auth.confirmSignUp function 101 | * @param {string} username - username 102 | * @param {string} code - verification code 103 | * @return {Promise} - A promise that resolves if success 104 | */ 105 | export const confirmSignUp = ({username, code}) => { 106 | return Auth.confirmSignUp(username, code) 107 | } 108 | 109 | /** 110 | * Wrapped Auth.currentUserInfo function 111 | * @return {Promise} - A promise that resolves to an object that contains successfully signed in user's info 112 | */ 113 | export const getCurrentUserInfo = () => { 114 | return Auth.currentUserInfo().then(info => { 115 | if (info === null) { 116 | return null; 117 | } 118 | return { 119 | userId: info.id, 120 | username: info.username, 121 | verified: info.attributes.email_verified, 122 | email: info.attributes.email 123 | } 124 | }); 125 | } -------------------------------------------------------------------------------- /src/cloud/aws-cognito-config.js: -------------------------------------------------------------------------------- 1 | exports.Cognito_Config = { 2 | region: 'us-east-2', 3 | identityPoolId: 'us-east-2:03654ec9-25fb-421c-b08b-e824354f9b6f', 4 | userPoolId: 'us-east-2_1Lk3mA2UO', 5 | userPoolWebClientId: '35jh4lt7qr6u84k64e7b4mlqfq', 6 | } -------------------------------------------------------------------------------- /src/cloud/dynamodb.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file This file defines the wrapped methods of AWS-DynamoDB and helpers for storing data to dynamoDB. 3 | */ 4 | 5 | import { AWS } from './index.js'; 6 | 7 | const API_VERSION = '2012-08-10'; 8 | const User_Table_Name = "jsPsych_Builder_Users"; 9 | const Experiment_Table_Name = "jsPsych_Builder_Experiments"; 10 | 11 | /** 12 | * Construct a DynamoDB document client 13 | * @return - A DynamoDB document client 14 | */ 15 | function connectDynamoDB() { 16 | return new(AWS.DynamoDB.DocumentClient)({ 17 | apiVersion: API_VERSION, 18 | }); 19 | } 20 | 21 | /** 22 | * Promise wrapper function of dynamoDB.put 23 | * @param {Object} param - parameters 24 | * @return {Promise} - A Promise that resolves if success 25 | */ 26 | function putItem(param) { 27 | return connectDynamoDB().put(param).promise(); 28 | } 29 | 30 | /** 31 | * Promise wrapper function of dynamoDB.get 32 | * @param {Object} param - parameters 33 | * @return {Promise} - A Promise that resolves if success 34 | */ 35 | function getItem(param) { 36 | return connectDynamoDB().get(param).promise(); 37 | } 38 | 39 | /** 40 | * Promise wrapper function of dynamoDB.delete 41 | * @param {Object} param - parameters 42 | * @return {Promise} - A Promise that resolves if success 43 | */ 44 | function deleteItem(param) { 45 | return connectDynamoDB().delete(param).promise(); 46 | } 47 | 48 | /** 49 | * Promise wrapper function of dynamoDB.scan 50 | * @param {Object} param - parameters 51 | * @return {Promise} - A Promise that resolves if success 52 | */ 53 | function scanItem(param) { 54 | return connectDynamoDB().scan(param).promise(); 55 | } 56 | 57 | /** 58 | * Wrapper function that put item to user table 59 | * @param {Object} data - Item to be put. (return value of extractUserData) 60 | * @return {Promise} - A Promise that resolves if success 61 | */ 62 | function putItemToUserTable(data) { 63 | let param = { 64 | TableName: User_Table_Name, 65 | Item: { 66 | ...data 67 | }, 68 | ReturnConsumedCapacity: "TOTAL", 69 | }; 70 | 71 | return putItem(param); 72 | } 73 | 74 | /** 75 | * Wrapper function that put item to experiment table 76 | * @param {Object} data - Item to be put. (return value of extractExperimentData) 77 | * @return {Promise} - A Promise that resolves if success 78 | */ 79 | function putItemToExperimentTable(data) { 80 | let param = { 81 | TableName: Experiment_Table_Name, 82 | Item: { 83 | ...data 84 | }, 85 | ReturnConsumedCapacity: "TOTAL", 86 | } 87 | 88 | return putItem(param); 89 | } 90 | 91 | /** 92 | * Process the userState so that it is ready to be put into User Table 93 | * @param {Object} userState - userState from reduex 94 | * @return {Object} - Key-Value pairs that map the design of user table 95 | */ 96 | function extractUserData(userState) { 97 | return { 98 | userId: userState.userId, 99 | username: userState.username, 100 | fetch: userState 101 | }; 102 | } 103 | 104 | /** 105 | * Process the experimentState so that it is ready to be put into Experiment Table 106 | * @param {Object} experimentState - experimentState from reduex 107 | * @return {Object} - Key-Value pairs that map the design of experiment table 108 | */ 109 | function extractExperimentData(experimentState) { 110 | return { 111 | experimentId: experimentState.experimentId, 112 | fetch: experimentState, 113 | ownerId: experimentState.ownerId, 114 | isPublic: experimentState.isPublic, 115 | createDate: experimentState.createDate, 116 | lastModifiedDate: experimentState.lastModifiedDate 117 | }; 118 | } 119 | 120 | /** 121 | * Wrapper function that fetch user data 122 | * @param {string} id - user's identity id 123 | * @return {Promise} - A Promise that resolves to DynamoDB response if success 124 | */ 125 | export function getUserDate(id) { 126 | let param = { 127 | TableName: User_Table_Name, 128 | Key: { 129 | 'userId': id, 130 | }, 131 | AttributesToGet: [ 'fetch' ] // fetch update local state needed info 132 | }; 133 | return getItem(param); 134 | } 135 | 136 | /** 137 | * Wrapper function that fetch experiment data by id 138 | * @param {string} id - experiment id 139 | * @return {Promise} - A Promise that resolves to DynamoDB response if success 140 | */ 141 | export function getExperimentById(id) { 142 | let param = { 143 | TableName: Experiment_Table_Name, 144 | Key: { 145 | 'experimentId': id 146 | }, 147 | AttributesToGet: [ 'fetch' ] // fetch update local state needed info 148 | }; 149 | return getItem(param); 150 | } 151 | 152 | /** 153 | * Wrapper function that fetch experiments by userid 154 | * @param {string} id - user id 155 | * @return {Promise} - A Promise that resolves to an array of experiments owned by targeted user (userId) if success 156 | */ 157 | export function getExperimentsOf(userId) { 158 | let param = { 159 | TableName: Experiment_Table_Name, 160 | FilterExpression: "#ownerId = :ownerId", 161 | ExpressionAttributeNames: { 162 | "#ownerId": "ownerId" 163 | }, 164 | ExpressionAttributeValues: { 165 | ":ownerId": userId 166 | } 167 | }; 168 | return scanItem(param).then(data => { 169 | let { Items } = data; 170 | return Items.map(item => item.fetch); 171 | }); 172 | } 173 | 174 | /** 175 | * Wrapper function that fetch an experiments by userid and largest last modified date 176 | * @param {string} id - user id 177 | * @return {Promise} - A Promise that resolves to last modified experiment of targeted user (userId) if success 178 | */ 179 | export function getLastModifiedExperimentOf(userId) { 180 | return getExperimentsOf(userId).then(experiments => { 181 | let res = null, max = 0; 182 | for (let experiment of experiments) { 183 | if (experiment.lastModifiedDate > max) { 184 | max = experiment.lastModifiedDate; 185 | res = experiment; 186 | } 187 | } 188 | return res; 189 | }); 190 | } 191 | 192 | /** 193 | * Wrapper function that put userState to User Table 194 | * @param {Object} userState - userState from redux 195 | * @return {Promise} - A Promise that resolves if success 196 | */ 197 | export function saveUserData(userState) { 198 | return putItemToUserTable(extractUserData(userState)); 199 | } 200 | 201 | /** 202 | * Wrapper function that put experimentState to Experiment Table 203 | * @param {Object} experimentState - experimentState from redux 204 | * @return {Promise} - A Promise that resolves if success 205 | */ 206 | export function saveExperiment(experimentState) { 207 | return putItemToExperimentTable(extractExperimentData(experimentState)); 208 | } 209 | 210 | 211 | /** 212 | * Wrapper function that delete an experiment from Experiment Table 213 | * @param {string} experimentId - the id of the experiment to be deleted 214 | * @return {Promise} - A Promise that resolves if success 215 | */ 216 | export function deleteExperiment(experimentId) { 217 | let param = { 218 | TableName: Experiment_Table_Name, 219 | Key: { 220 | experimentId: experimentId 221 | } 222 | } 223 | return deleteItem(param); 224 | } 225 | -------------------------------------------------------------------------------- /src/cloud/index.js: -------------------------------------------------------------------------------- 1 | import { Cognito_Config } from './aws-cognito-config.js'; 2 | import * as auth from './auth.js' 3 | import * as s3 from './s3.js'; 4 | import * as dynamodb from './dynamodb.js'; 5 | 6 | 7 | export var AWS = require('aws-sdk/global'); 8 | require('aws-sdk/clients/s3'); 9 | require('aws-sdk/clients/dynamodb'); 10 | AWS.config.region = Cognito_Config.region; 11 | if (typeof Promise === 'undefined') { 12 | AWS.config.setPromisesDependency(require('bluebird')); 13 | } else { 14 | AWS.config.setPromisesDependency(Promise); 15 | } 16 | 17 | export const Auth = auth; 18 | export const S3 = s3; 19 | export const DynamoDB = dynamodb; 20 | -------------------------------------------------------------------------------- /src/cloud/s3.js: -------------------------------------------------------------------------------- 1 | import { AWS } from './index.js'; 2 | 3 | export const Bucket_Name = "jspsych-builder"; 4 | export const Website_Bucket = "builder.jspsych.org"; 5 | export const Cloud_Bucket = "experiments.jspsych.org"; 6 | export const Api_Version = "2006-03-01"; 7 | export const Delimiter = "/"; 8 | 9 | /** 10 | * Construct an S3 object 11 | * @param {string} bucket - The name of the S3 bucket to be connected 12 | * @return - An S3 object 13 | */ 14 | function connectS3({bucket=Bucket_Name}={}) { 15 | return new AWS.S3({ 16 | apiVersion: Api_Version, 17 | params: { 18 | Bucket: bucket 19 | }, 20 | }); 21 | } 22 | 23 | /* 24 | Returns a promise of S3 API call "putObject" 25 | */ 26 | export function uploadFile({ 27 | param, 28 | progressHook = null, 29 | bucket = Bucket_Name 30 | }) { 31 | if (!progressHook) { 32 | return connectS3({bucket}).putObject({ 33 | ...param 34 | }).promise(); 35 | } else { 36 | return connectS3({bucket}).putObject({ 37 | ...param 38 | }).on('httpUploadProgress', function(evt) { 39 | progressHook(Math.round(evt.loaded * 100 / evt.total)); 40 | }).promise(); 41 | } 42 | } 43 | 44 | /* 45 | Returns require param for S3 API call "putObject" 46 | 47 | file --> should have file.name property (file object) 48 | */ 49 | export function generateUploadParam({Key, Body, ...params}) { 50 | return { 51 | // specified s3 path of to-be-stored file 52 | Key: Key, 53 | // file content 54 | Body: Body, 55 | ...params 56 | }; 57 | } 58 | 59 | /* 60 | Upload a list of files. 61 | 62 | progressHook --> callback that shows user uploading progress 63 | */ 64 | export function uploadFiles({params, progressHook=null, bucket=Bucket_Name}){ 65 | return Promise.all(params.map((param) => { 66 | return uploadFile({ 67 | param: param, 68 | progressHook: progressHook ? (p) => { progressHook(param.Body.name, p) } : (p) => {}, 69 | bucket: bucket 70 | }) 71 | })); 72 | } 73 | 74 | /* 75 | Returns require param for S3 API call "deleteObjects" 76 | */ 77 | function $deleteFiles({param, bucket}){ 78 | return connectS3({bucket}).deleteObjects({ 79 | ...param 80 | }).promise(); 81 | } 82 | 83 | /* 84 | Delete files from S3 bucket 85 | 86 | filePaths --> array of S3 file addresses 87 | */ 88 | export function deleteFiles({filePaths, bucket=Bucket_Name}) { 89 | if (filePaths.length < 1) { 90 | return Promise.resolve("0 file is requested to be deleted."); 91 | } 92 | return $deleteFiles({ 93 | param: { Delete: { Objects: filePaths.map((filePath) => ({Key: filePath})) } }, 94 | bucket, 95 | }); 96 | } 97 | 98 | /* 99 | List bucket contents, the fetched value should be experimentState.media 100 | */ 101 | export function listBucketContents({Prefix, Delimiter = Delimiter, bucket = Bucket_Name}){ 102 | return connectS3({bucket}).listObjectsV2({ 103 | Delimiter: Delimiter, 104 | Prefix: Prefix 105 | }).promise(); 106 | } 107 | 108 | /* 109 | Returns signed url 110 | */ 111 | export function getSignedUrl(Key, Expires=9000) { 112 | return connectS3().getSignedUrl('getObject', { 113 | Key, 114 | Expires 115 | }); 116 | } 117 | 118 | /* 119 | Returns array of signed url 120 | */ 121 | export function getSignedUrls(filePaths) { 122 | return filePaths.map((filePath) => (getSignedUrl(filePath))); 123 | } 124 | 125 | /* 126 | Download file 127 | 128 | key --> S3 file address 129 | callback --> callback that deals with fetched file content 130 | progressHook --> callback that shows user the downloading progress 131 | index --> index of file in its array 132 | */ 133 | export function getFile(key, callback, progressHook, index) { 134 | return connectS3().getObject({ 135 | Key: key 136 | }).on('httpDownloadProgress', function(evt) { 137 | progressHook({value: evt.loaded, index: index}); 138 | }).promise().then((data) => { 139 | callback(key, data.Body); 140 | }); 141 | } 142 | 143 | /* 144 | Download files 145 | */ 146 | export function getFiles(keys, callback, progressHook) { 147 | return Promise.all(keys.map((key, i) => (getFile(key, callback, progressHook, i)))); 148 | } 149 | 150 | /* 151 | Returns param for S3 API call "copyObject" 152 | */ 153 | export function generateCopyParam({ 154 | source, 155 | target, 156 | sourceBucket = Bucket_Name, 157 | targetBucket = Bucket_Name, 158 | ...params 159 | }) { 160 | return { 161 | Bucket: targetBucket, 162 | CopySource: `${sourceBucket}/${source}`, 163 | Key: target, 164 | ...params 165 | }; 166 | } 167 | 168 | /* 169 | Copy S3 file 170 | */ 171 | export function copyFile({param, bucket=Bucket_Name}) { 172 | return connectS3({bucket}).copyObject(param).promise(); 173 | } 174 | 175 | /* 176 | Copy S3 files 177 | */ 178 | export function copyFiles({params, bucket=Bucket_Name}) { 179 | return Promise.all(params.map((param) => (copyFile({param: param, bucket: bucket})))); 180 | } 181 | 182 | 183 | export function getJsPsychLib(callback) { 184 | let prefix = 'jsPsych/' 185 | let libFiles = ['jspsych.css', 'jspsych.min.js']; 186 | 187 | return Promise.all(libFiles.map((name) => { 188 | return connectS3({bucket: Website_Bucket}).getObject({ 189 | Key: prefix + name 190 | }).promise().then((data) => { 191 | callback(name, data.Body); 192 | }); 193 | })); 194 | } 195 | 196 | 197 | export function deleteObject({param, bucket=Bucket_Name}) { 198 | return connectS3({bucket}).deleteObject({...param}).promise(); 199 | } -------------------------------------------------------------------------------- /src/common/actions/editorActions.js: -------------------------------------------------------------------------------- 1 | import * as actionTypes from '../constants/ActionTypes'; 2 | 3 | // update media 4 | export function updateMediaAction(s3files) { 5 | return { 6 | type: actionTypes.UPDATE_MEDIA, 7 | s3files: s3files 8 | }; 9 | } 10 | 11 | /* ********************** Trial form ********************** */ 12 | export function onPluginTypeChange(newPluginVal) { 13 | return { 14 | type: actionTypes.CHANGE_PLUGIN_TYPE, 15 | newPluginVal: newPluginVal 16 | }; 17 | } 18 | 19 | export function setPluginParamAction(key, value, mode="", ifEval, language) { 20 | return { 21 | type: actionTypes.SET_PLUGIN_PARAMTER, 22 | key: key, 23 | value: value, 24 | mode: mode, 25 | // function object 26 | ifEval: ifEval, 27 | language: language 28 | }; 29 | } 30 | 31 | export function setPluginParamModeAction(key, mode, toggle=true) { 32 | return { 33 | type: actionTypes.SET_PLUGIN_PARAMTER_MODE, 34 | key: key, 35 | mode: mode, 36 | toggle: toggle 37 | }; 38 | } 39 | /* ********************** Trial form ********************** */ 40 | 41 | 42 | /* ********************** Timeline form ********************** */ 43 | export function updateCellAction(colName, rowNum, valueObject) { 44 | return { 45 | type: actionTypes.UPDATE_TIMELINE_VARIABLE_CELL, 46 | colName: colName, 47 | rowNum: rowNum, 48 | valueObject: valueObject 49 | } 50 | } 51 | 52 | export function updateTimelineVariableInputTypeAction(variableName, inputType, typeCoercion) { 53 | return { 54 | type: actionTypes.UPDATE_TIMELINE_VARIABLE_INPUT_TYPE, 55 | variableName: variableName, 56 | inputType: inputType, 57 | typeCoercion: typeCoercion 58 | } 59 | } 60 | 61 | export function updateTimelineVariableNameAction(oldName, newName) { 62 | return { 63 | type: actionTypes.UPDATE_TIMELINE_VARIABLE_TABLE_HEADER, 64 | oldName: oldName, 65 | newName: newName 66 | } 67 | } 68 | 69 | export function addTimelineVariableRowAction(index=-1) { 70 | return { 71 | type: actionTypes.ADD_TIMELINE_VARIABLE_ROW, 72 | index: index 73 | } 74 | } 75 | 76 | export function addTimelineVariableColumnAction() { 77 | return { 78 | type: actionTypes.ADD_TIMELINE_VARIABLE_COLUMN, 79 | } 80 | } 81 | 82 | export function deleteTimelineVariableRowAction(index) { 83 | return { 84 | type: actionTypes.DELETE_TIMELINE_VARIABLE_ROW, 85 | index: index 86 | } 87 | } 88 | 89 | export function deleteTimelineVariableColumnAction(index) { 90 | return { 91 | type: actionTypes.DELETE_TIMELINE_VARIABLE_COLUMN, 92 | index: index 93 | } 94 | } 95 | 96 | export function setTimelineVariableAction(table) { 97 | return { 98 | type: actionTypes.SET_TIMELINE_VARIABLE, 99 | table: table 100 | } 101 | } 102 | 103 | export function moveRowToAction(sourceIndex, targetIndex) { 104 | return { 105 | type: actionTypes.MOVE_TIMELINE_VARIABLE_ROW_TO, 106 | sourceIndex: sourceIndex, 107 | targetIndex: targetIndex 108 | } 109 | } 110 | 111 | export function setSamplingMethodAction(key, newVal) { 112 | return { 113 | type: actionTypes.SET_SAMPLING_METHOD, 114 | key: key, 115 | newVal: newVal 116 | }; 117 | } 118 | 119 | export function setSampleSizeAction(newVal) { 120 | return { 121 | type: actionTypes.SET_SAMPLE_SIZE, 122 | newVal: newVal 123 | }; 124 | } 125 | 126 | export function setRandomizeAction(newBool) { 127 | return { 128 | type: actionTypes.SET_RANDOMIZE, 129 | value: newBool 130 | }; 131 | } 132 | 133 | 134 | export function setRepetitionsAction(newVal) { 135 | return { 136 | type: actionTypes.SET_REPETITIONS, 137 | newVal: newVal 138 | }; 139 | } 140 | 141 | export function setLoopFunctionAction(newVal) { 142 | return { 143 | type: actionTypes.SET_LOOP_FUNCTION, 144 | newVal: newVal 145 | }; 146 | } 147 | 148 | export function setConditionFunctionAction(newVal) { 149 | return { 150 | type: actionTypes.SET_CONDITION_FUNCTION, 151 | newVal: newVal 152 | }; 153 | } 154 | 155 | /* ********************** Timeline form ********************** */ -------------------------------------------------------------------------------- /src/common/actions/experimentSettingActions.js: -------------------------------------------------------------------------------- 1 | import * as actionTypes from '../constants/ActionTypes'; 2 | 3 | export const setExperimentNameAction = (name) => ({ 4 | type: actionTypes.SET_EXPERIMENT_NAME, 5 | name: name, 6 | }); 7 | 8 | export function setJspyschInitAction(key, value) { 9 | return { 10 | type: actionTypes.SET_JSPSYCH_INIT, 11 | key: key, 12 | value: value, 13 | }; 14 | } 15 | 16 | export function setCloudDeployInfoAction(cloudDeployInfo) { 17 | return { 18 | type: actionTypes.SET_CLOUD_DEPLOY_INFO, 19 | cloudDeployInfo: cloudDeployInfo 20 | } 21 | } 22 | 23 | export function setDIYDeployInfoAction(diyDeployInfo) { 24 | return { 25 | type: actionTypes.SET_DIY_DEPLOY_INFO, 26 | diyDeployInfo: diyDeployInfo 27 | } 28 | } -------------------------------------------------------------------------------- /src/common/actions/organizerActions.js: -------------------------------------------------------------------------------- 1 | import * as actionTypes from '../constants/ActionTypes'; 2 | 3 | export function addTimelineAction(parent) { 4 | return { 5 | type: actionTypes.ADD_TIMELINE, 6 | parent: parent, 7 | }; 8 | } 9 | 10 | export function addTrialAction(parent) { 11 | return { 12 | type: actionTypes.ADD_TRIAL, 13 | parent: parent, 14 | }; 15 | } 16 | 17 | export function deleteTimelineAction(id) { 18 | return { 19 | type: actionTypes.DELETE_TIMELINE, 20 | id: id 21 | }; 22 | } 23 | 24 | export function deleteTrialAction(id) { 25 | return { 26 | type: actionTypes.DELETE_TRIAL, 27 | id: id 28 | }; 29 | } 30 | 31 | export function moveToAction(sourceId, targetId, isLast) { 32 | return { 33 | type: actionTypes.MOVE_TO, 34 | sourceId: sourceId, 35 | targetId: targetId, 36 | isLast: isLast, 37 | }; 38 | } 39 | 40 | export function moveIntoAction(id) { 41 | return { 42 | type: actionTypes.MOVE_INTO, 43 | id: id, 44 | }; 45 | } 46 | 47 | export function moveByKeyboardAction(id, key) { 48 | return { 49 | type: actionTypes.MOVE_BY_KEYBOARD, 50 | id: id, 51 | key: key, 52 | }; 53 | } 54 | 55 | export function onPreviewAction(id) { 56 | return { 57 | type: actionTypes.ON_PREVIEW, 58 | id: id 59 | }; 60 | } 61 | 62 | export function onToggleAction(id) { 63 | return { 64 | type: actionTypes.ON_TOGGLE, 65 | id: id 66 | }; 67 | } 68 | 69 | export function setCollapsed(id, toggle=true) { 70 | return { 71 | type: actionTypes.SET_COLLAPSED, 72 | id: id 73 | }; 74 | } 75 | 76 | export function insertNodeAfterTrialAction(targetId, isTimeline=false) { 77 | return { 78 | type: actionTypes.INSERT_NODE_AFTER_TRIAL, 79 | targetId: targetId, 80 | isTimeline: isTimeline 81 | }; 82 | } 83 | 84 | export function duplicateTimelineAction(targetId) { 85 | return { 86 | type: actionTypes.DUPLICATE_TIMELINE, 87 | targetId: targetId, 88 | }; 89 | } 90 | 91 | export function duplicateTrialAction(targetId) { 92 | return { 93 | type: actionTypes.DUPLICATE_TRIAL, 94 | targetId: targetId, 95 | }; 96 | } 97 | 98 | export function setNameAction(name) { 99 | return { 100 | type: actionTypes.SET_NAME, 101 | name: name 102 | }; 103 | } 104 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /src/common/actions/userActions.js: -------------------------------------------------------------------------------- 1 | import * as actionTypes from '../constants/ActionTypes'; 2 | 3 | export function setOsfAccessAction(osfAccess) { 4 | return { 5 | type: actionTypes.SET_OSF_ACCESS, 6 | osfAccess: osfAccess 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/common/components/Appbar/CloudDeploymentManager/index.js: -------------------------------------------------------------------------------- 1 | import CloudDeploymentManager from './CloudDeploymentManager.jsx'; 2 | 3 | export default CloudDeploymentManager; -------------------------------------------------------------------------------- /src/common/components/Appbar/DIYDeploymentManager/index.js: -------------------------------------------------------------------------------- 1 | import DIYDeploymentManager from './DIYDeploymentManager.jsx'; 2 | 3 | export default DIYDeploymentManager; -------------------------------------------------------------------------------- /src/common/components/Appbar/UserMenu/ExperimentList/index.js: -------------------------------------------------------------------------------- 1 | import ExperimentList from './ExperimentList.jsx'; 2 | 3 | export default ExperimentList; -------------------------------------------------------------------------------- /src/common/components/Appbar/UserMenu/UserMenu.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Popover from 'material-ui/Popover'; 3 | import Menu from 'material-ui/Menu'; 4 | import MenuItem from 'material-ui/MenuItem'; 5 | import { ListItem } from 'material-ui/List'; 6 | import Avatar from 'material-ui/Avatar'; 7 | import Divider from 'material-ui/Divider'; 8 | 9 | import SignInIcon from 'material-ui/svg-icons/action/input'; 10 | import SignUpIcon from 'material-ui/svg-icons/social/person-add'; 11 | import ProfileIcon from 'material-ui/svg-icons/social/person'; 12 | import ExperimentIcon from 'material-ui/svg-icons/action/book'; 13 | import SignOut from 'material-ui/svg-icons/action/exit-to-app'; 14 | 15 | import ExperimentList from '../../../containers/Appbar/UserMenu/ExperimentList'; 16 | import Profile from '../../../containers/Appbar/UserMenu/Profile'; 17 | 18 | import AppbarTheme from '../theme.js'; 19 | 20 | const colors = { 21 | ...AppbarTheme.colors 22 | } 23 | 24 | const style = { 25 | Icon: { 26 | hoverColor: colors.secondaryLight, 27 | color: colors.secondary, 28 | }, 29 | Avatar: utils.prefixer({ 30 | backgroundColor: 'white', 31 | color: colors.primary 32 | }), 33 | Username: login => (utils.prefixer({ 34 | color: colors.font, 35 | fontWeight: login ? 'bold' : 'normal', 36 | textDecoration: login ? 'underline' : 'none' 37 | })) 38 | } 39 | 40 | export default class UserMenu extends React.Component { 41 | constructor(props) { 42 | super(props); 43 | this.state = { 44 | open: false, 45 | isSignedIn: false 46 | } 47 | 48 | 49 | this.handleTouchTap = (event) => { 50 | this.setState({ 51 | open: true, 52 | anchorEl: event.currentTarget 53 | }); 54 | } 55 | 56 | this.handleRequestClose = () => { 57 | this.setState({ 58 | open: false 59 | }) 60 | } 61 | 62 | this.renderMenu = (login) => { 63 | if (!login) { 64 | return ( 65 | 66 | } 68 | primaryText={"Sign In"} 69 | onClick={() => { this.props.popSignIn(); this.handleRequestClose(); }} /> 70 | 71 | } 73 | primaryText={"Create Account"} 74 | onClick={() => { this.props.popSignUp(); this.handleRequestClose(); }} /> 75 | 76 | ) 77 | } else { 78 | return ( 79 | 80 | } 82 | primaryText={"Your profile"} 83 | onClick={() => { this.Profile.handleOpen(); this.handleRequestClose(); }} /> 84 | } 87 | onClick={() => { this.ExperimentList.handleOpen(); this.handleRequestClose(); }} /> 88 | 89 | } 92 | onClick={() => { this.props.handleSignOut().then(this.handleRequestClose); }} /> 93 | 94 | ) 95 | } 96 | } 97 | 98 | this.renderUserPic = (login, size=36) => { 99 | if (login) { 100 | return ( 101 | 105 | {this.props.username.charAt(0)} 106 | 107 | ) 108 | } else { 109 | return null; 110 | } 111 | } 112 | } 113 | 114 | componentDidMount() { 115 | this.props.openProfilePage(this.Profile.handleOpen); 116 | } 117 | 118 | componentWillUnmount() { 119 | this.props.openProfilePage(() => {}); 120 | } 121 | 122 | render() { 123 | let login = !!this.props.username; 124 | let buttonLabel = (!login) ? 'Sign Up/Log In' : this.props.username; 125 | 126 | return ( 127 |
128 |
129 | 135 |
136 | (this.Profile = ref)} /> 137 | (this.ExperimentList = ref)}/> 138 | 145 | {this.renderMenu(login)} 146 | 147 |
148 | ) 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/common/components/Appbar/UserMenu/index.js: -------------------------------------------------------------------------------- 1 | import UserMenu from './UserMenu.jsx'; 2 | 3 | export default UserMenu; -------------------------------------------------------------------------------- /src/common/components/Appbar/index.js: -------------------------------------------------------------------------------- 1 | import Appbar from './Appbar.jsx'; 2 | 3 | export default Appbar; -------------------------------------------------------------------------------- /src/common/components/Appbar/jsPsychInitEditor/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Dialog from 'material-ui/Dialog'; 3 | import Subheader from 'material-ui/Subheader'; 4 | import FlatButton from 'material-ui/FlatButton'; 5 | import IconButton from 'material-ui/IconButton'; 6 | import TextField from 'material-ui/TextField'; 7 | 8 | import { 9 | grey800 as normalColor, 10 | cyan500 as iconHighlightColor, 11 | green500 as checkColor, 12 | blue500 as titleIconColor 13 | } from 'material-ui/styles/colors'; 14 | import InitSettingIcon from 'material-ui/svg-icons/action/build'; 15 | import CheckIcon from 'material-ui/svg-icons/toggle/radio-button-checked'; 16 | import UnCheckIcon from 'material-ui/svg-icons/toggle/radio-button-unchecked'; 17 | 18 | import CodeEditor from '../../CodeEditor'; 19 | import { renderDialogTitle } from '../../gadgets'; 20 | import { settingType } from '../../../reducers/Experiment/jsPsychInit'; 21 | 22 | import AppbarTheme from '../theme.js'; 23 | 24 | const colors = { 25 | ...AppbarTheme.colors 26 | } 27 | 28 | const style = { 29 | Icon: AppbarTheme.AppbarIcon, 30 | TitleIcon: { 31 | color: colors.primaryDeep 32 | }, 33 | TextFieldFocusStyle: { 34 | ...AppbarTheme.TextFieldFocusStyle() 35 | }, 36 | Actions: { 37 | Close: { 38 | labelStyle: { 39 | color: colors.secondaryDeep 40 | } 41 | } 42 | } 43 | } 44 | 45 | export default class jsPsychInitEditor extends React.Component { 46 | constructor(props) { 47 | super(props); 48 | 49 | this.state = { 50 | open: false 51 | } 52 | 53 | this.handleOpen = () => { 54 | this.setState({ 55 | open: true 56 | }); 57 | }; 58 | 59 | this.handleClose = () => { 60 | this.setState({ 61 | open: false 62 | }); 63 | }; 64 | 65 | this.textFieldRow = (key, type="number", unit=null) => ( 66 |
67 |
{key + ((unit) ? " (" + unit + ")" : "")}:
68 | { this.props.setJsPsychInit(e, value, key); }} 74 | /> 75 |
76 | ) 77 | 78 | this.toggleRow = (key) => ( 79 |
80 |
{key}
81 | { this.props.setJsPsychInit(null, null, key); }} 84 | > 85 | {(this.props[key]) ? : }/> 86 | 87 |
88 | ) 89 | 90 | this.codeRow = (key) => ( 91 |
92 |
{key}
93 |
94 | { 98 | this.props.setJsPsychInit(null, newCode, key); 99 | }} 100 | title={key+": "} 101 | /> 102 |
103 |
104 | ) 105 | 106 | } 107 | 108 | 109 | render() { 110 | const actions = [ 111 | ]; 112 | 113 | return ( 114 |
115 | 119 | 120 | 121 | 127 |
128 |
129 | 130 |
131 |
132 | jsPsych.init properties 133 |
134 |
135 | , 136 | this.handleClose, 137 | null) 138 | } 139 | actions={actions} 140 | modal={true} 141 | open={this.state.open} 142 | onRequestClose={this.handleClose} 143 | autoScrollBodyContent={true} 144 | > 145 | {this.textFieldRow(settingType.default_iti)} 146 | {this.codeRow(settingType.on_finish)} 147 | {this.codeRow(settingType.on_trial_start)} 148 | {this.codeRow(settingType.on_trial_finish)} 149 | {this.codeRow(settingType.on_data_update)} 150 | {this.codeRow(settingType.on_interaction_data_update)} 151 | {this.toggleRow(settingType.show_progress_bar)} 152 | {this.toggleRow(settingType.auto_update_progress_bar)} 153 | {this.toggleRow(settingType.show_preload_progress_bar)} 154 |
preload_audio:
155 |
preload_images:
156 | {this.textFieldRow(settingType.max_load_time, "number", "ms")} 157 |
Exclusions:
158 |
159 | {this.textFieldRow(settingType.min_width)} 160 | {this.textFieldRow(settingType.min_height)} 161 | {this.toggleRow(settingType.audio)} 162 |
163 |
164 |
165 | ) 166 | } 167 | } -------------------------------------------------------------------------------- /src/common/components/Appbar/theme.js: -------------------------------------------------------------------------------- 1 | import GeneralTheme from '../theme.js'; 2 | 3 | export const colors = { 4 | ...GeneralTheme.colors, 5 | iconColor: 'white', 6 | hoverColor: '#B2FF59', 7 | highlightColor: '#B2FF59', 8 | background: '#24B24C', // green 9 | } 10 | 11 | export const AppbarIcon = { 12 | color: colors.iconColor, 13 | hoverColor: colors.primaryDeep, 14 | } 15 | 16 | const theme = { 17 | ...GeneralTheme, 18 | colors: colors, 19 | AppbarIcon: AppbarIcon, 20 | } 21 | 22 | export default theme; -------------------------------------------------------------------------------- /src/common/components/Authentications/Authentications.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Dialog from 'material-ui/Dialog'; 3 | import Subheader from 'material-ui/Subheader'; 4 | import {Tabs, Tab} from 'material-ui/Tabs'; 5 | 6 | import SignInWindow from './SignInWindow.js'; 7 | import RegisterWindow from './RegisterWindow.js'; 8 | import VerificationWindow from './VerificationWindow.js'; 9 | import ForgotPasswordWindow from './ForgotPasswordWindow.js'; 10 | 11 | import { DialogTitle } from '../gadgets'; 12 | 13 | 14 | const colors = { 15 | ...theme.colors, 16 | tabTextColor: '#212121', 17 | tabColor: '#EEEEEE', 18 | dialogBodyColor: '#FAFAFA' 19 | } 20 | 21 | const style = { 22 | Tabs: { 23 | inkBarStyle: { 24 | backgroundColor: colors.secondary 25 | } 26 | }, 27 | Tab_SignIn: { 28 | buttonStyle: loginMode => ({ 29 | backgroundColor: (enums.AUTH_MODES.signIn === loginMode) ? colors.primary : colors.tabColor, 30 | textTransform: "none", 31 | fontSize: 15 32 | }), 33 | style: loginMode => ({ 34 | color: (enums.AUTH_MODES.signIn === loginMode) ? 'white' : colors.tabTextColor 35 | }) 36 | }, 37 | Tab_Register: { 38 | buttonStyle: loginMode => ({ 39 | backgroundColor: (enums.AUTH_MODES.register === loginMode) ? colors.primary : colors.tabColor, 40 | textTransform: "none", 41 | fontSize: 15 42 | }), 43 | style: loginMode => ({ 44 | color: (enums.AUTH_MODES.register === loginMode) ? 'white' : colors.tabTextColor 45 | }) 46 | }, 47 | } 48 | 49 | 50 | export default class Authentications extends React.Component { 51 | constructor(props) { 52 | super(props); 53 | this.state = { 54 | username: '', 55 | password: '', 56 | email: '', 57 | loginMode: enums.AUTH_MODES.signIn 58 | } 59 | 60 | this.setUserName = (name) => { 61 | this.setState({username: name}); 62 | } 63 | 64 | this.setPassword = (password) => { 65 | this.setState({password: password}); 66 | } 67 | 68 | this.setEmail = (email) => { 69 | this.setState({email: email}); 70 | } 71 | 72 | this.clearField = () => { 73 | this.setState({ 74 | username: '', 75 | password: '', 76 | email: '', 77 | }); 78 | } 79 | 80 | this.handleClose = () => { 81 | this.clearField(); 82 | this.props.handleWindowClose(); 83 | } 84 | 85 | this.renderContent = () => { 86 | let { 87 | username, 88 | password, 89 | email, 90 | } = this.state; 91 | 92 | let { 93 | setUserName, 94 | setPassword, 95 | setEmail, 96 | clearField, 97 | handleClose 98 | } = this; 99 | 100 | let { 101 | loginMode, 102 | setLoginMode, 103 | popForgetPassword, 104 | popVerification 105 | } = this.props; 106 | 107 | switch(loginMode) { 108 | case enums.AUTH_MODES.signIn: 109 | case enums.AUTH_MODES.register: 110 | return ( 111 | { setLoginMode(mode); clearField(); }} 114 | {...style.Tabs} 115 | > 116 | 124 |
125 | 135 | 136 | 144 |
145 | 156 | 157 | 158 | 159 | ) 160 | case enums.AUTH_MODES.verification: 161 | return ( 162 | 166 | ) 167 | case enums.AUTH_MODES.forgotPassword: 168 | return ( 169 | 177 | 178 | ) 179 | default: 180 | return null; 181 | } 182 | } 183 | } 184 | 185 | render() { 186 | 187 | return ( 188 | } 194 | showCloseButton={true} 195 | closeCallback={this.handleClose} 196 | /> 197 | } 198 | onRequestClose={this.handleClose} 199 | contentStyle={{width: 450, height: 600,}} 200 | bodyStyle={{backgroundColor: colors.dialogBodyColor, paddingTop: 0}} 201 | modal={false} 202 | autoScrollBodyContent 203 | > 204 |
205 | { 206 | this.renderContent() 207 | } 208 |
209 |
210 | ) 211 | } 212 | } 213 | 214 | -------------------------------------------------------------------------------- /src/common/components/Authentications/RegisterWindow.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | import TextField from 'material-ui/TextField'; 5 | import FlatButton from 'material-ui/FlatButton'; 6 | import RaisedButton from 'material-ui/RaisedButton'; 7 | import Paper from 'material-ui/Paper'; 8 | import CircularProgress from 'material-ui/CircularProgress'; 9 | 10 | const colors = { 11 | ...theme.colors, 12 | } 13 | 14 | const style = { 15 | TextFieldFocusStyle: (error=false) => ({ 16 | ...theme.TextFieldFocusStyle(error) 17 | }), 18 | Actions: { 19 | Create: { 20 | labelStyle: { 21 | textTransform: "none", 22 | fontSize: 15, 23 | color: 'white' 24 | }, 25 | backgroundColor: colors.primary, 26 | fullWidth: true, 27 | }, 28 | Cancel: { 29 | labelStyle: { 30 | textTransform: "none", 31 | color: colors.secondary 32 | } 33 | }, 34 | Wait: { 35 | color: colors.primary 36 | } 37 | } 38 | } 39 | 40 | class RegisterWindow extends React.Component { 41 | constructor(props) { 42 | super(props); 43 | this.state = { 44 | usernameErrorText: null, 45 | emailErrorText: null, 46 | passwordErrorText: null, 47 | registering: false 48 | } 49 | 50 | this.setUsername = (e, newVal) => { 51 | this.props.setUserName(newVal); 52 | this.setState({ 53 | usernameErrorText: newVal.length > 0 ? null : "Please enter a username" 54 | }); 55 | } 56 | 57 | this.setEmail = (e, newVal) => { 58 | const re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; 59 | const validEmail = re.test(newVal); 60 | this.props.setEmail(newVal); 61 | this.setState({ 62 | emailErrorText: validEmail ? null : "Please enter a valid email address" 63 | }); 64 | } 65 | 66 | this.setPassword = (e, newVal) => { 67 | this.props.setPassword(newVal); 68 | this.setState({ 69 | passwordErrorText: newVal.length < 10 ? "Password must be at least 10 characters long" : null 70 | }); 71 | } 72 | 73 | this.handleCreateAccount = () => { 74 | let cont_flag = true; 75 | let { username, password, email, dispatch } = this.props; 76 | 77 | if(username === ''){ 78 | this.setState({usernameErrorText: "Please enter a username"}); 79 | cont_flag = false; 80 | } 81 | if(email === '' || this.state.emailErrorText !== null){ 82 | this.setState({emailErrorText: "Please enter a valid email address"}); 83 | cont_flag = false; 84 | } 85 | if(password === '' || this.state.passwordErrorText !== null){ 86 | this.setState({passwordErrorText: "Password must be at least 10 characters long"}); 87 | cont_flag = false; 88 | } 89 | 90 | if (cont_flag) { 91 | this.setState({ 92 | registering: true 93 | }); 94 | this.props.signUp({ 95 | username, 96 | password, 97 | attributes: { 98 | email 99 | } 100 | }).finally(() => { 101 | this.setState({ 102 | registering: false 103 | }); 104 | }).catch((err) => { 105 | if (err.code === "UsernameExistsException") { 106 | this.setState({ 107 | usernameErrorText: 'This username is already taken.' 108 | }); 109 | } else if (err.code === "EmailExistsException") { 110 | this.setState({ 111 | emailErrorText: 'This email is already used.' 112 | }); 113 | } else { 114 | console.log(err); 115 | utils.notifications.notifyErrorByDialog({ 116 | dispatch, 117 | message: err.message 118 | }); 119 | } 120 | }); 121 | } 122 | } 123 | } 124 | 125 | 126 | render(){ 127 | 128 | let { usernameErrorText, passwordErrorText, emailErrorText, registering } = this.state; 129 | 130 | return( 131 | 132 |
{ 134 | if (e.which === 13) { 135 | this.handleCreateAccount(); 136 | } 137 | }} 138 | > 139 | 147 | 148 | 156 | 157 | 166 | 167 |
168 | {!registering ? 169 | : 174 | 175 | } 176 |
177 |
178 | 183 |
184 |
185 |
186 | ) 187 | } 188 | } 189 | 190 | const mapStateToProps = (state, ownProps) => { 191 | return { 192 | } 193 | } 194 | 195 | const mapDispatchToProps = (dispatch, ownProps) => ({ 196 | dispatch: dispatch 197 | }) 198 | 199 | export default connect(mapStateToProps, mapDispatchToProps)(RegisterWindow); -------------------------------------------------------------------------------- /src/common/components/Authentications/SignInWindow.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | import TextField from 'material-ui/TextField'; 5 | import Paper from 'material-ui/Paper'; 6 | import RaisedButton from 'material-ui/RaisedButton'; 7 | import CircularProgress from 'material-ui/CircularProgress'; 8 | import FlatButton from 'material-ui/FlatButton'; 9 | 10 | const colors = { 11 | ...theme.colors, 12 | } 13 | 14 | const style = { 15 | TextFieldFocusStyle: (error=false) => ({ 16 | ...theme.TextFieldFocusStyle(error) 17 | }), 18 | Actions: { 19 | SignIn: { 20 | labelStyle: { 21 | textTransform: "none", 22 | fontSize: 15, 23 | color: 'white' 24 | }, 25 | backgroundColor: colors.primary, 26 | fullWidth: true, 27 | }, 28 | Forget: { 29 | labelStyle: { 30 | textTransform: "none", 31 | color: colors.secondary 32 | } 33 | }, 34 | Wait: { 35 | color: colors.primary 36 | } 37 | } 38 | } 39 | 40 | class SignInWindow extends React.Component { 41 | constructor(props) { 42 | super(props); 43 | 44 | this.state = { 45 | userErrorText: null, 46 | passwordErrorText: null, 47 | signIning: false 48 | } 49 | 50 | this.setUsername = (e, newVal) => { 51 | this.props.setUserName(newVal); 52 | this.setState({ 53 | userErrorText: newVal.length > 0 ? null : "Please enter your username or email address" 54 | }); 55 | } 56 | 57 | this.setPassword = (e, newVal) => { 58 | this.props.setPassword(newVal); 59 | this.setState({ 60 | passwordErrorText: newVal.length < 10 ? "Password must be at least 10 characters long" : null 61 | }); 62 | } 63 | 64 | this.signIn = () => { 65 | let cont_flag = true; 66 | if(this.props.username === ''){ 67 | this.setState({userErrorText: "Please enter your username or email"}); 68 | cont_flag = false; 69 | } 70 | if(this.props.password === ''){ 71 | this.setState({passwordErrorText: "Please enter your password"}); 72 | cont_flag = false; 73 | } 74 | if (cont_flag) { 75 | this.setState({ 76 | signIning: true 77 | }); 78 | this.props.signIn({ 79 | username: this.props.username, 80 | password: this.props.password, 81 | }).then(() => { 82 | this.props.handleClose(); 83 | }).catch((err) => { 84 | if (err.code === "NotAuthorizedException") { 85 | this.setState({ 86 | passwordErrorText: "Invalid password" 87 | }); 88 | } else if (err.code === "UserNotFoundException") { 89 | this.setState({ 90 | userErrorText: "No account found for this username / email" 91 | }); 92 | } else { 93 | console.log(err); 94 | utils.notifications.notifyErrorByDialog({ 95 | dispatch: this.props.dispatch, 96 | message: err.message 97 | }); 98 | } 99 | }).finally(() => { 100 | this.setState({ 101 | signIning: false 102 | }); 103 | }); 104 | } 105 | } 106 | 107 | } 108 | 109 | 110 | render(){ 111 | let { username, password, popForgetPassword } = this.props; 112 | let { userErrorText, passwordErrorText, signIning } = this.state; 113 | 114 | return( 115 | 116 |
{ 118 | if (e.which === 13) { 119 | this.signIn(); 120 | } 121 | }}> 122 | 131 | 141 |
142 | {!signIning ? 143 | : 148 | 149 | } 150 |
151 |
152 | 157 |
158 |
159 |
160 | ) 161 | } 162 | } 163 | 164 | const mapStateToProps = (state, ownProps) => { 165 | return { 166 | } 167 | } 168 | 169 | const mapDispatchToProps = (dispatch, ownProps) => ({ 170 | dispatch: dispatch 171 | }) 172 | 173 | export default connect(mapStateToProps, mapDispatchToProps)(SignInWindow); -------------------------------------------------------------------------------- /src/common/components/Authentications/VerificationWindow.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import TextField from 'material-ui/TextField'; 4 | import FlatButton from 'material-ui/FlatButton'; 5 | import MenuItem from 'material-ui/MenuItem'; 6 | import RaisedButton from 'material-ui/RaisedButton'; 7 | import CircularProgress from 'material-ui/CircularProgress'; 8 | 9 | import Verified from 'material-ui/svg-icons/action/verified-user'; 10 | 11 | const colors = { 12 | ...theme.colors, 13 | verifyColor: '#4CAF50' 14 | } 15 | 16 | const style = { 17 | VerifyIcon: { 18 | color: colors.verifyColor, 19 | }, 20 | TextFieldFocusStyle: (error=false) => ({ 21 | ...theme.TextFieldFocusStyle(error) 22 | }), 23 | Actions: { 24 | Wait: { 25 | color: colors.secondary 26 | }, 27 | Verify: { 28 | backgroundColor: colors.primary, 29 | labelStyle: { 30 | textTransform: "none", 31 | color: 'white' 32 | }, 33 | fullWidth: true, 34 | }, 35 | Resend:{ 36 | labelStyle: { 37 | textTransform: "none", 38 | color: colors.secondary 39 | }, 40 | fullWidth: true, 41 | }, 42 | } 43 | } 44 | 45 | let Modes = { 46 | ready: 0, 47 | processing: 1, 48 | success: 2, 49 | } 50 | 51 | class VerificationWindow extends React.Component { 52 | constructor(props) { 53 | super(props); 54 | this.state = { 55 | code: '', 56 | codeErrorText: '', 57 | mode: Modes.ready, 58 | resending: false 59 | } 60 | 61 | 62 | this.setCode = (e, newVal) => { 63 | this.setState({ 64 | code: newVal, 65 | codeErrorText: '' 66 | }); 67 | } 68 | 69 | this.clearField = () => { 70 | this.setState({ 71 | code: '', 72 | codeErrorText: '', 73 | }) 74 | } 75 | 76 | this.handleVerification = () => { 77 | let cont_flag = true; 78 | let { code } = this.state; 79 | 80 | if(code === ''){ 81 | this.setState({codeErrorText: "Please enter a valid verification code."}); 82 | cont_flag = false; 83 | } 84 | 85 | if (cont_flag) { 86 | this.setState({ 87 | mode: Modes.processing 88 | }); 89 | myaws.Auth.confirmSignUp({ 90 | username: this.props.username, 91 | code: this.state.code.trim() 92 | }).then(() => { 93 | this.setState({ 94 | mode: Modes.success 95 | }); 96 | return this.props.signInCallback().then(this.props.handleClose); 97 | }).catch((err) => { 98 | if (err.code === "CodeMismatchException") { 99 | this.setState({ 100 | mode: Modes.ready, 101 | codeErrorText: err.message 102 | }); 103 | } 104 | console.log(err); 105 | }); 106 | } 107 | 108 | } 109 | 110 | this.resendVerificationCode = () => { 111 | this.clearField(); 112 | this.setState({ 113 | resending: true 114 | }) 115 | myaws.Auth.resendVerification({username: this.props.username}).then(() => { 116 | utils.notifications.notifySuccessBySnackbar({ 117 | dispatch: this.props.dispatch, 118 | message: "Verification code was resent." 119 | }); 120 | }).catch((err) => { 121 | utils.notifications.notifyErrorByDialog({ 122 | dispatch: this.props.dispatch, 123 | message: err.message 124 | }); 125 | }).finally(() => { 126 | this.setState({ 127 | resending: false 128 | }) 129 | }); 130 | } 131 | 132 | this.renderVerifcationButton = () => { 133 | switch(this.state.mode) { 134 | case Modes.processing: 135 | return ( 136 | 137 | ); 138 | case Modes.success: 139 | return ( 140 | } 144 | /> 145 | ); 146 | case Modes.ready: 147 | default: 148 | return ( 149 | 154 | ); 155 | } 156 | } 157 | } 158 | 159 | 160 | render(){ 161 | return( 162 |
163 |

Your account won't be created until you enter the vertification code that you receive by email. Please enter the code below.

164 |
165 | { 174 | if (e.which === 13) { 175 | this.handleVerification(); 176 | } 177 | }} 178 | /> 179 | 180 |
182 | { 183 | this.renderVerifcationButton() 184 | } 185 |
186 |
187 | {!this.state.resending ? 188 | : 193 | 194 | } 195 |
196 |
197 |
198 | ) 199 | } 200 | } 201 | 202 | const mapStateToProps = (state, ownProps) => { 203 | return { 204 | signInCallback: state.authentications.signInCallback 205 | } 206 | } 207 | 208 | const mapDispatchToProps = (dispatch, ownProps) => ({ 209 | dispatch: dispatch 210 | }) 211 | 212 | export default connect(mapStateToProps, mapDispatchToProps)(VerificationWindow); -------------------------------------------------------------------------------- /src/common/components/Authentications/index.js: -------------------------------------------------------------------------------- 1 | import Authentications from './Authentications.js'; 2 | 3 | export default Authentications; -------------------------------------------------------------------------------- /src/common/components/CodeEditor/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Dialog from 'material-ui/Dialog'; 3 | import FlatButton from 'material-ui/FlatButton'; 4 | import IconButton from 'material-ui/IconButton'; 5 | import Subheader from 'material-ui/Subheader'; 6 | import MenuItem from 'material-ui/MenuItem'; 7 | import SelectField from 'material-ui/SelectField'; 8 | 9 | import CodeMirror from 'react-codemirror'; 10 | require('codemirror/lib/codemirror.css'); 11 | require('codemirror/mode/javascript/javascript'); 12 | require('codemirror/mode/htmlmixed/htmlmixed'); 13 | 14 | import EditCodeIcon from 'material-ui/svg-icons/action/code'; 15 | import CheckBoxIcon from 'material-ui/svg-icons/toggle/check-box'; 16 | import UnCheckBoxIcon from 'material-ui/svg-icons/toggle/check-box-outline-blank'; 17 | import { 18 | grey800 as normalColor, 19 | yellow500 as checkColor, 20 | } from 'material-ui/styles/colors'; 21 | import { renderDialogTitle } from '../gadgets'; 22 | import GeneralTheme from '../theme.js'; 23 | 24 | const colors = { 25 | ...GeneralTheme.colors, 26 | } 27 | 28 | const hoverColor = GeneralTheme.colors.secondary; 29 | 30 | const style = { 31 | Trigger: { 32 | labelColor: 'white', 33 | backgroundColor: colors.primary, 34 | labelStyle: { 35 | fontSize: '13px' 36 | } 37 | }, 38 | DefaultTrigger: { 39 | hoverColor: colors.secondary 40 | }, 41 | actionButtons: { 42 | Submit: { 43 | labelStyle: { 44 | textTransform: "none", 45 | color: colors.primaryDeep 46 | } 47 | }, 48 | }, 49 | SelectFieldStyle: { 50 | selectedMenuItemStyle: { 51 | color: colors.secondary 52 | }, 53 | style: { 54 | width: 180, 55 | } 56 | }, 57 | toolbar: { 58 | display: 'flex', 59 | justifyContent: 'space-between', 60 | alignItems: 'baseline' 61 | }, 62 | } 63 | 64 | export const CodeLanguage = { 65 | // text: [null, 'Plain Text'], 66 | javascript: ['javascript', 'Javascript'], 67 | html: ['htmlmixed', 'HTML/Plain Text'] 68 | } 69 | 70 | export default class CodeEditor extends React.Component { 71 | constructor(props) { 72 | super(props); 73 | this.state = { 74 | open: false, 75 | } 76 | 77 | this.handleOpen = () => { 78 | this.setState({ 79 | open: true, 80 | // init values 81 | code: utils.toEmptyString(this.props.value), 82 | language: this.props.language || CodeLanguage.javascript[0], 83 | evalAsFunction: !!this.props.ifEval 84 | }); 85 | } 86 | 87 | this.handleClose = () => { 88 | this.setState({ 89 | open: false, 90 | }); 91 | } 92 | 93 | this.onUpdate = (newCode) => { 94 | this.setState({ 95 | code: newCode 96 | }); 97 | } 98 | 99 | this.setLanguage = (language) => { 100 | this.setState({ 101 | language: language, 102 | evalAsFunction: language === CodeLanguage.javascript[0] 103 | }) 104 | } 105 | 106 | this.handleEvalAsFunction = () => { 107 | this.setState({ 108 | evalAsFunction: !this.state.evalAsFunction 109 | }) 110 | } 111 | 112 | this.onCommit = () => { 113 | this.props.onCommit(utils.toNull(this.state.code), this.state.evalAsFunction, this.state.language); 114 | this.handleClose(); 115 | } 116 | } 117 | 118 | static defaultProps = { 119 | value: "", 120 | language: CodeLanguage.javascript[0], 121 | tooltip: "Insert code", 122 | title: "Code Editor", 123 | onCommit: function(newCode) { return; }, 124 | Trigger: ({onClick, tooltip}) => ( 125 | 129 | 130 | 131 | ), 132 | }; 133 | 134 | 135 | render() { 136 | const { buttonIcon, title, onCommit, Trigger } = this.props; 137 | 138 | const actions = [ 139 | , 144 | ]; 145 | 146 | const items = Object.values(CodeLanguage).map((mode, i) => ( 147 | 148 | )) 149 | 150 | let disabled = this.props.onlyString || this.props.onlyFunction; 151 | 152 | // add this.state.open ? tag : null to trigger reset (because we don't have control to CodeMirror) 153 | return ( 154 |
155 | 156 | 161 | {title} 162 | , 163 | this.handleClose, 164 | null)} 165 | actions={actions} 166 | modal={true} 167 | open={this.state.open} 168 | > 169 |
170 | { this.setLanguage(value)}} 173 | {...style.SelectFieldStyle} 174 | floatingLabelFixed 175 | floatingLabelText="Language" 176 | value={this.state.language} 177 | > 178 | {items} 179 | 180 | {!disabled ? 181 | : 186 | 187 | } 188 | style={{color:colors.primaryDeep, textDecoration: 'underline'}} 189 | onClick={this.handleEvalAsFunction} 190 | /> : 191 | null 192 | } 193 |
194 | 203 |
204 |
205 | ) 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /src/common/components/MediaManager/index.js: -------------------------------------------------------------------------------- 1 | import MediaManager from './MediaManager.jsx'; 2 | 3 | export default MediaManager; 4 | -------------------------------------------------------------------------------- /src/common/components/Notifications/index.js: -------------------------------------------------------------------------------- 1 | import Notifications from './Notifications.jsx'; 2 | 3 | export default Notifications; -------------------------------------------------------------------------------- /src/common/components/PreviewWindow/ZoomBar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TextField from 'material-ui/TextField'; 3 | import SelectField from 'material-ui/SelectField'; 4 | import MenuItem from 'material-ui/MenuItem'; 5 | import { getFullScreenState } from './index'; 6 | import GeneralTheme from '../theme.js'; 7 | 8 | const colors = GeneralTheme.colors; 9 | const TextFieldWidth = 80; 10 | const SelectFieldWidth = 120; 11 | const style = { 12 | Zoombar: { 13 | margin: '0 auto', 14 | flexBasis: '72px', 15 | display: 'flex', 16 | alignItems: 'center', 17 | flexShrink: 0, 18 | }, 19 | TextFieldContainer: { 20 | display: 'flex', 21 | alignItems: 'baseline', 22 | paddingRight: '16px' 23 | }, 24 | TextFieldStyle: { 25 | style: { 26 | maxWidth: `${TextFieldWidth}px`, 27 | minWidth: `${TextFieldWidth}px`, 28 | }, 29 | inputStyle: { 30 | textAlign: 'left' 31 | }, 32 | ...GeneralTheme.TextFieldFocusStyle() 33 | }, 34 | X: { 35 | paddingLeft: 8, 36 | paddingRight: 8, 37 | fontSize: '14px', 38 | color: 'black', 39 | }, 40 | SelectFieldStyle: { 41 | style: { 42 | maxWidth: `${SelectFieldWidth}px`, 43 | minWidth: `${SelectFieldWidth}px`, 44 | }, 45 | autoWidth: true, 46 | floatingLabelText: "Zoom", 47 | floatingLabelFixed: true, 48 | selectedMenuItemStyle: { 49 | color: colors.secondary 50 | }, 51 | underlineFocusStyle: { 52 | color: colors.secondary 53 | } 54 | } 55 | } 56 | 57 | export default class ZoomBar extends React.Component { 58 | constructor(props) { 59 | super(props); 60 | } 61 | 62 | render() { 63 | const { 64 | zoomScale, 65 | displayZoom, 66 | zoomWidthByUser, 67 | zoomHeightByUser, 68 | onInputZoomHeight, 69 | onInputZoomWidth, 70 | setZoomHeight, 71 | setZoomWidth, 72 | setDisplayZoom, 73 | } = this.props; 74 | 75 | let zoomVal = Math.round(zoomScale * 100); 76 | 77 | return ( 78 |
79 |
80 | { let e = {which: 13}; setZoomWidth(e) }} 91 | /> 92 |
94 | x 95 |
96 | { let e = {which: 13}; setZoomHeight(e) }} 107 | /> 108 |
109 | { setDisplayZoom(e, i, v); }} 114 | > 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 |
124 | ) 125 | } 126 | } -------------------------------------------------------------------------------- /src/common/components/TimelineNodeEditor/CommonComponents/index.js: -------------------------------------------------------------------------------- 1 | import { CommonComponents, style as CommonComponentsStyle } from './CommonComponents.jsx'; 2 | 3 | export { CommonComponents, CommonComponentsStyle }; -------------------------------------------------------------------------------- /src/common/components/TimelineNodeEditor/TimelineForm/TimelineForm.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import SelectField from 'material-ui/SelectField'; 3 | import MenuItem from 'material-ui/MenuItem'; 4 | import TextField from 'material-ui/TextField'; 5 | 6 | import { CommonComponents } from '../CommonComponents'; 7 | import TimelineVariableTable from '../../../containers/TimelineNodeEditor/TimelineForm/TimelineVariableTableContainer'; 8 | import CodeEditor from '../../CodeEditor'; 9 | 10 | const colors = { 11 | ...theme.colors, 12 | } 13 | 14 | const style = { 15 | SelectFieldStyle: { 16 | selectedMenuItemStyle: { 17 | color: colors.secondary 18 | }, 19 | fullWidth: true, 20 | floatingLabelFixed: true, 21 | labelStyle: { 22 | color: colors.secondary 23 | } 24 | }, 25 | NumberFieldStyle: { 26 | fullWidth: true, 27 | type: "number", 28 | floatingLabelFixed: true, 29 | ...theme.TextFieldFocusStyle() 30 | } 31 | } 32 | 33 | class TimelineForm extends React.Component { 34 | render(){ 35 | return ( 36 |
37 | 38 |
39 | { this.props.setRandomize(value)}} 42 | floatingLabelText="Randomize Order" 43 | {...style.SelectFieldStyle} 44 | > 45 | 47 | 49 | 50 |
51 |
52 | 58 | 60 | 62 | 64 | 66 | 67 |
68 |
69 | this.props.setSampleSize(newVal)} 73 | floatingLabelText="Sample size" 74 | {...style.NumberFieldStyle} 75 | /> 76 |
77 |
78 | 85 |
86 | 87 |
88 | 98 | } 99 | /> 100 |
101 |
102 | 112 | } 113 | /> 114 |
115 |
116 | ) 117 | } 118 | } 119 | 120 | export default TimelineForm; 121 | -------------------------------------------------------------------------------- /src/common/components/TimelineNodeEditor/TimelineForm/index.js: -------------------------------------------------------------------------------- 1 | import TimelineForm from './TimelineForm.jsx'; 2 | 3 | export default TimelineForm; -------------------------------------------------------------------------------- /src/common/components/TimelineNodeEditor/TimelineNodeEditor.css: -------------------------------------------------------------------------------- 1 | .TimelineNode-Editor-Dragger:hover .TimelineNode-Editor-Close-Handle-Container{ 2 | display: inline-block; 3 | } 4 | 5 | .Trial-Form-Item-Container { 6 | width: 95%; 7 | display: -webkit-box; 8 | display: -ms-flexbox; 9 | display: flex; 10 | -webkit-box-align: baseline; 11 | -ms-flex-align: baseline; 12 | align-items: baseline; 13 | margin: 0 auto; 14 | } 15 | 16 | .Trial-Form-Item-Field-Container { 17 | -webkit-box-flex: 1; 18 | -ms-flex-positive: 1; 19 | flex-grow: 1; 20 | -webkit-box-align: baseline; 21 | -ms-flex-align: baseline; 22 | align-items: baseline; 23 | } 24 | 25 | .Trial-Form-Item-Tool-Container { 26 | float: right; 27 | -ms-flex-preferred-size: 96px; 28 | flex-basis: 96px; 29 | -webkit-box-align: baseline; 30 | -ms-flex-align: baseline; 31 | align-items: baseline; 32 | display: -webkit-box; 33 | display: -ms-flexbox; 34 | display: flex; 35 | } 36 | 37 | /* *********************************************** */ 38 | 39 | .Trial-Form-Label-Container { 40 | -ms-flex-preferred-size: 25%; 41 | flex-basis: 25%; 42 | word-wrap: break-word; 43 | white-space: -moz-pre-wrap; 44 | white-space: pre-wrap; 45 | text-align: left; 46 | } 47 | 48 | 49 | .Trial-Form-Content-Container { 50 | -webkit-box-flex: 1; 51 | -ms-flex-positive: 1; 52 | flex-grow: 1; 53 | display: -webkit-box; 54 | display: -ms-flexbox; 55 | display: flex; 56 | } -------------------------------------------------------------------------------- /src/common/components/TimelineNodeEditor/TrialForm/TimelineVariableSelector.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Dialog from 'material-ui/Dialog'; 3 | import Subheader from 'material-ui/Subheader'; 4 | import IconButton from 'material-ui/IconButton'; 5 | import RaisedButton from 'material-ui/RaisedButton'; 6 | import FlatButton from 'material-ui/FlatButton'; 7 | import Paper from 'material-ui/Paper'; 8 | import { List, ListItem } from 'material-ui/List'; 9 | 10 | import AddTimelineVarIcon from 'material-ui/svg-icons/action/swap-horiz'; 11 | import CheckNoIcon from 'material-ui/svg-icons/toggle/check-box-outline-blank'; 12 | import CheckYesIcon from 'material-ui/svg-icons/toggle/check-box'; 13 | 14 | import { DialogTitle } from '../../gadgets'; 15 | 16 | const colors = { 17 | ...theme.colors, 18 | checkColor: theme.colors.primary, 19 | } 20 | 21 | const style = { 22 | TriggerIcon: theme.Icon, 23 | Window: { 24 | contentStyle: { 25 | minHeight: 500 26 | }, 27 | titleStyle: { 28 | padding: 0 29 | } 30 | }, 31 | Content: { 32 | root: utils.prefixer({ 33 | minHeight: 400, 34 | maxHeight: 400, 35 | heigth: '100%' 36 | }), 37 | List: utils.prefixer({ 38 | minHeight: 400, 39 | maxHeight: 400, 40 | overflowY: 'auto', 41 | width: '98%', 42 | margin: 'auto' 43 | }), 44 | Empty: utils.prefixer({ 45 | textAlign: 'center', 46 | minHeight: 400, 47 | maxHeight: 400, 48 | display: 'flex', 49 | flexDirection: 'column', 50 | justifyContent: 'center', 51 | fontSize: '25px', 52 | lineHeight: '25px', 53 | fontWeight: 'bold', 54 | color: 'grey' 55 | }) 56 | } 57 | } 58 | 59 | 60 | class TimelineVariableSelector extends React.Component { 61 | constructor(props) { 62 | super(props); 63 | this.state = { 64 | open: false, 65 | } 66 | 67 | this.handleOpen = () => { 68 | this.setState({ 69 | open: true 70 | }); 71 | } 72 | 73 | this.handleClose = () => { 74 | this.setState({ 75 | open: false 76 | }); 77 | } 78 | } 79 | 80 | static defaultProps = { 81 | title: "Timeline Variables", 82 | onCommit: () => {}, 83 | Trigger: ({onClick}) => ( 84 | 85 | 86 | 87 | ), 88 | value: '' 89 | } 90 | 91 | render() { 92 | let { timelineVariables } = this.props; 93 | let isEmpty = Array.isArray(timelineVariables) && timelineVariables.length === 0; 94 | 95 | let list = ( 96 | 97 | { 98 | Array.isArray(timelineVariables) && timelineVariables.map((v) => ( 99 | { this.props.onCommit(v); }} 103 | rightIcon={ 104 | v === this.props.value ? : 105 | } 106 | /> 107 | )) 108 | } 109 | 110 | ) 111 | 112 | return ( 113 | 114 | 115 | 122 | {this.props.title} 123 | 124 | } 125 | closeCallback={this.handleClose} 126 | /> 127 | } 128 | actions={[]} 129 | {...style.Window} 130 | > 131 | 132 | {isEmpty ? 133 |
134 | There is no available timeline variable for this trial. 135 |
: 136 | list 137 | } 138 |
139 |
140 |
141 | ) 142 | } 143 | } 144 | 145 | export default TimelineVariableSelector; -------------------------------------------------------------------------------- /src/common/components/TimelineNodeEditor/TrialForm/TrialFormItem/index.js: -------------------------------------------------------------------------------- 1 | import TrialFormItem from './TrialFormItem.jsx'; 2 | 3 | export default TrialFormItem; -------------------------------------------------------------------------------- /src/common/components/TimelineNodeEditor/TrialForm/TrialFormItem/utils.js: -------------------------------------------------------------------------------- 1 | /* 2 | key: string, 3 | position: int, 4 | next: PathNode 5 | */ 6 | function PathNode(key, position=-1, next=null) { 7 | return { 8 | key: key, 9 | position: position, 10 | next: next 11 | }; 12 | } 13 | 14 | /* 15 | parameterInfo: jsPsych parameter information object (defined in jspsych), 16 | path: PathNode (defined above) 17 | */ 18 | const locateNestedParameterInfo = (paramInfo, path) => { 19 | let parameterInfo = paramInfo; 20 | 21 | if (typeof path === 'object') { 22 | while (path) { 23 | if (path.next) { 24 | parameterInfo = parameterInfo.nested; 25 | parameterInfo = parameterInfo[path.next.key]; 26 | } 27 | path = path.next; 28 | } 29 | } 30 | 31 | return parameterInfo 32 | } 33 | 34 | /* 35 | parameterInfo: jsPsych parameter information object (defined in jspsych) 36 | */ 37 | const isParameterRequired = (parameterInfo) => { 38 | let isRequired = false; 39 | if (parameterInfo.hasOwnProperty('default')) { 40 | isRequired = parameterInfo.default === undefined; 41 | } 42 | return isRequired; 43 | } 44 | 45 | export { 46 | PathNode, 47 | locateNestedParameterInfo, 48 | isParameterRequired, 49 | }; 50 | 51 | -------------------------------------------------------------------------------- /src/common/components/TimelineNodeEditor/TrialForm/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import TrialFormItem from '../../../containers/TimelineNodeEditor/TrialForm/TrialFormItemContainer'; 4 | import { injectJsPsychUniversalPluginParameters } from '../../../utils'; 5 | 6 | const jsPsych = window.jsPsych; 7 | const PluginList = Object.keys(jsPsych.plugins).filter((t) => (t !== 'parameterType' && t !== 'universalPluginParameters')); 8 | 9 | 10 | class TrialForm extends React.Component { 11 | renderPluginParams = () => { 12 | if (!this.props.pluginType) return null; 13 | let pluginInfo = jsPsych.plugins[this.props.pluginType].info; 14 | let parameters = injectJsPsychUniversalPluginParameters(pluginInfo.parameters); 15 | // params are the keys of plugin.info 16 | /* e.g. 17 | param --> 18 | stimulus: { 19 | type: [jsPsych.plugins.parameterType.AUDIO], 20 | default: undefined, 21 | no_function: false, 22 | description: '' 23 | }, 24 | */ 25 | // paramTypes are the type (jspsych enum) of the above param 26 | /* 27 | props explanations: 28 | 29 | param: Field name of a plugin's parameter 30 | For example, "stimulus" would be the param 31 | 32 | paramInfo: jsPsych.plugins[Plugin Type].info.parameters[Field Name] 33 | For example, { 34 | type: [jsPsych.plugins.parameterType.AUDIO], 35 | default: undefined, 36 | no_function: false, 37 | description: '' 38 | } would be the paramInfo 39 | 40 | */ 41 | return Object.keys(parameters).map((param, i) => { 42 | return ( 43 | 48 | )}); 49 | } 50 | 51 | render() { 52 | return ( 53 |
54 | {this.renderPluginParams()} 55 |
56 | ) 57 | } 58 | } 59 | 60 | 61 | export default TrialForm; 62 | -------------------------------------------------------------------------------- /src/common/components/TimelineNodeEditor/index.js: -------------------------------------------------------------------------------- 1 | import TimelineNodeEditor from './TimelineNodeEditor.jsx'; 2 | 3 | export default TimelineNodeEditor; -------------------------------------------------------------------------------- /src/common/components/TimelineNodeOrganizer/SortableTreeMenu/NestedContextMenus.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Menu from 'material-ui/Menu'; 3 | import Popover from 'material-ui/Popover'; 4 | import MenuItem from 'material-ui/MenuItem'; 5 | import Divider from 'material-ui/Divider'; 6 | 7 | import NewTimelineIcon from 'material-ui/svg-icons/av/playlist-add'; 8 | import NewTrialIcon from 'material-ui/svg-icons/action/note-add'; 9 | import Delete from 'material-ui/svg-icons/action/delete'; 10 | import Duplicate from 'material-ui/svg-icons/content/content-copy'; 11 | 12 | import SelectAllIcon from 'material-ui/svg-icons/content/select-all'; 13 | import DeselectAllIcon from 'material-ui/svg-icons/content/block'; 14 | import SelectThisOnlyIcon from 'material-ui/svg-icons/device/gps-fixed'; 15 | import CheckIcon from 'material-ui/svg-icons/toggle/radio-button-checked'; 16 | import UnCheckIcon from 'material-ui/svg-icons/toggle/radio-button-unchecked'; 17 | 18 | import theme from './theme.js'; 19 | 20 | const contextMenuStyle = theme.contextMenuStyle; 21 | 22 | export default class NestedContextMenus extends React.Component { 23 | 24 | render() { 25 | return ( 26 |
27 | 34 | 35 | } 37 | onClick={()=>{ this.props.insertTimeline(); this.props.onRequestCloseItemMenu()}} 38 | /> 39 | 40 | } 42 | onClick={()=>{ this.props.insertTrial(); this.props.onRequestCloseItemMenu()}} 43 | /> 44 | } 46 | onClick={()=>{ this.props.deleteNode(); this.props.onRequestCloseItemMenu()}} 47 | /> 48 | 49 | } 51 | onClick={()=>{ this.props.duplicateNode(); this.props.onRequestCloseItemMenu()}} 52 | /> 53 | 54 | : 58 | 59 | } 60 | onClick={()=>{ this.props.onToggle(); this.props.onRequestCloseItemMenu()}} 61 | /> 62 | 63 | 64 |
65 | ) 66 | } 67 | } 68 | 69 | -------------------------------------------------------------------------------- /src/common/components/TimelineNodeOrganizer/SortableTreeMenu/TimelineItem.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import IconButton from 'material-ui/IconButton'; 3 | import { ListItem } from 'material-ui/List'; 4 | 5 | import CollapsedIcon from 'material-ui/svg-icons/navigation/chevron-right'; 6 | import ExpandedIcon from 'material-ui/svg-icons/navigation/expand-more'; 7 | 8 | 9 | import { DropTarget, DragSource } from 'react-dnd'; 10 | import flow from 'lodash/flow'; 11 | 12 | import Tree from '../../../containers/TimelineNodeOrganizer/SortableTreeMenu/TreeContainer'; 13 | import NestedContextMenus from './NestedContextMenus'; 14 | import { moveToAction, moveIntoAction } from '../../../actions/organizerActions'; 15 | 16 | import theme, { INDENT, colorSelector, listItemStyle } from './theme.js'; 17 | 18 | 19 | export const treeNodeDnD = { 20 | ITEM_TYPE: "Organizer-Item", 21 | 22 | itemSource: { 23 | beginDrag(props) { 24 | return { 25 | id: props.id, 26 | parent: props.parent, 27 | children: props.children 28 | }; 29 | }, 30 | 31 | isDragging(props, monitor) { 32 | return props.id === monitor.getItem().id; 33 | } 34 | }, 35 | 36 | itemTarget: { 37 | // better this way since we always want hover (for preview effects) 38 | canDrop() { 39 | return false; 40 | }, 41 | 42 | hover(props, monitor, component) { 43 | const {id: draggedId } = monitor.getItem() 44 | const {id: overId, lastItem } = props 45 | 46 | // leave 47 | // if parent dragged into its children (will check more in redux) 48 | // or if source is not over current target 49 | if (draggedId === props.parent || 50 | !monitor.isOver({shallow: true})) { 51 | return; 52 | } 53 | 54 | // allow move into 55 | let offset = monitor.getDifferenceFromInitialOffset(); 56 | if (draggedId === overId) { 57 | if (offset.x >= INDENT && draggedId) { 58 | let action = moveIntoAction(draggedId); 59 | props.dispatch(action); 60 | } 61 | return; 62 | } 63 | 64 | let isLast = lastItem === draggedId; 65 | if (offset.x < 0 && !isLast) { 66 | return; 67 | } 68 | 69 | // replace 70 | props.dispatch(moveToAction(draggedId, overId, isLast)); 71 | } 72 | }, 73 | 74 | sourceCollector: (connect, monitor) => ({ 75 | connectDragSource: connect.dragSource(), 76 | connectDragPreview: connect.dragPreview(), 77 | isDragging: monitor.isDragging(), 78 | }), 79 | 80 | targetCollector: (connect, monitor) => ({ 81 | connectDropTarget: connect.dropTarget(), 82 | isOverCurrent: monitor.isOver({ 83 | shallow: true 84 | }), 85 | }) 86 | } 87 | 88 | var keyboardFocusId = null; 89 | 90 | export const setKeyboardFocusId = (id) => { 91 | keyboardFocusId = id; 92 | } 93 | 94 | export const getKeyboardFocusId = () => (keyboardFocusId); 95 | 96 | class TimelineItem extends React.Component { 97 | constructor(props) { 98 | super(props); 99 | 100 | this.state = { 101 | contextMenuOpen: false, 102 | toggleContextMenuOpen: false, 103 | } 104 | 105 | this.openContextMenu = (event) => { 106 | event.preventDefault(); 107 | event.stopPropagation(); 108 | this.setState({ 109 | contextMenuOpen: true, 110 | anchorEl: event.currentTarget, 111 | }) 112 | } 113 | 114 | this.closeContextMenu = () => { 115 | this.setState({ 116 | contextMenuOpen: false 117 | }) 118 | } 119 | 120 | this.openToggleContextMenu = (event) => { 121 | event.preventDefault(); 122 | event.stopPropagation(); 123 | this.setState({ 124 | toggleContextMenuOpen: true, 125 | anchorEl: event.currentTarget, 126 | }) 127 | } 128 | 129 | this.closeToggleContextMenu = () => { 130 | this.setState({ 131 | toggleContextMenuOpen: false 132 | }) 133 | } 134 | } 135 | 136 | componentDidMount() { 137 | if (getKeyboardFocusId() === this.props.id) { 138 | this.refs[this.props.id].applyFocusState('keyboard-focused'); 139 | } 140 | } 141 | 142 | render() { 143 | const { 144 | connectDropTarget, 145 | connectDragPreview, 146 | connectDragSource, 147 | isOverCurrent, 148 | isEnabled, 149 | isSelected 150 | } = this.props; 151 | 152 | return connectDragPreview(connectDropTarget( 153 |
154 |
158 | { 159 | connectDragSource( 160 |
161 | 166 | {(this.props.collapsed || this.props.hasNoChildren) ? 167 | : 168 | 169 | } 170 | 171 |
172 | )} 173 |
174 | { this.props.listenKey(e, getKeyboardFocusId) }} 179 | onContextMenu={this.openContextMenu} 180 | onClick={(e) => { 181 | if (e.nativeEvent.which === 1) { 182 | this.props.onClick(setKeyboardFocusId); 183 | } 184 | }} 185 | /> 186 |
187 | 201 |
202 |
203 | 207 |
208 |
209 | )) 210 | } 211 | } 212 | 213 | export default flow( 214 | DragSource( 215 | treeNodeDnD.ITEM_TYPE, 216 | treeNodeDnD.itemSource, 217 | treeNodeDnD.sourceCollector), 218 | DropTarget( 219 | treeNodeDnD.ITEM_TYPE, 220 | treeNodeDnD.itemTarget, 221 | treeNodeDnD.targetCollector))(TimelineItem); -------------------------------------------------------------------------------- /src/common/components/TimelineNodeOrganizer/SortableTreeMenu/Tree.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TreeNode from '../../../containers/TimelineNodeOrganizer/SortableTreeMenu/TreeNodeContainer'; 3 | 4 | 5 | class Tree extends React.Component { 6 | 7 | render() { 8 | const { 9 | children, 10 | collapsed, 11 | } = this.props; 12 | 13 | return ( 14 |
15 | {(collapsed) ? 16 | null: 17 | (children && children.map((id) => ( 18 | 23 | ))) 24 | } 25 |
26 | ) 27 | } 28 | } 29 | 30 | export default Tree; 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/common/components/TimelineNodeOrganizer/SortableTreeMenu/TreeNode.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import TrialItem from '../../../containers/TimelineNodeOrganizer/SortableTreeMenu/TrialItemContainer'; 4 | import TimelineItem from '../../../containers/TimelineNodeOrganizer/SortableTreeMenu/TimelineItemContainer'; 5 | 6 | class TreeNode extends React.Component { 7 | 8 | render() { 9 | const { 10 | id, 11 | children, 12 | } = this.props; 13 | 14 | return ( 15 |
16 | {(this.props.isTimeline) ? 17 | () : 23 | ()} 28 |
29 | ) 30 | } 31 | } 32 | 33 | export default TreeNode; 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/common/components/TimelineNodeOrganizer/SortableTreeMenu/TrialItem.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import IconButton from 'material-ui/IconButton'; 4 | import { ListItem } from 'material-ui/List'; 5 | 6 | import TrialIcon from 'material-ui/svg-icons/editor/mode-edit'; 7 | 8 | import { DropTarget, DragSource } from 'react-dnd'; 9 | import flow from 'lodash/flow'; 10 | import { 11 | treeNodeDnD, 12 | setKeyboardFocusId, 13 | getKeyboardFocusId 14 | } from './TimelineItem'; 15 | 16 | import NestedContextMenus from './NestedContextMenus'; 17 | 18 | import theme, { colorSelector, listItemStyle } from './theme.js'; 19 | 20 | class TrialItem extends React.Component { 21 | constructor(props) { 22 | super(props); 23 | 24 | this.state = { 25 | contextMenuOpen: false, 26 | toggleContextMenuOpen: false, 27 | } 28 | 29 | this.openContextMenu = (event) => { 30 | event.preventDefault(); 31 | event.stopPropagation(); 32 | this.setState({ 33 | contextMenuOpen: true, 34 | anchorEl: event.currentTarget, 35 | }) 36 | } 37 | 38 | this.closeContextMenu = () => { 39 | this.setState({ 40 | contextMenuOpen: false 41 | }) 42 | } 43 | 44 | this.openToggleContextMenu = (event) => { 45 | event.preventDefault(); 46 | event.stopPropagation(); 47 | this.setState({ 48 | toggleContextMenuOpen: true, 49 | anchorEl: event.currentTarget, 50 | }) 51 | } 52 | 53 | this.closeToggleContextMenu = () => { 54 | this.setState({ 55 | toggleContextMenuOpen: false 56 | }) 57 | } 58 | } 59 | 60 | componentDidMount() { 61 | if (getKeyboardFocusId() === this.props.id) { 62 | this.refs[this.props.id].applyFocusState('keyboard-focused'); 63 | } 64 | } 65 | 66 | render() { 67 | const { 68 | connectDropTarget, 69 | connectDragPreview, 70 | connectDragSource, 71 | isOverCurrent, 72 | isEnabled, 73 | isSelected, 74 | onClick, 75 | id, 76 | name, 77 | listenKey 78 | } = this.props; 79 | 80 | return connectDragPreview(connectDropTarget( 81 |
82 |
86 | { 87 | connectDragSource(
88 | 92 | 93 | 94 |
) 95 | } 96 |
97 | { listenKey(e, getKeyboardFocusId) }} 102 | onContextMenu={this.openContextMenu} 103 | onClick={(e) => { 104 | if (e.nativeEvent.which === 1) { 105 | this.props.onClick(setKeyboardFocusId); 106 | } 107 | }} 108 | /> 109 |
110 | 124 |
125 |
126 | )) 127 | } 128 | } 129 | 130 | export default flow( 131 | DragSource( 132 | treeNodeDnD.ITEM_TYPE, 133 | treeNodeDnD.itemSource, 134 | treeNodeDnD.sourceCollector), 135 | DropTarget( 136 | treeNodeDnD.ITEM_TYPE, 137 | treeNodeDnD.itemTarget, 138 | treeNodeDnD.targetCollector))(TrialItem) -------------------------------------------------------------------------------- /src/common/components/TimelineNodeOrganizer/SortableTreeMenu/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Tree from '../../../containers/TimelineNodeOrganizer/SortableTreeMenu/TreeContainer'; 4 | 5 | class SortableTreeMenu extends React.Component { 6 | 7 | render() { 8 | const { openTimelineEditorCallback, closeTimelineEditorCallback } = this.props; 9 | 10 | return ( 11 |
12 | 16 |
17 | ) 18 | } 19 | } 20 | 21 | export default utils.withDnDContext(SortableTreeMenu); 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/common/components/TimelineNodeOrganizer/SortableTreeMenu/theme.js: -------------------------------------------------------------------------------- 1 | import GeneralTheme from '../../theme.js'; 2 | 3 | 4 | const colors = { 5 | ...GeneralTheme.colors, 6 | deepGrey: '#424242', 7 | lightDeepGrey: '#757575', 8 | lightGrey: '#BDBDBD', 9 | } 10 | 11 | export const INDENT = 32; 12 | 13 | export const colorSelector = (hovered, isSelected) => { 14 | if (hovered) return null; 15 | if (isSelected) return '#E3E3E3' 16 | return null; 17 | } 18 | 19 | export const listItemStyle = (isEnabled, isSelected) => ({ 20 | color: isEnabled ? colors.deepGrey : colors.lightGrey, 21 | fontWeight: isSelected ? 'bold' : 'normal', 22 | }) 23 | 24 | const iconColorSelector = (isEnabled, isSelected) => { 25 | if (isSelected) return colors.secondary; // deep orange 26 | return isEnabled ? colors.lightDeepGrey : colors.lightGrey; 27 | } 28 | 29 | const theme = { 30 | collpaseButtonHoverStyle: { 31 | backgroundColor: '#CDCDCD' 32 | }, 33 | icon: (isEnabled, isSelected) => ({ 34 | color: iconColorSelector(isEnabled, isSelected), 35 | hoverColor: colors.secondaryLight, 36 | }), 37 | contextMenuStyle: { 38 | outerDiv: { 39 | position: 'absolute', 40 | zIndex: 20 41 | }, 42 | innerDiv: { 43 | backgroundColor: '#F5F5F5', 44 | borderBottom: '1px solid #BDBDBD' 45 | }, 46 | lastInnerDiv: { 47 | backgroundColor: '#F5F5F5' 48 | }, 49 | iconColor: colors.secondaryDeep, 50 | } 51 | } 52 | 53 | 54 | export default theme; -------------------------------------------------------------------------------- /src/common/components/TimelineNodeOrganizer/TimelineNodeOrganizer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Subheader from 'material-ui/Subheader'; 3 | import IconButton from 'material-ui/IconButton'; 4 | import Divider from 'material-ui/Divider'; 5 | import { List } from 'material-ui/List'; 6 | import Avatar from 'material-ui/Avatar'; 7 | import { SpeedDial, BubbleList, BubbleListItem } from 'react-speed-dial'; 8 | 9 | import ContentAdd from 'material-ui/svg-icons/content/add'; 10 | import NavigationClose from 'material-ui/svg-icons/navigation/close'; 11 | import NewTimelineIcon from 'material-ui/svg-icons/av/playlist-add'; 12 | import NewTrialIcon from 'material-ui/svg-icons/action/note-add'; 13 | import Delete from 'material-ui/svg-icons/action/delete'; 14 | import Duplicate from 'material-ui/svg-icons/content/content-copy'; 15 | 16 | import CloseDrawerHandle from 'material-ui/svg-icons/navigation/chevron-left'; 17 | import OpenDrawer from 'material-ui/svg-icons/navigation/chevron-right'; 18 | 19 | import SortableTreeMenu from '../../containers/TimelineNodeOrganizer/SortableTreeMenu'; 20 | 21 | import GeneralTheme from '../theme.js'; 22 | 23 | export const TREE_MENU_INDENT = 20; 24 | 25 | export const WIDTH = 285; 26 | 27 | const colors = GeneralTheme.colors; 28 | 29 | const duration = 400; 30 | 31 | const style = { 32 | TimelineNodeOrganizer: (open) => (utils.prefixer({ 33 | width: (open) ? `${WIDTH}px` : "0px", 34 | flexBasis: 'auto', 35 | flexShrink: 0, 36 | 'WebkitTransition': `all ${duration}ms ease`, 37 | 'MozTransition': `all ${duration}ms ease`, 38 | transition: `all ${duration}ms ease`, 39 | height: '100%', 40 | display: 'flex', 41 | overflow: 'hidden', 42 | flexDirection: 'row', 43 | backgroundColor: colors.background 44 | })), 45 | TimelineNodeOrganizerContainer: utils.prefixer({ 46 | height: '100%', 47 | width: '100%', 48 | position: 'relative', 49 | flexGrow: '1', 50 | }), 51 | TimelineNodeOrganizerContent: utils.prefixer({ 52 | height: '100%', 53 | width: '100%', 54 | display: 'flex', 55 | flexDirection: 'column-reverse' 56 | }), 57 | TimelineNodeSheet: utils.prefixer({ 58 | overflow: 'auto', 59 | flexGrow: 1, 60 | maxWidth: '100%', 61 | paddingLeft: '0px', 62 | width: '100%', 63 | position: 'relative' 64 | }), 65 | SpeedDial: { 66 | FloatingActionButton: { 67 | backgroundColor: colors.primary, 68 | }, 69 | AvatarStyle: { 70 | backgroundColor: colors.primaryDeep 71 | } 72 | }, 73 | } 74 | 75 | 76 | class TimelineNodeOrganizer extends React.Component { 77 | constructor(props) { 78 | super(props); 79 | 80 | this.state = { 81 | isSpeedDialOpen: false 82 | } 83 | 84 | this.handleToogleSpeedDialOpen = () => { 85 | this.setState({ 86 | isSpeedDialOpen: !this.state.isSpeedDialOpen, 87 | }); 88 | } 89 | 90 | this.handleChangeSpeedDial = ({ isOpen }) => { 91 | this.setState({ 92 | isSpeedDialOpen: isOpen, 93 | }); 94 | } 95 | } 96 | 97 | render() { 98 | return ( 99 |
100 |
101 | {(this.props.open) ? 102 |
103 |
104 | 108 |
109 | 121 | 122 | } 127 | {...style.SpeedDial.AvatarStyle} 128 | size={30} 129 | /> 130 | } 131 | onClick={() => { this.handleToogleSpeedDialOpen(); this.props.insertTimeline(); }} 132 | /> 133 | } 138 | {...style.SpeedDial.AvatarStyle} 139 | size={30} 140 | /> 141 | } 142 | onClick={() => { this.handleToogleSpeedDialOpen(); this.props.insertTrial(); }} 143 | /> 144 | } 149 | {...style.SpeedDial.AvatarStyle} 150 | size={30} 151 | /> 152 | } 153 | onClick={() => { this.handleToogleSpeedDialOpen(); this.props.deleteSelected(); }} 154 | /> 155 | } 160 | {...style.SpeedDial.AvatarStyle} 161 | size={30} 162 | /> 163 | } 164 | onClick={() => { this.handleToogleSpeedDialOpen(); this.props.duplicateNode(); }} 165 | /> 166 | 167 | 168 |
: null} 169 |
170 |
171 | ) 172 | } 173 | } 174 | 175 | 176 | export default TimelineNodeOrganizer; 177 | -------------------------------------------------------------------------------- /src/common/components/TimelineNodeOrganizer/index.js: -------------------------------------------------------------------------------- 1 | import TimelineNodeOrganizer from './TimelineNodeOrganizer.jsx'; 2 | 3 | export default TimelineNodeOrganizer; -------------------------------------------------------------------------------- /src/common/components/gadgets/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | Repeatedly used components 3 | 4 | */ 5 | 6 | 7 | import React from 'react'; 8 | import IconButton from 'material-ui/IconButton'; 9 | import FlatButton from 'material-ui/FlatButton'; 10 | import TextField from 'material-ui/TextField'; 11 | import Close from 'material-ui/svg-icons/navigation/close'; 12 | import { 13 | grey50 as dialogTitleColor, 14 | grey300 as CloseBackHighlightColor, 15 | grey50 as CloseDrawerHoverColor 16 | } from 'material-ui/styles/colors'; 17 | 18 | const colors = { 19 | ...theme.colors, 20 | dividerColor: 'rgb(224, 224, 224)', 21 | dialogTitleColor: '#FAFAFA', 22 | closeBackHighlightColor: '#E0E0E0', 23 | closeDrawerHoverColor: '#FAFAFA', 24 | }; 25 | 26 | const style = { 27 | } 28 | 29 | export const DialogTitle = ({ 30 | node = null, 31 | closeCallback = () => {}, 32 | titleColor = colors.dialogTitleColor, 33 | style = {}, 34 | showCloseButton = true, 35 | ...props 36 | }) => { 37 | return ( 38 |
39 | { node } 40 | { showCloseButton ? 41 | 48 | 49 | : 50 | null 51 | } 52 |
53 | ) 54 | } 55 | 56 | export const renderDialogTitle = (messageNode = null, 57 | handleClose = () => {}, 58 | titleColor = colors.dialogTitleColor, 59 | style = {}, 60 | showCloseButton = true 61 | ) => ( 62 |
63 | {messageNode} 64 | { showCloseButton ? 65 | 72 | 73 | : 74 | null 75 | } 76 | 77 |
78 | ) 79 | 80 | 81 | export const Text = ({text, style={}, ...props}) => ( 82 |
96 | {text} 97 |
98 | ) 99 | 100 | export class EditorTextField extends React.Component { 101 | constructor(props) { 102 | super(props); 103 | this.state = { 104 | value: this.props.value 105 | }; 106 | 107 | this.onChange = (e, v) => { 108 | this.setState({ 109 | value: v 110 | }); 111 | }; 112 | 113 | this.onCommit = () => { 114 | this.props.onCommit(this.state.value); 115 | }; 116 | } 117 | 118 | static defaultProps = { 119 | value: "", 120 | onCommit: v => {}, 121 | styles: {} 122 | }; 123 | 124 | static getDerivedStateFromProps(nextProps, prevState) { 125 | return { 126 | ...nextProps 127 | }; 128 | } 129 | 130 | render() { 131 | const { value } = this.state; 132 | const { onCommit, value : propValue, ...textFieldProps } = this.props; 133 | 134 | return ( 135 | { 141 | if (e.which === 13) { 142 | document.activeElement.blur(); 143 | } 144 | }} 145 | id={`textfield-${utils.getUUID()}`} 146 | /> 147 | ); 148 | } 149 | } -------------------------------------------------------------------------------- /src/common/components/theme.js: -------------------------------------------------------------------------------- 1 | 2 | const colors = { 3 | primary: '#24B24C', // green 4 | primaryDeep: '#04673A', // deep green 5 | secondary: '#FF9800', // orange 6 | background: '#F0F0F0', // grey 7 | secondaryDeep: '#FF5722', 8 | secondaryLight: '#FFB74D', 9 | font: 'white', 10 | errorRed: '#F34335', 11 | defaultFontColor: '#424242', 12 | }; 13 | 14 | const Icon = { 15 | color: colors.primary, 16 | hoverColor: colors.secondary 17 | }; 18 | 19 | const TextFieldFocusStyle = (error = false) => { 20 | var res = { 21 | floatingLabelFocusStyle: { 22 | color: error ? colors.errorRed : colors.secondary 23 | }, 24 | underlineFocusStyle: { 25 | borderColor: error ? colors.errorRed : colors.secondary 26 | } 27 | } 28 | 29 | if (error) { 30 | res.floatingLabelStyle = { 31 | color: colors.errorRed 32 | } 33 | } 34 | 35 | return res; 36 | }; 37 | 38 | export default { 39 | colors: colors, 40 | Icon: Icon, 41 | TextFieldFocusStyle: TextFieldFocusStyle 42 | }; -------------------------------------------------------------------------------- /src/common/constants/ActionTypes.js: -------------------------------------------------------------------------------- 1 | 2 | export const ActionTypes = { 3 | // organizer 4 | ADD_TIMELINE: "ADD_TIMELINE", 5 | DELETE_TIMELINE: "DELETE_TIMELINE", 6 | ADD_TRIAL: "ADD_TRIAL", 7 | DELETE_TRIAL: "DELETE_TRIAL", 8 | MOVE_TO: "MOVE_TO", 9 | MOVE_INTO: "MOVE_INTO", 10 | MOVE_BY_KEYBOARD: "MOVE_BY_KEYBOARD", 11 | ON_PREVIEW: "ON_PREVIEW", 12 | ON_TOGGLE: "ON_TOGGLE", 13 | SET_COLLAPSED: "SET_COLLAPSED", 14 | INSERT_NODE_AFTER_TRIAL: "INSERT_NODE_AFTER_TRIAL", 15 | DUPLICATE_TIMELINE: "DUPLICATE_TIMELINE", 16 | DUPLICATE_TRIAL: "DUPLICATE_TRIAL", 17 | SET_NAME: "SET_NAME", 18 | 19 | // jsPsych init editor 20 | SET_JSPSYCH_INIT: "SET_JSPSYCH_INIT", 21 | 22 | // preview 23 | PLAY_ALL: "PLAY_ALL", 24 | 25 | // main 26 | SET_EXPERIMENT_NAME: "SET_EXPERIMENT_NAME", 27 | LOAD_EXPERIMENT: "LOAD_EXPERIMENT", 28 | LOAD_USER: "LOAD_USER", 29 | 30 | // editor 31 | SET_PLUGIN_PARAMTER: "SET_PLUGIN_PARAMTER", 32 | SET_PLUGIN_PARAMTER_MODE: "SET_PLUGIN_PARAMTER_MODE", 33 | UPDATE_MEDIA: "UPDATE_MEDIA", 34 | 35 | // editor - TrialForm Actions 36 | CHANGE_PLUGIN_TYPE: "CHANGE_PLUGIN_TYPE", 37 | 38 | // editor - TimelineForm Actions 39 | UPDATE_TIMELINE_VARIABLE_INPUT_TYPE: "UPDATE_TIMELINE_VARIABLE_INPUT_TYPE", 40 | UPDATE_TIMELINE_VARIABLE_CELL: "UPDATE_TIMELINE_VARIABLE_CELL", 41 | UPDATE_TIMELINE_VARIABLE_TABLE_HEADER: "UPDATE_TIMELINE_VARIABLE_TABLE_HEADER", 42 | ADD_TIMELINE_VARIABLE_ROW: "ADD_TIMELINE_VARIABLE_ROW", 43 | ADD_TIMELINE_VARIABLE_COLUMN: "ADD_TIMELINE_VARIABLE_COLUMN", 44 | DELETE_TIMELINE_VARIABLE_ROW: "DELETE_TIMELINE_VARIABLE_ROW", 45 | DELETE_TIMELINE_VARIABLE_COLUMN: "DELETE_TIMELINE_VARIABLE_COLUMN", 46 | MOVE_TIMELINE_VARIABLE_ROW_TO: "MOVE_TIMELINE_VARIABLE_ROW_TO", 47 | SET_SAMPLING_METHOD: "SET_SAMPLING_METHOD", 48 | SET_SAMPLE_SIZE: "SET_SAMPLE_SIZE", 49 | SET_RANDOMIZE: "SET_RANDOMIZE", 50 | SET_REPETITIONS: "SET_REPETITIONS", 51 | SET_LOOP_FUNCTION: "SET_LOOP_FUNCTION", 52 | SET_CONDITION_FUNCTION: "SET_CONDITION_FUNCTION", 53 | SET_TIMELINE_VARIABLE: "SET_TIMELINE_VARIABLE", 54 | 55 | // Cloud 56 | SET_OSF_ACCESS: "SET_OSF_ACCESS", 57 | SET_CLOUD_DEPLOY_INFO: "SET_CLOUD_DEPLOY_INFO", 58 | SET_DIY_DEPLOY_INFO: "SET_DIY_DEPLOY_INFO", 59 | 60 | /************ GUI States ************/ 61 | 62 | // Notifcations Window 63 | NOTIFY_WARNING_DIALOG: "NOTIFY_WARNING_DIALOG", 64 | NOTIFY_WARNING_SNACKBAR: "NOTIFY_WARNING_SNACKBAR", 65 | NOTIFY_SUCCESS_DIALOG: "NOTIFY_SUCCESS_DIALOG", 66 | NOTIFY_SUCCESS_SNACKBAR: "NOTIFY_SUCCESS_SNACKBAR", 67 | NOTIFY_ERROR_DIALOG: "NOTIFY_ERROR_DIALOG", 68 | NOTIFY_ERROR_SNACKBAR: "NOTIFY_ERROR_SNACKBAR", 69 | NOTIFY_DIALOG_CLOSE: "NOTIFY_DIALOG_CLOSE", 70 | NOTIFY_SNACKBAR_CLOSE: "NOTIFY_SNACKBAR_CLOSE", 71 | POP_UP_CONFIRM: "POP_UP_CONFIRM", 72 | 73 | // Authentications Window 74 | SET_AUTH_WINDOW: "SET_AUTH_WINDOW", 75 | } 76 | 77 | 78 | export const actionCreator = ({type, ...args}) => ({ 79 | type: type, 80 | ...args 81 | }) 82 | 83 | 84 | // organizer 85 | export const ADD_TIMELINE = "ADD_TIMELINE"; 86 | export const DELETE_TIMELINE = "DELETE_TIMELINE"; 87 | export const ADD_TRIAL = "ADD_TRIAL"; 88 | export const DELETE_TRIAL = "DELETE_TRIAL"; 89 | export const MOVE_TO = "MOVE_TO"; 90 | export const MOVE_INTO = "MOVE_INTO"; 91 | export const MOVE_BY_KEYBOARD = "MOVE_BY_KEYBOARD"; 92 | export const ON_PREVIEW = "ON_PREVIEW"; 93 | export const ON_TOGGLE = "ON_TOGGLE"; 94 | export const SET_COLLAPSED = "SET_COLLAPSED"; 95 | export const INSERT_NODE_AFTER_TRIAL = "INSERT_NODE_AFTER_TRIAL"; 96 | export const DUPLICATE_TIMELINE = "DUPLICATE_TIMELINE"; 97 | export const DUPLICATE_TRIAL = "DUPLICATE_TRIAL"; 98 | export const SET_NAME = "SET_NAME"; 99 | 100 | // jsPsych init editor 101 | export const SET_JSPSYCH_INIT = "SET_JSPSYCH_INIT"; 102 | 103 | // preview 104 | export const PLAY_ALL = "PLAY_ALL"; 105 | 106 | // main 107 | export const SET_EXPERIMENT_NAME = "SET_EXPERIMENT_NAME"; 108 | 109 | // editor 110 | export const SET_PLUGIN_PARAMTER = "SET_PLUGIN_PARAMTER"; 111 | export const SET_PLUGIN_PARAMTER_MODE = "SET_PLUGIN_PARAMTER_MODE"; 112 | export const UPDATE_MEDIA = "UPDATE_MEDIA"; 113 | 114 | // editor - TrialForm Actions 115 | export const CHANGE_PLUGIN_TYPE = "CHANGE_PLUGIN_TYPE"; 116 | 117 | // editor - TimelineForm Actions 118 | export const UPDATE_TIMELINE_VARIABLE_INPUT_TYPE = "UPDATE_TIMELINE_VARIABLE_INPUT_TYPE"; 119 | export const UPDATE_TIMELINE_VARIABLE_CELL = "UPDATE_TIMELINE_VARIABLE_CELL"; 120 | export const UPDATE_TIMELINE_VARIABLE_TABLE_HEADER = "UPDATE_TIMELINE_VARIABLE_TABLE_HEADER"; 121 | export const ADD_TIMELINE_VARIABLE_ROW = "ADD_TIMELINE_VARIABLE_ROW"; 122 | export const ADD_TIMELINE_VARIABLE_COLUMN = "ADD_TIMELINE_VARIABLE_COLUMN"; 123 | export const DELETE_TIMELINE_VARIABLE_ROW = "DELETE_TIMELINE_VARIABLE_ROW"; 124 | export const DELETE_TIMELINE_VARIABLE_COLUMN = "DELETE_TIMELINE_VARIABLE_COLUMN"; 125 | export const MOVE_TIMELINE_VARIABLE_ROW_TO = "MOVE_TIMELINE_VARIABLE_ROW_TO" 126 | export const SET_SAMPLING_METHOD = "SET_SAMPLING_METHOD"; 127 | export const SET_SAMPLE_SIZE = "SET_SAMPLE_SIZE"; 128 | export const SET_RANDOMIZE = "SET_RANDOMIZE"; 129 | export const SET_REPETITIONS = "SET_REPETITIONS"; 130 | export const SET_LOOP_FUNCTION = "SET_LOOP_FUNCTION"; 131 | export const SET_CONDITION_FUNCTION = "SET_CONDITION_FUNCTION"; 132 | export const SET_TIMELINE_VARIABLE = "SET_TIMELINE_VARIABLE"; 133 | 134 | // Cloud 135 | export const SET_OSF_ACCESS = "SET_OSF_ACCESS"; 136 | export const SET_CLOUD_DEPLOY_INFO = "SET_CLOUD_DEPLOY_INFO"; 137 | export const SET_DIY_DEPLOY_INFO = "SET_DIY_DEPLOY_INFO"; -------------------------------------------------------------------------------- /src/common/constants/Errors.js: -------------------------------------------------------------------------------- 1 | export const InternetError = new Error("Your internet may be disconnected !"); 2 | 3 | export class NotVerifiedException extends Error { 4 | constructor(message='You must verify your account first.') { 5 | super(message); 6 | this.name = 'NotVerifiedException'; 7 | this.message = message; 8 | } 9 | } 10 | 11 | export class NoCurrentUserException extends Error { 12 | constructor(message='There is no currently signed in user.') { 13 | super(message); 14 | this.name = 'NoCurrentUserException'; 15 | this.code = 'NoCurrentUserException'; 16 | this.message = message; 17 | } 18 | } -------------------------------------------------------------------------------- /src/common/constants/core.js: -------------------------------------------------------------------------------- 1 | import { initState as jsPsychInitState } from '../reducers/Experiment/jsPsychInit.js'; 2 | 3 | /** 4 | * @typeof {(string|number|object)} guiValue - A native javascript value. 5 | * It is important that if the input value is empty string, it should be converted to null for AWS.DynamoDB storage purpose. 6 | */ 7 | 8 | /** 9 | * User State Template 10 | * @namespace ExperimentState 11 | * @property {guiValue} username=null - User Name 12 | * @property {guiValue} userId=null - User's identity id (see docs for AWS.Cognito) 13 | * @property {guiValue} email=null - User's email 14 | * @property {Array.} osfAccess=[] - Access information to OSF 15 | * @property {Array.} diyAccess=[] - Access information to user's server 16 | * @description State template for User state. 17 | * ***NOTE THAT***: All empty string '' will be converted to null for storage (AWS.DynamoDB) purpose 18 | */ 19 | export const createUser = ({ 20 | userId = null, 21 | username = null, 22 | email = null, 23 | }={}) => ({ 24 | // Cognito Identity Id 25 | userId: userId, 26 | username: username, 27 | email: email, 28 | 29 | // osf access information 30 | osfAccess: [], 31 | 32 | // diy access information 33 | diyAccess: [], 34 | }) 35 | 36 | /** 37 | * Experiment State Template 38 | * @namespace ExperimentState 39 | * @property {guiValue} experimentName=null - Experiment Name 40 | * @property {guiValue} experimentId=null - Experiment's identifier in DynamoDB, should be generated as a UUID by uuid() 41 | * @property {guiValue} ownerId - Experiment Owner's identity id (see docs for AWS.Cognito) 42 | * @property {boolean} isPublic - True if the experiment is public 43 | * @property {number} createDate - The date the experiment is created 44 | * @property {number} lastModifiedDate - The date that the last edit happens to the experiment 45 | * @property {guiValue} previewId=null - The id of the node that is getting previewed 46 | * @property {Array.} mainTimeline=[] - The main jsPsych timeline, should hold the id of nodes 47 | * @property {Object} jsPsychInit - The object that sets jsPsych (initialization/launch) options 48 | * @property {} 49 | * @property {Object} [timelineNode-{id}] - {@link TimelineNode} 50 | * @property {Object} [trialNode-{id}] - {@link TrialNode} 51 | * @description State template for Experiment state. 52 | * ***NOTE THAT***: All empty string '' will be converted to null for storage (AWS.DynamoDB) purpose 53 | */ 54 | export const createExperiment = ({ 55 | experimentName = "Untitled Experiment", 56 | ownerId = null, 57 | isPublic = false, 58 | experimentId = generateExperimentId() 59 | }={}) => ({ 60 | previewId: null, 61 | 62 | experimentName: experimentName, 63 | experimentId: null, 64 | description: null, 65 | 66 | createDate: null, 67 | lastModifiedDate: null, 68 | ownerId: ownerId, 69 | isPublic: isPublic, 70 | 71 | /********** experiment contents **********/ 72 | mainTimeline: [], 73 | jsPsychInit: utils.deepCopy(jsPsychInitState), 74 | 75 | /********** Deployment Information **********/ 76 | cloudDeployInfo: getDefaultInitCloudDeployInfo(), 77 | diyDeployInfo: getDefaultInitDiyDeployInfo(), 78 | }); 79 | 80 | /************************ Utility Functions for UserState ************************/ 81 | 82 | /* 83 | * @typeof {object} OsfAccessItem - Hold osf access info 84 | * @property {guiValue} token=null - Access key to OSF 85 | * @property {guiValue} alias=null - Name of this item 86 | */ 87 | export const createUserOsfAccessItem = ({token=null, alias=null}={}) => ({ 88 | token, 89 | alias, 90 | }); 91 | 92 | 93 | /************************ Utility Functions for experimentState ************************/ 94 | 95 | /* 96 | * @typeof {object} CloudDeployItem - Hold information for cloud deploy information 97 | * @property {guiValue} osfNode=null - The osf node that stores data 98 | * @property {OsfAccessItem} osfAccess=null - osf access item 99 | * @property {number} saveAfter=0 - Save data to osf after the timeline node at this index 100 | */ 101 | export const getDefaultInitCloudDeployInfo = () => ({ 102 | osfNode: null, 103 | osfAccess: null, 104 | saveAfter: 0 105 | }); 106 | 107 | /* 108 | * @typeof {object} OsfAccessItem - Hold osf access info 109 | * @property {guiValue} token=null - Access key to OSF 110 | * @property {guiValue} alias=null - Name of this item 111 | */ 112 | export const getDefaultInitDiyDeployInfo = () => ({ 113 | mode: enums.DIY_Deploy_Mode.disk, 114 | saveAfter: 0, 115 | }); 116 | 117 | export const getInitExperimentState = () => createExperiment({experimentId: null}); 118 | 119 | export const generateExperimentId = () => `E_${utils.getUUID()}`; 120 | 121 | /* 122 | * Duplicate an experiment 123 | * Assign it a new experiment id (UUID) if necessary 124 | * Clear its time stamps 125 | */ 126 | export const duplicateExperiment = ({sourceExperimentState, newName=null}) => { 127 | let targetExperimentState = utils.deepCopy(sourceExperimentState); 128 | 129 | if (newName) { 130 | targetExperimentState.experimentName = newName; 131 | } 132 | targetExperimentState.experimentId = generateExperimentId(); 133 | targetExperimentState.createDate = null; 134 | targetExperimentState.lastModifiedDate = null; 135 | 136 | return targetExperimentState; 137 | }; 138 | 139 | /* 140 | * Prepare experimentState for being saved to AWS 141 | * Associate it with a user if necessary 142 | * Assign it an experiment id (UUID) if necessary 143 | * Stamp create date and last modified date if necessary 144 | */ 145 | export const registerExperiment = ({experimentState, userId=null}) => { 146 | experimentState = utils.deepCopy(experimentState); 147 | if (!experimentState.ownerId) { 148 | experimentState.ownerId = userId; 149 | } 150 | if (!experimentState.experimentId) { 151 | experimentState.experimentId = generateExperimentId(); 152 | } 153 | 154 | let now = Date.now(); 155 | if (!experimentState.createDate) { 156 | experimentState.createDate = now; 157 | } 158 | experimentState.lastModifiedDate = now; 159 | 160 | return experimentState; 161 | }; 162 | -------------------------------------------------------------------------------- /src/common/constants/enumerators.js: -------------------------------------------------------------------------------- 1 | export const DIY_Deploy_Mode = { 2 | disk: 'save_to_disk_as_csv', 3 | sqlite: 'save_to_sqlite', 4 | mysql: 'save_to_mysql' 5 | } 6 | 7 | export const Notify_Type = { 8 | success: "success", 9 | warning: "warning", 10 | error: "error", 11 | confirm: "confirm" 12 | } 13 | 14 | export const AUTH_MODES = { 15 | signIn: 'signIn', 16 | register: 'register', 17 | verification: 'verification', 18 | forgotPassword: 'forgotPassword' 19 | } 20 | 21 | export const MediaManagerMode = { 22 | upload: 'Upload', 23 | select: 'Select', 24 | multiSelect: 'multi-Select' 25 | } 26 | 27 | /** 28 | * @typeof {string} ParameterModeEnum 29 | * @description Indicate which value (native value, function or timeline variable) should be used 30 | * @readonly 31 | * @enum {string} 32 | */ 33 | export const ParameterMode = { 34 | /** The value that indicates deployment function should interpret the value as function when generating the code */ 35 | USE_FUNC: 'USE_FUNC', 36 | /** The value that indicates deployment function should interpret the value as timeline variable when generating the code */ 37 | USE_TV: "USE_TIMELINE_VARIABLE", 38 | /** The value that indicates deployment function should interpret the value as native javascript value when generating the code */ 39 | USE_VAL: "USE_VALUE" 40 | } 41 | 42 | /** 43 | * @typeof {string} GuiIgnoredInforEnum 44 | * @readonly 45 | * @enum {string} 46 | * @description The object that holds enumerators for information that will not be evaluated when generating deployment code 47 | */ 48 | export const TimelineVariableInputType = { 49 | // string 50 | TEXT: 'String', 51 | NUMBER: 'Number', 52 | LONG_TEXT: 'HTML/Long String', 53 | // string, but use special editor 54 | MEDIA: 'Media Resources', 55 | OBJECT: 'Object', 56 | ARRAY: 'Array', 57 | FUNCTION: 'Function' 58 | } 59 | 60 | /** 61 | * @enum {string} 62 | * @constant 63 | * @default 64 | */ 65 | export const jsPsych_Display_Element = "jsPsych-Window"; -------------------------------------------------------------------------------- /src/common/constants/theme.js: -------------------------------------------------------------------------------- 1 | const colors = { 2 | primary: '#24B24C', // green 3 | primaryDeep: '#04673A', // deep green 4 | secondary: '#FF9800', // orange 5 | background: '#F0F0F0', // grey 6 | secondaryDeep: '#FF5722', 7 | secondaryLight: '#FFB74D', 8 | font: 'white', 9 | errorRed: '#F34335', 10 | defaultFontColor: '#424242', 11 | }; 12 | 13 | const Icon = { 14 | color: colors.primary, 15 | hoverColor: colors.secondary 16 | }; 17 | 18 | const TextFieldFocusStyle = (error = false) => { 19 | var res = { 20 | floatingLabelFocusStyle: { 21 | color: error ? colors.errorRed : colors.secondary 22 | }, 23 | underlineFocusStyle: { 24 | borderColor: error ? colors.errorRed : colors.secondary 25 | } 26 | } 27 | 28 | if (error) { 29 | res.floatingLabelStyle = { 30 | color: colors.errorRed 31 | } 32 | } 33 | 34 | return res; 35 | }; 36 | 37 | export { 38 | colors, 39 | Icon, 40 | TextFieldFocusStyle 41 | }; -------------------------------------------------------------------------------- /src/common/containers/AppContainer.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import App from '../components/App'; 3 | 4 | 5 | const mapStateToProps = (state, ownProps) => { 6 | let experimentState = state.experimentState; 7 | 8 | // let shouldOrganizerStayOpen = !!experimentState.previewId || experimentState.mainTimeline.length > 0; 9 | let shouldEditorStayOpen = !!experimentState.previewId; 10 | return { 11 | shouldOrganizerStayOpen: true, 12 | shouldEditorStayOpen: shouldEditorStayOpen 13 | } 14 | }; 15 | 16 | const mapDispatchToProps = (dispatch, ownProps) => ({ 17 | 18 | }) 19 | 20 | export default connect(mapStateToProps, mapDispatchToProps)(App); -------------------------------------------------------------------------------- /src/common/containers/Appbar/AppbarContainer.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import Appbar from '../../components/Appbar'; 3 | 4 | import * as experimentSettingActions from '../../actions/experimentSettingActions'; 5 | 6 | 7 | const changeExperimentName = (dispatch, text) => { 8 | text = utils.toNull(text); 9 | dispatch(experimentSettingActions.setExperimentNameAction(text)); 10 | } 11 | 12 | const clickSave = ({dispatch}) => { 13 | return dispatch((dispatch, getState) => { 14 | return utils.commonFlows.isUserSignedIn().then((signedIn) => { 15 | if (!signedIn) { 16 | utils.notifications.notifyWarningBySnackbar({ 17 | dispatch, 18 | message: 'You need to sign in before saving your work !' 19 | }); 20 | } else { 21 | if (utils.commonFlows.hasExperimentChanged(getState().experimentState)) { 22 | return utils.commonFlows.saveCurrentExperiment({ dispatch }); 23 | } else { 24 | utils.notifications.notifyWarningBySnackbar({ 25 | dispatch, 26 | message: 'Nothing has changed since last save !' 27 | }); 28 | } 29 | } 30 | 31 | return Promise.resolve(); 32 | }).catch((err) => { 33 | console.log(err); 34 | utils.notifications.notifyErrorByDialog({ 35 | dispatch, 36 | message: err.message 37 | }); 38 | }); 39 | }); 40 | } 41 | 42 | const clickNewExperiment = ({dispatch}) => { 43 | let loadNewExperiment = () => { 44 | dispatch((dispatch, getState) => { 45 | let newExperiment = core.getInitExperimentState(); 46 | dispatch(actions.actionCreator({ 47 | type: actions.ActionTypes.LOAD_EXPERIMENT, 48 | experimentState: core.registerExperiment({ 49 | experimentState: newExperiment, 50 | userId: getState().userState.userId 51 | }), 52 | })); 53 | }); 54 | } 55 | 56 | return dispatch((dispatch, getState) => { 57 | let { experimentState } = getState(); 58 | if (utils.commonFlows.hasExperimentChanged(getState().experimentState)) { 59 | utils.notifications.popUpConfirmation({ 60 | dispatch: dispatch, 61 | message: "Do you want to save the changes before creating a new experiment?", 62 | continueWithOperation: () => { 63 | return utils.commonFlows.saveCurrentExperiment({dispatch}).then(loadNewExperiment); 64 | }, 65 | continueWithoutOperation: loadNewExperiment, 66 | continueWithOperationLabel: "Yes (Continue with saving)", 67 | continueWithoutOperationLabel: "No (Continue without saving)", 68 | showCancelButton: true, 69 | withExtraCare: true, 70 | extraCareText: experimentState.experimentId ? experimentState.experimentId : "Yes, I know what I am doing." 71 | }); 72 | } else { 73 | loadNewExperiment(); 74 | } 75 | }); 76 | } 77 | 78 | const clickSaveAs = ({dispatch, newName}) => { 79 | return dispatch((dispatch, getState) => { 80 | let sourceExperimentState = getState().experimentState; 81 | return utils.commonFlows.duplicateExperiment({ 82 | dispatch, 83 | sourceExperimentState, 84 | newName 85 | }); 86 | }); 87 | } 88 | 89 | const mapStateToProps = (state, ownProps) => { 90 | return { 91 | experimentName: utils.toEmptyString(state.experimentState.experimentName), 92 | } 93 | }; 94 | 95 | const mapDispatchToProps = (dispatch, ownProps) => ({ 96 | dispatch, 97 | changeExperimentName: (text) => { changeExperimentName(dispatch, text); }, 98 | clickSaveAs: ({newName}) => clickSaveAs({dispatch, newName}), 99 | clickSave: () => clickSave({dispatch}), 100 | clickNewExperiment: () => clickNewExperiment({dispatch}) 101 | }) 102 | 103 | export default connect(mapStateToProps, mapDispatchToProps)(Appbar); -------------------------------------------------------------------------------- /src/common/containers/Appbar/CloudDeploymentManager/index.js: -------------------------------------------------------------------------------- 1 | import CloudDeploymentManagerContainer from './CloudDeploymentManagerContainer.js'; 2 | 3 | export default CloudDeploymentManagerContainer; -------------------------------------------------------------------------------- /src/common/containers/Appbar/DIYDeploymentManager/DIYDeploymentManagerContainer.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import * as experimentActions from '../../../actions/experimentSettingActions'; 3 | import DIYDeploymentManager from '../../../components/Appbar/DIYDeploymentManager'; 4 | 5 | import { diyDeploy as $diyDeploy } from '../../../backend/deploy'; 6 | 7 | const diyDeploy = ({dispatch, progressHook, ...diyDeployInfo}) => { 8 | return dispatch((dispatch, getState) => { 9 | dispatch(experimentActions.setDIYDeployInfoAction({ 10 | ...diyDeployInfo 11 | })); 12 | 13 | return utils.commonFlows.isUserSignedIn().then((signedIn) => { 14 | if (signedIn) { 15 | let experimentState = getState().experimentState, 16 | Prefix = [experimentState.ownerId, experimentState.experimentId].join(myaws.S3.Delimiter); 17 | 18 | return utils.commonFlows.saveCurrentExperiment({ 19 | dispatch, 20 | displayNotification: false 21 | }).then(() => { 22 | return myaws.S3.listBucketContents({ Prefix }).then((media) => { 23 | $diyDeploy({ 24 | state: getState(), 25 | progressHook: progressHook, 26 | media 27 | }); 28 | }); 29 | }); 30 | } else { 31 | $diyDeploy({ 32 | state: getState(), 33 | progressHook: progressHook, 34 | }); 35 | } 36 | }); 37 | }); 38 | } 39 | 40 | const mapStateToProps = (state, ownProps) => { 41 | let experimentState = state.experimentState, 42 | indexedNodeNames = experimentState.mainTimeline.map((id, i) => `${i+1}. ${experimentState[id].name}`); 43 | 44 | return { 45 | indexedNodeNames: indexedNodeNames, 46 | ...experimentState.diyDeployInfo 47 | } 48 | }; 49 | 50 | const mapDispatchToProps = (dispatch, ownProps) => ({ 51 | diyDeploy: ({ 52 | progressHook, 53 | ...diyDeployInfo 54 | }) => diyDeploy({ 55 | dispatch: dispatch, 56 | progressHook: progressHook, 57 | ...diyDeployInfo 58 | }), 59 | 60 | }) 61 | 62 | export default connect(mapStateToProps, mapDispatchToProps)(DIYDeploymentManager); -------------------------------------------------------------------------------- /src/common/containers/Appbar/DIYDeploymentManager/index.js: -------------------------------------------------------------------------------- 1 | import DIYDeploymentManagerContainer from './DIYDeploymentManagerContainer.js'; 2 | 3 | export default DIYDeploymentManagerContainer; -------------------------------------------------------------------------------- /src/common/containers/Appbar/UserMenu/ExperimentList/ExperimentListContainer.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import ExperimentList from '../../../../components/Appbar/UserMenu/ExperimentList'; 3 | 4 | import { pureCloudDelete as cloudDelete } from '../../CloudDeploymentManager/CloudDeploymentManagerContainer.js'; 5 | 6 | 7 | const pullExperiment = ({dispatch, targetExperimentId, saveFirst=false}) => { 8 | let $save = () => { 9 | if (saveFirst) { 10 | return utils.commonFlows.saveCurrentExperiment({dispatch}); 11 | } else { 12 | return Promise.resolve(); 13 | } 14 | } 15 | return $save().then(() => { 16 | return myaws.DynamoDB.getExperimentById(targetExperimentId).then((data) => { 17 | utils.commonFlows.loadExperimentToLocal({ 18 | dispatch, 19 | experimentState: data.Item.fetch 20 | }); 21 | utils.notifications.notifySuccessBySnackbar({ 22 | dispatch, 23 | message: "Opened !" 24 | }); 25 | return Promise.resolve(); 26 | }); 27 | }); 28 | } 29 | 30 | const deleteExperiment = ({dispatch, targetExperimentState}) => { 31 | let { experimentId, ownerId } = targetExperimentState; 32 | return myaws.S3.listBucketContents({ 33 | Prefix: `${ownerId}/${experimentId}/` 34 | }).then((data) => { 35 | let filePaths = []; 36 | if (data && data.Contents) { 37 | filePaths = data.Contents.map((f) => (f.Key)); 38 | } 39 | return Promise.all([ 40 | myaws.S3.deleteFiles({filePaths}), 41 | myaws.DynamoDB.deleteExperiment(experimentId), 42 | cloudDelete(experimentId) 43 | ]).then(() => { 44 | utils.notifications.notifySuccessBySnackbar({ 45 | dispatch, 46 | message: "Deleted !" 47 | }); 48 | return Promise.resolve(); 49 | }).catch((err) => { 50 | console.log(err); 51 | utils.notifications.notifyErrorByDialog({ 52 | dispatch, 53 | message: err.message 54 | }); 55 | return Promise.reject(err); 56 | }); 57 | }); 58 | } 59 | 60 | const mapStateToProps = (state, ownProps) => { 61 | return { 62 | userId: state.userState.userId, 63 | currentExperimentId: state.experimentState.experimentId, 64 | currentExperimentState: state.experimentState 65 | }; 66 | }; 67 | 68 | const mapDispatchToProps = (dispatch, ownProps) => ({ 69 | pullExperiment: ({...args}) => pullExperiment({dispatch, ...args}), 70 | deleteExperiment: ({...args}) => deleteExperiment({dispatch, ...args}), 71 | dispatch 72 | }) 73 | 74 | export default connect(mapStateToProps, mapDispatchToProps)(ExperimentList); 75 | -------------------------------------------------------------------------------- /src/common/containers/Appbar/UserMenu/ExperimentList/index.js: -------------------------------------------------------------------------------- 1 | import ExperimentListContainer from './ExperimentListContainer.js'; 2 | 3 | export default ExperimentListContainer; -------------------------------------------------------------------------------- /src/common/containers/Appbar/UserMenu/Profile/index.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import Profile from '../../../../components/Appbar/UserMenu/Profile'; 3 | import * as userActions from '../../../../actions/userActions'; 4 | 5 | 6 | const setOsfAccess = (dispatch, osfAccess, setReactState) => { 7 | return dispatch((dispatch, getState) => { 8 | dispatch(userActions.setOsfAccessAction(osfAccess)); 9 | return myaws.DynamoDB.pushUserData(getState().userState).then(() => { 10 | utils.notifications.notifySuccessBySnackbar({ 11 | dispatch, 12 | message: "Profile Updated !" 13 | }); 14 | }).catch((err) => { 15 | utils.notifications.notifyErrorByDialog({ 16 | dispatch, 17 | message: err.message 18 | }); 19 | }); 20 | }); 21 | } 22 | 23 | const mapStateToProps = (state, ownProps) => { 24 | let userState = state.userState; 25 | 26 | return { 27 | username: userState.username, 28 | osfAccess: userState.osfAccess || [], 29 | }; 30 | }; 31 | 32 | const mapDispatchToProps = (dispatch, ownProps) => ({ 33 | setOsfAccess: (osfAccess, setReactState) => setOsfAccess(dispatch, osfAccess, setReactState), 34 | notifyWarningBySnackbar: (message) => notifyWarningBySnackbar(dispatch, message), 35 | }) 36 | 37 | export default connect(mapStateToProps, mapDispatchToProps)(Profile); -------------------------------------------------------------------------------- /src/common/containers/Appbar/UserMenu/UserMenuContainer.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | 3 | import UserMenu from '../../../components/Appbar/UserMenu'; 4 | 5 | 6 | const handleSignOut = ({dispatch}) => { 7 | return utils.commonFlows.signOut(); 8 | } 9 | 10 | const mapStateToProps = (state, ownProps) => { 11 | let userState = state.userState; 12 | return { 13 | username: userState.username 14 | } 15 | } 16 | 17 | const mapDispatchToProps = (dispatch, ownProps) => ({ 18 | handleSignOut: () => handleSignOut({dispatch}), 19 | popSignUp: () => utils.loginWindows.popRegister({dispatch}), 20 | popSignIn: () => utils.loginWindows.popSignIn({dispatch}) 21 | }) 22 | 23 | export default connect(mapStateToProps, mapDispatchToProps)(UserMenu); 24 | -------------------------------------------------------------------------------- /src/common/containers/Appbar/UserMenu/index.js: -------------------------------------------------------------------------------- 1 | import UserMenuContainer from './UserMenuContainer.js'; 2 | 3 | export default UserMenuContainer; 4 | 5 | -------------------------------------------------------------------------------- /src/common/containers/Appbar/index.js: -------------------------------------------------------------------------------- 1 | import AppbarContainer from './AppbarContainer.js'; 2 | 3 | export default AppbarContainer; -------------------------------------------------------------------------------- /src/common/containers/Appbar/jsPsychInitEditor/index.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import * as experimentSettingActions from '../../../actions/experimentSettingActions'; 3 | 4 | import jsPsychInitEditor from '../../../components/Appbar/jsPsychInitEditor'; 5 | 6 | const setJsPsychInit = (dispatch, key, value) => { 7 | value = utils.toNull(value); 8 | dispatch(experimentSettingActions.setJspyschInitAction(key, value)); 9 | } 10 | 11 | const mapStateToProps = (state, ownProps) => { 12 | let jsPsychInit = state.experimentState.jsPsychInit; 13 | 14 | return { 15 | ...jsPsychInit, 16 | min_width: jsPsychInit.exclusions.min_width, 17 | min_height: jsPsychInit.exclusions.min_height, 18 | audio: jsPsychInit.exclusions.audio, 19 | }; 20 | }; 21 | 22 | const mapDispatchToProps = (dispatch, ownProps) => ({ 23 | setJsPsychInit: (e, value, key) => { setJsPsychInit(dispatch, key, value); }, 24 | }) 25 | 26 | export default connect(mapStateToProps, mapDispatchToProps)(jsPsychInitEditor); 27 | -------------------------------------------------------------------------------- /src/common/containers/ArrayEditor/index.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import ArrayEditor from '../../components/ArrayEditor'; 3 | 4 | const mapStateToProps = (state, ownProps) => { 5 | return { 6 | }; 7 | } 8 | 9 | const mapDispatchToProps = (dispatch, ownProps) => ({ 10 | notifySuccess: (message) => { utils.notifications.notifySuccessBySnackbar({dispatch, message}); }, 11 | notifyError: (message) => { utils.notifications.notifyErrorBySnackbar({dispatch, message}); } 12 | }) 13 | 14 | export default connect(mapStateToProps, mapDispatchToProps)(ArrayEditor); 15 | 16 | -------------------------------------------------------------------------------- /src/common/containers/Authentications/AuthenticationsContainer.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import Authentications from '../../components/Authentications'; 3 | 4 | 5 | const signIn = ({dispatch, username, password, firstSignIn=false}) => { 6 | return dispatch((dispatch, getState) => { 7 | return myaws.Auth.signIn({ 8 | username, 9 | password 10 | }).then((userInfo) => { 11 | // if it is first time signing in 12 | // register user in DynamoDB 13 | if (firstSignIn) { 14 | let { userId, username, email } = userInfo; 15 | 16 | return myaws.DynamoDB.saveUserData(core.createUser({ 17 | userId, 18 | username, 19 | email 20 | })); 21 | } 22 | 23 | return Promise.resolve(); 24 | }).then(() => { 25 | // if user has changed anything 26 | // save the change 27 | 28 | if (utils.commonFlows.hasExperimentChanged(getState().experimentState)) { 29 | return utils.commonFlows.saveCurrentExperiment({dispatch}); 30 | } 31 | 32 | return Promise.resolve(); 33 | }).then(() => { 34 | return utils.commonFlows.load({dispatch}); 35 | }).catch((err) => { 36 | if (err.code === "UserNotConfirmedException") { 37 | popVerification({ 38 | dispatch, 39 | signInCallback: () => { 40 | return signIn({ 41 | dispatch, 42 | username, 43 | password, 44 | firstSignIn 45 | }); 46 | } 47 | }); 48 | } else { 49 | throw err; 50 | } 51 | }); 52 | }); 53 | } 54 | 55 | const signUp = ({dispatch, username, password, attributes, ...options}) => { 56 | return myaws.Auth.signUp({username, password, attributes, ...options}).then(() => { 57 | popVerification({ 58 | dispatch, 59 | signInCallback: () => { 60 | return signIn({ 61 | dispatch, 62 | username, 63 | password, 64 | firstSignIn: true, 65 | }) 66 | } 67 | }); 68 | }); 69 | } 70 | 71 | const handleWindowClose = ({dispatch}) => { 72 | dispatch(actions.actionCreator({ 73 | type: actions.ActionTypes.SET_AUTH_WINDOW, 74 | open: false 75 | })); 76 | } 77 | 78 | export const popSignIn = ({dispatch}) => { 79 | dispatch(actions.actionCreator({ 80 | type: actions.ActionTypes.SET_AUTH_WINDOW, 81 | open: true, 82 | loginMode: enums.AUTH_MODES.signIn 83 | })); 84 | } 85 | 86 | export const popRegister = ({dispatch}) => { 87 | dispatch(actions.actionCreator({ 88 | type: actions.ActionTypes.SET_AUTH_WINDOW, 89 | open: true, 90 | loginMode: enums.AUTH_MODES.register 91 | })); 92 | } 93 | 94 | export const popForgetPassword = ({dispatch}) => { 95 | dispatch(actions.actionCreator({ 96 | type: actions.ActionTypes.SET_AUTH_WINDOW, 97 | open: true, 98 | loginMode: enums.AUTH_MODES.forgotPassword 99 | })); 100 | } 101 | 102 | export const popVerification = ({dispatch, ...args}) => { 103 | dispatch(actions.actionCreator({ 104 | type: actions.ActionTypes.SET_AUTH_WINDOW, 105 | open: true, 106 | loginMode: enums.AUTH_MODES.verification, 107 | ...args 108 | })); 109 | } 110 | 111 | const setLoginMode = ({dispatch, mode}) => { 112 | dispatch(actions.actionCreator({ 113 | type: actions.ActionTypes.SET_AUTH_WINDOW, 114 | loginMode: mode 115 | })); 116 | } 117 | 118 | const mapStateToProps = (state, ownProps) => { 119 | return { 120 | ...state.authentications 121 | } 122 | } 123 | 124 | const mapDispatchToProps = (dispatch, ownProps) => ({ 125 | signIn: ({...args}) => signIn({dispatch, ...args}), 126 | signUp: ({...args}) => signUp({dispatch, ...args}), 127 | handleWindowClose: () => handleWindowClose({dispatch}), 128 | popSignIn: () => popSignIn({dispatch}), 129 | popRegister: () => popRegister({dispatch}), 130 | popForgetPassword: () => popForgetPassword({dispatch}), 131 | popVerification: () => popVerification({dispatch}), 132 | setLoginMode: (mode) => setLoginMode({dispatch, mode}), 133 | }) 134 | 135 | export default connect(mapStateToProps, mapDispatchToProps)(Authentications); 136 | -------------------------------------------------------------------------------- /src/common/containers/Authentications/index.js: -------------------------------------------------------------------------------- 1 | import Authentications from './AuthenticationsContainer'; 2 | 3 | export default Authentications; -------------------------------------------------------------------------------- /src/common/containers/MediaManager/MediaManagerContainer.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import MediaManager from '../../components/MediaManager'; 3 | import * as editorActions from '../../actions/editorActions'; 4 | 5 | 6 | const Upload_Limit_MB = 100; 7 | const Upload_Limit = Upload_Limit_MB * 1024 * 1024; 8 | 9 | 10 | const uploadFiles = ({dispatch, files, progressHook, userId, experimentId}) => { 11 | let params = [] 12 | for (let f of files) { 13 | if (f.size > Upload_Limit) { 14 | utils.notifications.notifyWarningBySnackbar({ 15 | dispatch, 16 | message: "Exceed upload limit: " + Upload_Limit_MB + " MB !" 17 | }); 18 | return Promise.resolve(); 19 | } 20 | params.push(myaws.S3.generateUploadParam({ 21 | Key: [userId, experimentId, f.name].join(myaws.S3.Delimiter), 22 | Body: f 23 | })); 24 | } 25 | 26 | return myaws.S3.uploadFiles({params, progressHook}).then(() => { 27 | utils.notifications.notifySuccessBySnackbar({ 28 | dispatch, 29 | message: "Uploaded !" 30 | }); 31 | }); 32 | } 33 | 34 | const deleteFiles = ({dispatch, filePaths}) => { 35 | return myaws.S3.deleteFiles({filePaths}).then((data) => { 36 | utils.notifications.notifySuccessBySnackbar({ 37 | dispatch, 38 | message: "Deleted !" 39 | }); 40 | }).catch((err) => { 41 | utils.notifications.notifyErrorByDialog({ 42 | dispatch, 43 | message: err.message 44 | }); 45 | }); 46 | } 47 | 48 | const checkBeforeOpen = ({dispatch}) => { 49 | return dispatch((dispatch, getState) => { 50 | // not logged in 51 | if (!getState().userState.userId) { 52 | utils.notifications.notifyWarningBySnackbar({ 53 | dispatch, 54 | message: 'You need to sign in before uploading your resources !' 55 | }); 56 | utils.loginWindows.popSignIn({dispatch}); 57 | return Promise.resolve(false); 58 | } 59 | // unregistered experiment 60 | if (!getState().experimentState.experimentId) { 61 | utils.notifications.notifyWarningByDialog({ 62 | dispatch, 63 | message: 'You need to save this experiment before uploading your resources !' 64 | }); 65 | return Promise.resolve(false); 66 | } 67 | 68 | return Promise.resolve(true); 69 | }); 70 | } 71 | 72 | /* 73 | Note that FOR NOW AWS S3 Media Type Object MUST be in the first level 74 | of trial.paramters 75 | */ 76 | const mapStateToProps = (state, ownProps) => { 77 | return { 78 | userId: state.experimentState.ownerId, 79 | experimentId: state.experimentState.experimentId 80 | }; 81 | } 82 | 83 | const mapDispatchToProps = (dispatch, ownProps) => ({ 84 | uploadFiles: ({...args}) => uploadFiles({dispatch, ...args}), 85 | deleteFiles: ({...args}) => deleteFiles({dispatch, ...args}), 86 | checkBeforeOpen: () => checkBeforeOpen({dispatch}), 87 | notifySuccessBySnackbar: (message) => { utils.notifications.notifySuccessBySnackbar({dispatch, message}); }, 88 | notifyWarningByDialog: (message) => { utils.notifications.notifyWarningByDialog({dispatch, message}); }, 89 | notifyWarningBySnackbar: (message) => { utils.notifications.notifyWarningBySnackbar({dispatch, message}); } 90 | }) 91 | 92 | export default connect(mapStateToProps, mapDispatchToProps)(MediaManager); 93 | -------------------------------------------------------------------------------- /src/common/containers/MediaManager/index.js: -------------------------------------------------------------------------------- 1 | import MediaManagerContainer from './MediaManagerContainer.js'; 2 | 3 | export default MediaManagerContainer; -------------------------------------------------------------------------------- /src/common/containers/Notifications/NotificationsContainer.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import Notifications from '../../components/Notifications'; 3 | 4 | 5 | const handleDialogClose = ({dispatch}) => { 6 | dispatch(actions.actionCreator({ 7 | type: actions.ActionTypes.NOTIFY_DIALOG_CLOSE, 8 | })) 9 | } 10 | 11 | const handleSnackbarClose = ({dispatch}) => { 12 | dispatch(actions.actionCreator({ 13 | type: actions.ActionTypes.NOTIFY_SNACKBAR_CLOSE, 14 | })) 15 | } 16 | 17 | export const notifySuccessByDialog = ({dispatch, message}) => { 18 | dispatch(actions.actionCreator({ 19 | type: actions.ActionTypes.NOTIFY_SUCCESS_DIALOG, 20 | notifyType: enums.Notify_Type.success, 21 | message 22 | })) 23 | } 24 | 25 | export const notifyWarningByDialog = ({dispatch, message}) => { 26 | dispatch(actions.actionCreator({ 27 | type: actions.ActionTypes.NOTIFY_WARNING_DIALOG, 28 | notifyType: enums.Notify_Type.warning, 29 | message 30 | })) 31 | } 32 | 33 | export const notifyErrorByDialog = ({dispatch, message}) => { 34 | dispatch(actions.actionCreator({ 35 | type: actions.ActionTypes.NOTIFY_ERROR_DIALOG, 36 | notifyType: enums.Notify_Type.error, 37 | message 38 | })) 39 | } 40 | 41 | export const notifySuccessBySnackbar = ({dispatch, message}) => { 42 | dispatch(actions.actionCreator({ 43 | type: actions.ActionTypes.NOTIFY_SUCCESS_SNACKBAR, 44 | notifyType: enums.Notify_Type.success, 45 | message 46 | })); 47 | } 48 | 49 | export const notifyWarningBySnackbar = ({dispatch, message}) => { 50 | dispatch(actions.actionCreator({ 51 | type: actions.ActionTypes.NOTIFY_WARNING_SNACKBAR, 52 | notifyType: enums.Notify_Type.warning, 53 | message 54 | })); 55 | } 56 | 57 | export const notifyErrorBySnackbar = ({dispatch, message}) => { 58 | dispatch(actions.actionCreator({ 59 | type: actions.ActionTypes.NOTIFY_ERROR_SNACKBAR, 60 | notifyType: enums.Notify_Type.error, 61 | message 62 | })); 63 | } 64 | 65 | export const popUpConfirmation = (args = { 66 | dispatch, 67 | message: "", 68 | continueWithOperation: () => Promise.resolve(), 69 | continueWithoutOperation: () => Promise.resolve(), 70 | continueWithOperationLabel: "Yes", 71 | continueWithoutOperationLabel: "No", 72 | showCancelButton: true, 73 | withExtraCare: false, 74 | extraCareText: "Yes, I know what I am doing." 75 | }) => { 76 | let { dispatch } = args; 77 | dispatch(actions.actionCreator({ 78 | type: actions.ActionTypes.POP_UP_CONFIRM, 79 | notifyType: enums.Notify_Type.confirm, 80 | ...args 81 | })); 82 | } 83 | 84 | const mapStateToProps = (state, ownProps) => { 85 | return { 86 | ...state.notifications 87 | }; 88 | } 89 | 90 | const mapDispatchToProps = (dispatch, ownProps) => ({ 91 | handleDialogClose: () => { return handleDialogClose({dispatch,}) }, 92 | handleSnackbarClose: () => { return handleSnackbarClose({dispatch,}) }, 93 | }) 94 | 95 | export default connect(mapStateToProps, mapDispatchToProps)(Notifications); -------------------------------------------------------------------------------- /src/common/containers/Notifications/index.js: -------------------------------------------------------------------------------- 1 | import NotificationsContainer from './NotificationsContainer.js'; 2 | 3 | export default NotificationsContainer; -------------------------------------------------------------------------------- /src/common/containers/ObjectEditor/index.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import ObjectEditor from '../../components/ObjectEditor'; 3 | 4 | 5 | const mapStateToProps = (state, ownProps) => { 6 | return { 7 | }; 8 | } 9 | 10 | const mapDispatchToProps = (dispatch, ownProps) => ({ 11 | notifySuccess: (message) => { utils.notifications.notifySuccessBySnackbar({dispatch, message}); }, 12 | notifyError: (message) => { utils.notifications.notifyErrorBySnackbar({dispatch, message}); } 13 | }) 14 | 15 | export default connect(mapStateToProps, mapDispatchToProps)(ObjectEditor); 16 | 17 | -------------------------------------------------------------------------------- /src/common/containers/PreviewWindow/index.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | // import * as experimentSettingActions from '../../actions/experimentSettingActions'; 3 | import PreviewWindow from '../../components/PreviewWindow'; 4 | import { generateCode } from '../../backend/deploy'; 5 | import { isTimeline, isTrial } from '../../reducers/Experiment/utils'; 6 | 7 | let code = ""; 8 | 9 | const playAll = (dispatch, load) => { 10 | dispatch((dispatch, getState) => { 11 | code = generateCode({ 12 | state: getState().experimentState, 13 | all: true, 14 | deploy: false 15 | }); 16 | load(code); 17 | }) 18 | } 19 | 20 | const hotUpdate = (dispatch, load) => { 21 | dispatch((dispatch, getState) => { 22 | code = generateCode({ 23 | state: getState().experimentState, 24 | all: false, 25 | deploy: false 26 | }); 27 | load(code); 28 | }) 29 | } 30 | 31 | const mapStateToProps = (state, ownProps) => { 32 | let obj = Object.assign({}, utils.deepCopy(state.experimentState), { experimentName: null }); //ignore experiment name change 33 | for (let key of Object.keys(obj)) { 34 | if (obj[key] && (isTimeline(obj[key]) || isTrial(obj[key]))) { 35 | obj[key] = Object.assign({}, obj[key], { name: null }); //ignore name change 36 | } 37 | } 38 | 39 | return { 40 | state: obj 41 | }; 42 | } 43 | 44 | const mapDispatchToProps = (dispatch, ownProps) => ({ 45 | playAll: (load) => { playAll(dispatch, load); }, 46 | hotUpdate: (load) => { hotUpdate(dispatch, load); } 47 | }) 48 | 49 | export default connect(mapStateToProps, mapDispatchToProps)(PreviewWindow); 50 | 51 | -------------------------------------------------------------------------------- /src/common/containers/TimelineNodeEditor/TimelineForm/TimelineFormContainer.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import TimelineForm from '../../../components/TimelineNodeEditor/TimelineForm'; 3 | import * as editorActions from '../../../actions/editorActions'; 4 | 5 | const setRandomize = (dispatch, flag) => { 6 | dispatch(editorActions.setRandomizeAction(flag)); 7 | } 8 | 9 | const setRepetitions = (dispatch,newVal) => { 10 | dispatch(editorActions.setRepetitionsAction(newVal)); 11 | } 12 | 13 | const setSampling = (dispatch, key, newVal) => { 14 | dispatch(editorActions.setSamplingMethodAction(key, newVal)); 15 | } 16 | 17 | const setSampleSize = (dispatch, newVal) => { 18 | dispatch(editorActions.setSampleSizeAction(newVal)); 19 | } 20 | 21 | const setLoopFunction = (dispatch, newVal) => { 22 | dispatch(editorActions.setLoopFunctionAction(newVal)); 23 | } 24 | 25 | const setConditionFunction = (dispatch, newVal) => { 26 | dispatch(editorActions.setConditionFunctionAction(newVal)); 27 | } 28 | 29 | const mapStateToProps = (state, ownProps) => { 30 | let experimentState = state.experimentState; 31 | 32 | let timeline = experimentState[experimentState.previewId]; 33 | return { 34 | id: timeline.id, 35 | randomize: timeline.parameters.randomize_order, 36 | repetitions: timeline.parameters.repetitions, 37 | samplingType: timeline.parameters.sample.type, 38 | samplingSize: timeline.parameters.sample.size, 39 | loopFunction: timeline.parameters.loop_function, 40 | conditionalFunction: timeline.parameters.conditional_function 41 | } 42 | }; 43 | 44 | const mapDispatchToProps = (dispatch,ownProps) => ({ 45 | setRandomize: (flag) => { setRandomize(dispatch, flag); }, 46 | setRepetitions: (e, newVal) => { setRepetitions(dispatch, newVal) }, 47 | setSampling: (e, key, newVal) => { setSampling(dispatch, key, newVal) }, 48 | setSampleSize: (newVal) => { setSampleSize(dispatch, newVal) }, 49 | setLoopFunction: (newVal) => { setLoopFunction(dispatch, newVal) }, 50 | setConditionFunction: (newVal) => { setConditionFunction(dispatch, newVal) } 51 | }) 52 | 53 | export default connect(mapStateToProps, mapDispatchToProps)(TimelineForm); -------------------------------------------------------------------------------- /src/common/containers/TimelineNodeEditor/TimelineForm/TimelineVariableTableContainer.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import TimelineVariableTable from '../../../components/TimelineNodeEditor/TimelineForm/TimelineVariableTable'; 3 | import * as editorActions from '../../../actions/editorActions'; 4 | import { GuiIgonoredInfoEnum } from '../../../reducers/Experiment/editor'; 5 | 6 | 7 | const updateTimelineVariableName = (dispatch, oldName, newName) => { 8 | dispatch(editorActions.updateTimelineVariableNameAction(oldName, newName)); 9 | } 10 | 11 | const addRow = (dispatch, index) => { 12 | dispatch(editorActions.addTimelineVariableRowAction(index)); 13 | } 14 | 15 | const addColumn = (dispatch) => { 16 | dispatch(editorActions.addTimelineVariableColumnAction()); 17 | } 18 | 19 | const deleteRow = (dispatch, index) => { 20 | dispatch(editorActions.deleteTimelineVariableRowAction(index)); 21 | } 22 | 23 | const deleteColumn = (dispatch, index) => { 24 | dispatch(editorActions.deleteTimelineVariableColumnAction(index)); 25 | } 26 | 27 | const setTable = (dispatch, table) => { 28 | dispatch(editorActions.setTimelineVariableAction(table)); 29 | } 30 | 31 | const updateTimelineVariableInputType = (dispatch, name, inputType, typeCoercion) => { 32 | dispatch(editorActions.updateTimelineVariableInputTypeAction(name, inputType, typeCoercion)) 33 | } 34 | 35 | const updateCell = (dispatch, colName, rowNum, valueObject) => { 36 | dispatch(editorActions.updateCellAction(colName, rowNum, valueObject)) 37 | } 38 | 39 | const moveTo = (dispatch, sourceIndex, targetIndex) => { 40 | dispatch(editorActions.moveRowToAction(sourceIndex, targetIndex)) 41 | } 42 | 43 | const mapStateToProps = (state, ownProps) => { 44 | let experimentState = state.experimentState, 45 | timeline = experimentState[experimentState.previewId], 46 | parameters = timeline.parameters; 47 | return { 48 | id: timeline.id, 49 | // the whole gui info 50 | parameters: parameters, 51 | // the table 52 | table: timeline.parameters.timeline_variables || [], 53 | // input type of each header 54 | inputType: parameters[GuiIgonoredInfoEnum.root] && parameters[GuiIgonoredInfoEnum.root][GuiIgonoredInfoEnum.TVHeaderInputType], 55 | headers: parameters[GuiIgonoredInfoEnum.root] && parameters[GuiIgonoredInfoEnum.root][GuiIgonoredInfoEnum.TVHeaderOrder], 56 | rowIds: parameters[GuiIgonoredInfoEnum.root] && parameters[GuiIgonoredInfoEnum.root][GuiIgonoredInfoEnum.TVRowIds], 57 | } 58 | }; 59 | 60 | const mapDispatchToProps = (dispatch, ownProps) => ({ 61 | updateTimelineVariableInputType: (name, inputType, typeCoercion) => { updateTimelineVariableInputType(dispatch, name, inputType, typeCoercion); }, 62 | updateCell: (colName, rowNum, valueObject) => { updateCell(dispatch, colName, rowNum, valueObject); }, 63 | updateTimelineVariableName: (oldName, newName) => { updateTimelineVariableName(dispatch, oldName, newName); }, 64 | addRow: (index=-1) => { addRow(dispatch, index); }, 65 | addColumn: () => { addColumn(dispatch); }, 66 | deleteRow: (index) => { deleteRow(dispatch, index); }, 67 | deleteColumn: (index) => { deleteColumn(dispatch, index); }, 68 | setTable: (table) => { setTable(dispatch, table); }, 69 | notifyConfirm: (message, continueWithOperation) => { utils.notifications.popUpConfirmation({dispatch, message, continueWithOperation}); }, 70 | notifyError: (message) => { utils.notifications.notifyErrorByDialog({dispatch, message}); }, 71 | notifyWarningBySnackbar: (message) => { utils.notifications.notifyWarningBySnackbar({dispatch, message}); }, 72 | moveTo: (sourceIndex, targetIndex) => { moveTo(dispatch, sourceIndex, targetIndex); }, 73 | }) 74 | 75 | export default connect(mapStateToProps, mapDispatchToProps)(TimelineVariableTable); -------------------------------------------------------------------------------- /src/common/containers/TimelineNodeEditor/TimelineForm/index.js: -------------------------------------------------------------------------------- 1 | import TimelineFormContainer from './TimelineFormContainer.js'; 2 | 3 | export default TimelineFormContainer; -------------------------------------------------------------------------------- /src/common/containers/TimelineNodeEditor/TimelineNodeEditorContainer.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import * as organizerActions from '../../actions/organizerActions'; 3 | import TimelineNodeEditor from '../../components/TimelineNodeEditor'; 4 | import { isTimeline } from '../../reducers/Experiment/utils'; 5 | import * as editorActions from '../../actions/editorActions'; 6 | 7 | 8 | const changeNodeName = (name, dispatch) => { 9 | dispatch(organizerActions.setNameAction(name)); 10 | } 11 | 12 | const changePlugin = (dispatch, newPluginVal) => { 13 | dispatch(editorActions.onPluginTypeChange(newPluginVal)); 14 | } 15 | 16 | const mapStateToProps = (state, ownProps) => { 17 | let experimentState = state.experimentState; 18 | 19 | let node = experimentState[experimentState.previewId]; 20 | if (!node) { 21 | return { 22 | previewId: null, 23 | nodeName: "", 24 | label: "" 25 | }; 26 | } 27 | 28 | return { 29 | previewId: experimentState.previewId, 30 | pluginType: node.parameters.type, 31 | nodeName: node.name, 32 | isTimeline: isTimeline(node), 33 | // label: ((isTimeline(node)) ? "Timeline" : "Trial") + " Name" 34 | label: "", 35 | } 36 | }; 37 | 38 | 39 | const mapDispatchToProps = (dispatch, ownProps) => ({ 40 | changeNodeName: (e, name) => { changeNodeName(name, dispatch) }, 41 | changePlugin: (newPluginVal) => { changePlugin(dispatch, newPluginVal); }, 42 | }) 43 | 44 | export default connect(mapStateToProps, mapDispatchToProps)(TimelineNodeEditor); 45 | -------------------------------------------------------------------------------- /src/common/containers/TimelineNodeEditor/TrialForm/TimelineVariableSelectorContainer.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import TimelineVariableSelector from '../../../components/TimelineNodeEditor/TrialForm/TimelineVariableSelector'; 3 | 4 | const mapStateToProps = (state, ownProps) => { 5 | let experimentState = state.experimentState; 6 | let trial = experimentState[experimentState.previewId]; 7 | let hist = {}, timelineVariables = []; 8 | let timeline = (trial.parent) ? experimentState[trial.parent] : null; 9 | while (timeline) { 10 | for (let tobj of timeline.parameters.timeline_variables) { 11 | for (let name of Object.keys(tobj)) { 12 | if (!hist[name]) { 13 | hist[name] = true; 14 | timelineVariables.push(name); 15 | } 16 | } 17 | } 18 | timeline = experimentState[timeline.parent]; 19 | } 20 | 21 | return { 22 | timelineVariables: timelineVariables, 23 | }; 24 | } 25 | 26 | const mapDispatchToProps = (dispatch,ownProps) => ({ 27 | }) 28 | 29 | export default connect(mapStateToProps, mapDispatchToProps)(TimelineVariableSelector); 30 | -------------------------------------------------------------------------------- /src/common/containers/TimelineNodeEditor/TrialForm/TrialFormItemContainer.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import TrialFormItem from '../../../components/TimelineNodeEditor/TrialForm/TrialFormItem'; 3 | import * as editorActions from '../../../actions/editorActions'; 4 | import { ParameterMode, locateNestedParameterValue, createComplexDataObject } from '../../../reducers/Experiment/editor'; 5 | 6 | const onChangePluginType = (dispatch, newPluginVal) => { 7 | dispatch(editorActions.onPluginTypeChange(newPluginVal)); 8 | } 9 | 10 | const setFunc = (dispatch, key, code, ifEval, language) => { 11 | dispatch(editorActions.setPluginParamAction(key, utils.toNull(code), ParameterMode.USE_FUNC, ifEval, language)); 12 | } 13 | 14 | const setTimelineVariable = (dispatch, key, tv) => { 15 | dispatch(editorActions.setPluginParamAction(key, utils.toNull(tv), ParameterMode.USE_TV)); 16 | } 17 | 18 | const setParamMode = (dispatch, key, mode=ParameterMode.USE_FUNC) => { 19 | dispatch(editorActions.setPluginParamModeAction(key, mode)); 20 | } 21 | 22 | const setText = (dispatch, key, value) => { 23 | dispatch(editorActions.setPluginParamAction(key, utils.toNull(value))); 24 | } 25 | 26 | const setObject = (dispatch, key, obj) => { 27 | dispatch(editorActions.setPluginParamAction(key, obj)); 28 | } 29 | 30 | const setKey = (dispatch, key, value) => { 31 | dispatch(editorActions.setPluginParamAction(key, utils.toNull(value))); 32 | } 33 | 34 | const setToggle = (dispatch, key, flag) => { 35 | dispatch(editorActions.setPluginParamAction(key, flag)); 36 | } 37 | 38 | const setNumber = (dispatch, key, value, isFloat) => { 39 | dispatch(editorActions.setPluginParamAction(key, utils.toNull(value))); 40 | } 41 | 42 | const insertFile = (dispatch, key, value) => { 43 | dispatch(editorActions.setPluginParamAction(key, value)); 44 | } 45 | 46 | const populateComplex = (dispatch, key, paramInfo) => { 47 | let paramPairs = Object.keys(paramInfo).map(k => ({key: k, value: paramInfo[k]})); 48 | 49 | dispatch((dispatch, getState) => { 50 | let experimentState = getState().experimentState; 51 | let node = experimentState[experimentState.previewId]; 52 | let parameterValue = locateNestedParameterValue(node.parameters, key); 53 | let updatedParameterValue = parameterValue.value.slice(); 54 | let update = {}; 55 | for (let entry of paramPairs) { 56 | let defaultValue = entry.value.default; 57 | if (entry.value.array && !entry.value.default) { 58 | defaultValue = []; 59 | } 60 | update[entry.key] = createComplexDataObject(defaultValue); 61 | } 62 | updatedParameterValue.push(update); 63 | dispatch(editorActions.setPluginParamAction(key, updatedParameterValue)); 64 | }) 65 | } 66 | 67 | const depopulateComplex = (dispatch, key, index) => { 68 | dispatch((dispatch, getState) => { 69 | let experimentState = getState().experimentState; 70 | let node = experimentState[experimentState.previewId]; 71 | let parameterValue = locateNestedParameterValue(node.parameters, key); 72 | let updatedParameterValue = parameterValue.value.slice(); 73 | updatedParameterValue.splice(index, 1); 74 | dispatch(editorActions.setPluginParamAction(key, updatedParameterValue)); 75 | }) 76 | } 77 | 78 | const mapStateToProps = (state, ownProps) => { 79 | let experimentState = state.experimentState; 80 | let node = experimentState[experimentState.previewId]; 81 | 82 | return { 83 | id: node.id, 84 | parameters: node.parameters, 85 | }; 86 | } 87 | 88 | const mapDispatchToProps = (dispatch,ownProps) => ({ 89 | onChange: (newPluginVal) => { onChangePluginType(dispatch, newPluginVal); }, 90 | setText: (key, newVal) => { setText(dispatch, key, newVal); }, 91 | setToggle: (key, flag) => { setToggle(dispatch, key, flag); }, 92 | setNumber: (key, newVal, isFloat) => { setNumber(dispatch, key, newVal, isFloat); }, 93 | setFunc: (key, code, ifEval, language) => { setFunc(dispatch, key, code, ifEval, language); }, 94 | setParamMode: (key, mode) => { setParamMode(dispatch, key, mode); }, 95 | setKey: (key, value) => { setKey(dispatch, key, value); }, 96 | setTimelineVariable: (key, tv) => { setTimelineVariable(dispatch, key, tv); }, 97 | insertFile: (key, value) => { insertFile(dispatch, key, value); }, 98 | setObject: (key, obj) => { setObject(dispatch, key, obj); }, 99 | populateComplex: (key, paramInfo) => { populateComplex(dispatch, key, paramInfo); }, 100 | depopulateComplex: (key, index) => { depopulateComplex(dispatch, key, index); } 101 | }) 102 | 103 | export default connect(mapStateToProps, mapDispatchToProps)(TrialFormItem); 104 | -------------------------------------------------------------------------------- /src/common/containers/TimelineNodeEditor/TrialForm/index.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import TrialForm from '../../../components/TimelineNodeEditor/TrialForm'; 3 | import * as editorActions from '../../../actions/editorActions'; 4 | 5 | const onChangePluginType = (dispatch, newPluginVal) => { 6 | dispatch(editorActions.onPluginTypeChange(newPluginVal)); 7 | } 8 | 9 | const mapStateToProps = (state, ownProps) => { 10 | let experimentState = state.experimentState; 11 | let trial = experimentState[experimentState.previewId]; 12 | return { 13 | pluginType: trial.parameters.type, 14 | }; 15 | } 16 | 17 | const mapDispatchToProps = (dispatch,ownProps) => ({ 18 | onChange: (newPluginVal) => { onChangePluginType(dispatch, newPluginVal); }, 19 | }) 20 | 21 | export default connect(mapStateToProps, mapDispatchToProps)(TrialForm); 22 | -------------------------------------------------------------------------------- /src/common/containers/TimelineNodeEditor/index.js: -------------------------------------------------------------------------------- 1 | import TimelineNodeEditorContainer from './TimelineNodeEditorContainer.js'; 2 | 3 | export default TimelineNodeEditorContainer; -------------------------------------------------------------------------------- /src/common/containers/TimelineNodeOrganizer/SortableTreeMenu/TimelineItemContainer.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import * as organizerActions from '../../../actions/organizerActions'; 3 | import TimelineItem from '../../../components/TimelineNodeOrganizer/SortableTreeMenu/TimelineItem'; 4 | 5 | const onPreview = (dispatch, ownProps, setKeyboardFocusId) => { 6 | dispatch((dispatch, getState) => { 7 | let experimentState = getState().experimentState; 8 | let previewId = experimentState.previewId; 9 | if (previewId === null || previewId !== ownProps.id) { 10 | dispatch(organizerActions.onPreviewAction(ownProps.id)); 11 | ownProps.openTimelineEditorCallback(); 12 | if (setKeyboardFocusId) setKeyboardFocusId(ownProps.id); 13 | } else { 14 | dispatch(organizerActions.onPreviewAction(null)); 15 | // ownProps.closeTimelineEditorCallback(); 16 | if (setKeyboardFocusId) setKeyboardFocusId(null); 17 | } 18 | }) 19 | } 20 | 21 | const onToggle = (dispatch, ownProps) => { 22 | dispatch(organizerActions.onToggleAction(ownProps.id)); 23 | } 24 | 25 | const toggleCollapsed = (dispatch, ownProps) => { 26 | dispatch(organizerActions.setCollapsed(ownProps.id)); 27 | } 28 | 29 | const insertTimeline = (dispatch, ownProps) => { 30 | dispatch(organizerActions.addTimelineAction(ownProps.id)); 31 | } 32 | 33 | const insertTrial = (dispatch, ownProps) => { 34 | dispatch(organizerActions.addTrialAction(ownProps.id)); 35 | } 36 | 37 | const deleteTimeline = (dispatch, ownProps) => { 38 | dispatch(organizerActions.deleteTimelineAction(ownProps.id)); 39 | } 40 | 41 | const duplicateTimeline = (dispatch, ownProps) => { 42 | dispatch(organizerActions.duplicateTimelineAction(ownProps.id)); 43 | } 44 | 45 | export const listenKey = (e, getKeyboardFocusId, dispatch, ownProps) => { 46 | e.preventDefault(); 47 | 48 | if (getKeyboardFocusId() === ownProps.id && 49 | e.which >= 37 && 50 | e.which <= 40) { 51 | dispatch(organizerActions.moveByKeyboardAction(ownProps.id, e.which)); 52 | } 53 | } 54 | 55 | const mapStateToProps = (state, ownProps) => { 56 | let experimentState = state.experimentState; 57 | 58 | let node = experimentState[ownProps.id]; 59 | let len = node.childrenById.length; 60 | return { 61 | isSelected: ownProps.id === experimentState.previewId, 62 | isEnabled: node.enabled, 63 | name: node.name, 64 | collapsed: node.collapsed, 65 | hasNoChildren: len === 0, 66 | childrenById: node.childrenById, 67 | parent: node.parent, 68 | lastItem: (len > 0) ? node.childrenById[len-1] : null 69 | } 70 | }; 71 | 72 | 73 | const mapDispatchToProps = (dispatch, ownProps) => ({ 74 | dispatch: dispatch, 75 | onClick: (setKeyboardFocusId) => { onPreview(dispatch, ownProps, setKeyboardFocusId) }, 76 | onToggle: () => { onToggle(dispatch, ownProps) }, 77 | toggleCollapsed: () => { toggleCollapsed(dispatch, ownProps) }, 78 | insertTimeline: () => { insertTimeline(dispatch, ownProps)}, 79 | insertTrial: () => { insertTrial(dispatch, ownProps)}, 80 | deleteTimeline: () => { deleteTimeline(dispatch, ownProps)}, 81 | duplicateTimeline: () => { duplicateTimeline(dispatch, ownProps) }, 82 | listenKey: (e, getKeyboardFocusId) => { listenKey(e, getKeyboardFocusId, dispatch, ownProps) }, 83 | }) 84 | 85 | export default connect(mapStateToProps, mapDispatchToProps)(TimelineItem); 86 | -------------------------------------------------------------------------------- /src/common/containers/TimelineNodeOrganizer/SortableTreeMenu/TreeContainer.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import Tree from '../../../components/TimelineNodeOrganizer/SortableTreeMenu/Tree'; 3 | 4 | const mapStateToProps = (state, ownProps) => { 5 | return { 6 | } 7 | }; 8 | 9 | 10 | const mapDispatchToProps = (dispatch, ownProps) => ({ 11 | dispatch: dispatch 12 | }) 13 | 14 | export default connect(mapStateToProps, mapDispatchToProps)(Tree); 15 | -------------------------------------------------------------------------------- /src/common/containers/TimelineNodeOrganizer/SortableTreeMenu/TreeNodeContainer.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import TreeNode from '../../../components/TimelineNodeOrganizer/SortableTreeMenu/TreeNode'; 3 | import { isTimeline } from '../../../reducers/Experiment/utils'; 4 | 5 | 6 | const mapStateToProps = (state, ownProps) => { 7 | let experimentState = state.experimentState; 8 | 9 | let node = experimentState[ownProps.id]; 10 | let isTimelineNode = isTimeline(node); 11 | 12 | return { 13 | isTimeline: isTimelineNode, 14 | children: (isTimelineNode) ? node.childrenById : [] 15 | } 16 | }; 17 | 18 | 19 | const mapDispatchToProps = (dispatch, ownProps) => ({ 20 | }) 21 | 22 | export default connect(mapStateToProps, mapDispatchToProps)(TreeNode); 23 | -------------------------------------------------------------------------------- /src/common/containers/TimelineNodeOrganizer/SortableTreeMenu/TrialItemContainer.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import * as organizerActions from '../../../actions/organizerActions'; 3 | import TrialItem from '../../../components/TimelineNodeOrganizer/SortableTreeMenu/TrialItem'; 4 | import { listenKey } from './TimelineItemContainer'; 5 | 6 | const onPreview = (dispatch, ownProps, setKeyboardFocusId) => { 7 | // console.log(e.nativeEvent.which) 8 | dispatch((dispatch, getState) => { 9 | let experimentState = getState().experimentState; 10 | let previewId = experimentState.previewId; 11 | if (previewId === null || previewId !== ownProps.id) { 12 | dispatch(organizerActions.onPreviewAction(ownProps.id)); 13 | ownProps.openTimelineEditorCallback(); 14 | if (setKeyboardFocusId) setKeyboardFocusId(ownProps.id); 15 | } else { 16 | dispatch(organizerActions.onPreviewAction(null)); 17 | // ownProps.closeTimelineEditorCallback(); 18 | if (setKeyboardFocusId) setKeyboardFocusId(null); 19 | } 20 | }) 21 | } 22 | 23 | const onToggle = (dispatch, ownProps) => { 24 | dispatch(organizerActions.onToggleAction(ownProps.id)); 25 | } 26 | 27 | const insertTimeline = (dispatch, ownProps) => { 28 | dispatch(organizerActions.insertNodeAfterTrialAction(ownProps.id, true)); 29 | } 30 | 31 | const insertTrial = (dispatch, ownProps) => { 32 | dispatch(organizerActions.insertNodeAfterTrialAction(ownProps.id, false)); 33 | } 34 | 35 | const deleteTrial = (dispatch, ownProps) => { 36 | dispatch(organizerActions.deleteTrialAction(ownProps.id)); 37 | } 38 | 39 | const duplicateTrial = (dispatch, ownProps) => { 40 | dispatch(organizerActions.duplicateTrialAction(ownProps.id)); 41 | } 42 | 43 | const mapStateToProps = (state, ownProps) => { 44 | let experimentState = state.experimentState; 45 | 46 | let node = experimentState[ownProps.id]; 47 | 48 | return { 49 | isSelected: ownProps.id === experimentState.previewId, 50 | isEnabled: node.enabled, 51 | name: node.name, 52 | parent: node.parent, 53 | } 54 | }; 55 | 56 | 57 | const mapDispatchToProps = (dispatch, ownProps) => ({ 58 | dispatch: dispatch, 59 | onClick: (setKeyboardFocusId) => { onPreview(dispatch, ownProps, setKeyboardFocusId) }, 60 | onToggle: () => { onToggle(dispatch, ownProps) }, 61 | insertTimeline: () => { insertTimeline(dispatch, ownProps)}, 62 | insertTrial: () => { insertTrial(dispatch, ownProps)}, 63 | deleteTrial: () => { deleteTrial(dispatch, ownProps)}, 64 | duplicateTrial: () => { duplicateTrial(dispatch, ownProps) }, 65 | listenKey: (e, getKeyboardFocusId) => { listenKey(e, getKeyboardFocusId, dispatch, ownProps) }, 66 | }) 67 | 68 | export default connect(mapStateToProps, mapDispatchToProps)(TrialItem); 69 | -------------------------------------------------------------------------------- /src/common/containers/TimelineNodeOrganizer/SortableTreeMenu/index.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import SortableTreeMenu from '../../../components/TimelineNodeOrganizer/SortableTreeMenu'; 3 | 4 | 5 | const mapStateToProps = (state, ownProps) => { 6 | let experimentState = state.experimentState; 7 | 8 | return { 9 | children: experimentState.mainTimeline, 10 | } 11 | }; 12 | 13 | 14 | const mapDispatchToProps = (dispatch, ownProps) => ({ 15 | }) 16 | 17 | export default connect(mapStateToProps, mapDispatchToProps)(SortableTreeMenu); 18 | -------------------------------------------------------------------------------- /src/common/containers/TimelineNodeOrganizer/TimelineNodeOrganizerContainer.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import * as organizerActions from '../../actions/organizerActions'; 3 | import TimelineNodeOrganizer from '../../components/TimelineNodeOrganizer'; 4 | import { isTimeline } from '../../reducers/Experiment/utils'; 5 | 6 | 7 | const insertTrial = (dispatch) => { 8 | dispatch((dispatch, getState) => { 9 | let experimentState = getState().experimentState; 10 | let previewId = experimentState.previewId; 11 | if (previewId === null) { 12 | dispatch(organizerActions.addTrialAction(null)); 13 | // its a timeline 14 | } else if (isTimeline(experimentState[previewId])) { 15 | dispatch(organizerActions.addTrialAction(previewId)); 16 | // its a trial 17 | } else { 18 | let parent = experimentState[previewId].parent; 19 | dispatch(organizerActions.addTrialAction(parent)); 20 | } 21 | }) 22 | } 23 | 24 | const insertTimeline = (dispatch) => { 25 | dispatch((dispatch, getState) => { 26 | let experimentState = getState().experimentState; 27 | let previewId = experimentState.previewId; 28 | if (previewId === null) { 29 | dispatch(organizerActions.addTimelineAction(null)); 30 | // its a timeline 31 | } else if (isTimeline(experimentState[previewId])) { 32 | dispatch(organizerActions.addTimelineAction(previewId)); 33 | // its a trial 34 | } else { 35 | let parent = experimentState[previewId].parent; 36 | dispatch(organizerActions.addTimelineAction(parent)); 37 | } 38 | }) 39 | } 40 | 41 | const deleteSelected = (dispatch) => { 42 | dispatch((dispatch, getState) => { 43 | let experimentState = getState().experimentState; 44 | let previewId = experimentState.previewId; 45 | if (previewId === null) { 46 | return; 47 | // its a timeline 48 | } else if (isTimeline(experimentState[previewId])) { 49 | dispatch(organizerActions.deleteTimelineAction(previewId)); 50 | // its a trial 51 | } else { 52 | dispatch(organizerActions.deleteTrialAction(previewId)); 53 | } 54 | }) 55 | } 56 | 57 | const duplicateNode = (dispatch) => { 58 | dispatch((dispatch, getState) => { 59 | let experimentState = getState().experimentState; 60 | let previewId = experimentState.previewId; 61 | if (previewId === null) { 62 | return; 63 | // its a timeline 64 | } else if (isTimeline(experimentState[previewId])) { 65 | dispatch(organizerActions.duplicateTimelineAction(previewId)); 66 | // its a trial 67 | } else { 68 | dispatch(organizerActions.duplicateTrialAction(previewId)); 69 | } 70 | }) 71 | } 72 | 73 | const mapStateToProps = (state, ownProps) => { 74 | // let experimentState = state.experimentState; 75 | 76 | return { 77 | } 78 | }; 79 | 80 | 81 | const mapDispatchToProps = (dispatch, ownProps) => ({ 82 | insertTrial: () => { insertTrial(dispatch) }, 83 | insertTimeline: () => { insertTimeline(dispatch) }, 84 | deleteSelected: () => { deleteSelected(dispatch) }, 85 | duplicateNode: () => { duplicateNode(dispatch) }, 86 | }) 87 | 88 | export default connect(mapStateToProps, mapDispatchToProps)(TimelineNodeOrganizer); 89 | -------------------------------------------------------------------------------- /src/common/containers/TimelineNodeOrganizer/index.js: -------------------------------------------------------------------------------- 1 | import TimelineNodeOrganizerContainer from './TimelineNodeOrganizerContainer.js'; 2 | 3 | export default TimelineNodeOrganizerContainer; -------------------------------------------------------------------------------- /src/common/reducers/Authentications/authenticationsReducer.js: -------------------------------------------------------------------------------- 1 | const initState = { 2 | open: false, 3 | loginMode: enums.AUTH_MODES.signIn, 4 | signInCallback: () => Promise.resolve(), 5 | } 6 | 7 | const setAuthWindow = (state, action) => { 8 | action = utils.deepCopy(action); 9 | delete action.type; 10 | return Object.assign({}, state, { 11 | ...action 12 | }) 13 | } 14 | 15 | export default function reducer(state=initState, action) { 16 | switch(action.type) { 17 | case actions.ActionTypes.SET_AUTH_WINDOW: 18 | return setAuthWindow(state, action); 19 | default: 20 | return state; 21 | } 22 | } -------------------------------------------------------------------------------- /src/common/reducers/Authentications/index.js: -------------------------------------------------------------------------------- 1 | import authenticationsReducer from './authenticationsReducer.js'; 2 | 3 | export default authenticationsReducer; -------------------------------------------------------------------------------- /src/common/reducers/Experiment/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | *@file This file describes the state template and root reducer for Experiment State. 3 | */ 4 | import * as organizer from './organizer'; 5 | import * as jsPsychInit from './jsPsychInit'; 6 | import * as editor from './editor'; 7 | 8 | 9 | export const initState = core.getInitExperimentState(); 10 | 11 | /**@function(state, action) 12 | * @name setExperimentName 13 | * @description Set experiment's name 14 | * @param {Object} state - The Experiment State Object 15 | * @param {Object} action - Describes the action user invokes 16 | * @param {guiValue} action.name - The experiment's user defined name 17 | * @returns {object} Returns a completely new Experiment State object 18 | */ 19 | const setExperimentName = (state, action) => { 20 | return Object.assign({}, state, { 21 | experimentName: action.name 22 | }) 23 | } 24 | 25 | const setCloudDeployInfo = (state, action) => { 26 | return Object.assign({}, state, { 27 | cloudDeployInfo: action.cloudDeployInfo 28 | }) 29 | } 30 | 31 | const setDIYDeployInfo = (state, action) => { 32 | return Object.assign({}, state, { 33 | diyDeployInfo: action.diyDeployInfo 34 | }) 35 | } 36 | 37 | /**@function(state, action) 38 | * Always init view to preview the first timeline node 39 | * @param {Object} action.experimentState 40 | * 41 | */ 42 | const loadExperiment = (state, action) => { 43 | let { experimentState } = action; 44 | let mainTimeline = experimentState.mainTimeline; 45 | return Object.assign({}, experimentState, { 46 | previewId: mainTimeline.length > 0 ? mainTimeline[0] : null 47 | }); 48 | } 49 | 50 | 51 | /**@function(state, action) 52 | * @name experimentReducer 53 | * @description The root reducer for the whole experiment state 54 | * @param {object} state - The Experiment State Object 55 | * @param {object} action - Describes the action user invokes 56 | * @returns {object} Returns a completely new Experiment State object 57 | */ 58 | export default function experimentReducer(state=initState, action) { 59 | switch(action.type) { 60 | // organizer starts 61 | case actions.ActionTypes.ADD_TIMELINE: 62 | return organizer.addTimeline(state, action); 63 | case actions.ActionTypes.DELETE_TIMELINE: 64 | return organizer.deleteTimeline(state, action); 65 | case actions.ActionTypes.ADD_TRIAL: 66 | return organizer.addTrial(state, action); 67 | case actions.ActionTypes.DELETE_TRIAL: 68 | return organizer.deleteTrial(state, action); 69 | case actions.ActionTypes.INSERT_NODE_AFTER_TRIAL: 70 | return organizer.insertNodeAfterTrial(state, action); 71 | case actions.ActionTypes.DUPLICATE_TRIAL: 72 | return organizer.duplicateTrial(state, action); 73 | case actions.ActionTypes.DUPLICATE_TIMELINE: 74 | return organizer.duplicateTimeline(state, action); 75 | case actions.ActionTypes.MOVE_TO: 76 | return organizer.moveTo(state, action); 77 | case actions.ActionTypes.MOVE_INTO: 78 | return organizer.moveInto(state, action); 79 | case actions.ActionTypes.MOVE_BY_KEYBOARD: 80 | return organizer.moveByKeyboard(state, action); 81 | case actions.ActionTypes.ON_PREVIEW: 82 | return organizer.onPreview(state, action); 83 | case actions.ActionTypes.ON_TOGGLE: 84 | return organizer.onToggle(state, action); 85 | case actions.ActionTypes.SET_COLLAPSED: 86 | return organizer.setCollapsed(state, action); 87 | 88 | // jspsych.init starts 89 | case actions.ActionTypes.SET_JSPSYCH_INIT: 90 | return jsPsychInit.setJspyschInit(state, action); 91 | 92 | // Main 93 | case actions.ActionTypes.SET_EXPERIMENT_NAME: 94 | return setExperimentName(state, action); 95 | case actions.ActionTypes.LOAD_EXPERIMENT: 96 | return loadExperiment(state, action); 97 | 98 | // Deploy 99 | case actions.ActionTypes.SET_CLOUD_DEPLOY_INFO: 100 | return setCloudDeployInfo(state, action); 101 | case actions.ActionTypes.SET_DIY_DEPLOY_INFO: 102 | return setDIYDeployInfo(state, action); 103 | 104 | // editor starts 105 | case actions.ActionTypes.SET_NAME: 106 | return editor.setName(state, action); 107 | 108 | // Trial form 109 | case actions.ActionTypes.CHANGE_PLUGIN_TYPE: 110 | return editor.changePlugin(state, action); 111 | case actions.ActionTypes.SET_PLUGIN_PARAMTER: 112 | return editor.setPluginParam(state, action); 113 | case actions.ActionTypes.SET_PLUGIN_PARAMTER_MODE: 114 | return editor.setPluginParamMode(state, action); 115 | case actions.ActionTypes.UPDATE_MEDIA: 116 | return editor.updateMedia(state, action); 117 | 118 | // Timeline form 119 | case actions.ActionTypes.UPDATE_TIMELINE_VARIABLE_TABLE_ROW: 120 | return editor.updateTimelineVariableRow(state, action); 121 | case actions.ActionTypes.UPDATE_TIMELINE_VARIABLE_INPUT_TYPE: 122 | return editor.updateTimelineVariableInputType(state, action); 123 | case actions.ActionTypes.UPDATE_TIMELINE_VARIABLE_CELL: 124 | return editor.updateTimelineVariableCell(state, action); 125 | case actions.ActionTypes.UPDATE_TIMELINE_VARIABLE_TABLE_HEADER: 126 | return editor.updateTimelineVariableName(state, action); 127 | case actions.ActionTypes.MOVE_TIMELINE_VARIABLE_ROW_TO: 128 | return editor.moveRowTo(state, action); 129 | case actions.ActionTypes.ADD_TIMELINE_VARIABLE_ROW: 130 | return editor.addTimelineVariableRow(state, action); 131 | case actions.ActionTypes.ADD_TIMELINE_VARIABLE_COLUMN: 132 | return editor.addTimelineVariableColumn(state, action); 133 | case actions.ActionTypes.DELETE_TIMELINE_VARIABLE_ROW: 134 | return editor.deleteTimelineVariableRow(state, action); 135 | case actions.ActionTypes.DELETE_TIMELINE_VARIABLE_COLUMN: 136 | return editor.deleteTimelineVariableColumn(state, action); 137 | case actions.ActionTypes.SET_SAMPLING_METHOD: 138 | return editor.setSamplingMethod(state, action); 139 | case actions.ActionTypes.SET_SAMPLE_SIZE: 140 | return editor.setSampleSize(state, action); 141 | case actions.ActionTypes.SET_RANDOMIZE: 142 | return editor.setRandomize(state, action); 143 | case actions.ActionTypes.SET_REPETITIONS: 144 | return editor.setRepetitions(state, action); 145 | case actions.ActionTypes.SET_LOOP_FUNCTION: 146 | return editor.setLoopFunction(state, action); 147 | case actions.ActionTypes.SET_CONDITION_FUNCTION: 148 | return editor.setConditionFunction(state, action); 149 | case actions.ActionTypes.SET_TIMELINE_VARIABLE: 150 | return editor.setTimelineVariable(state, action); 151 | default: 152 | return state; 153 | } 154 | } -------------------------------------------------------------------------------- /src/common/reducers/Experiment/tests/initSetting.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | This file tests jsPsychInit.js 3 | 4 | */ 5 | 6 | import { initState } from '../'; 7 | import reducer from '../'; 8 | 9 | import { deepCopy } from '../../../utils'; 10 | import * as Actions from '../../../actions/experimentSettingActions'; 11 | import { settingType, createFuncObj } from '../jsPsychInit'; 12 | 13 | 14 | let expected = deepCopy(initState); 15 | for (var key of Object.keys(settingType)) { 16 | switch(key) { 17 | case settingType.on_finish: 18 | case settingType.on_data_update: 19 | case settingType.on_trial_start: 20 | case settingType.on_trial_finish: 21 | case settingType.on_interaction_data_update: 22 | expected.jsPsychInit[key] = createFuncObj(key); 23 | break; 24 | case settingType.min_width: 25 | expected.jsPsychInit.exclusions.min_width = key; 26 | break; 27 | case settingType.min_height: 28 | expected.jsPsychInit.exclusions.min_height = key; 29 | break; 30 | case settingType.audio: 31 | expected.jsPsychInit.exclusions.audio = !expected.jsPsychInit.exclusions.audio; 32 | break; 33 | case settingType.show_progress_bar: 34 | case settingType.auto_update_progress_bar: 35 | case settingType.show_preload_progress_bar: 36 | expected.jsPsychInit[key] = !expected.jsPsychInit[key]; 37 | break; 38 | default: 39 | expected.jsPsychInit[key] = key; 40 | } 41 | } 42 | 43 | describe('Set jsPysch Init Properties', () => { 44 | it('should editing jsPsych Init properties for the experiment', () => { 45 | let s1 = deepCopy(initState); 46 | for (var key of Object.keys(settingType)) { 47 | s1 = reducer(s1, Actions.setJspyschInitAction(key, key)); 48 | } 49 | expect(s1).toEqual(expected); 50 | }) 51 | }) -------------------------------------------------------------------------------- /src/common/reducers/Experiment/utils/index.js: -------------------------------------------------------------------------------- 1 | const TIMELINE_ID_PREFIX = "TIMELINE"; 2 | const TRIAL_ID_PREFIX = "TRIAL"; 3 | 4 | export const TIMELINE_TYPE = "TIMELINE"; 5 | export const TRIAL_TYPE = "TRIAL"; 6 | 7 | 8 | export const genTimelineId = () => `${TIMELINE_TYPE}-${utils.getUUID()}` 9 | 10 | export const genTrialId = () => `${TRIAL_TYPE}-${utils.getUUID()}` 11 | 12 | export const isTimeline = (node) => (node.type === TIMELINE_TYPE); 13 | 14 | export const isTrial = (node) => (node.type === TRIAL_TYPE); 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/common/reducers/Notifications/index.js: -------------------------------------------------------------------------------- 1 | const initState = { 2 | dialogOpen: false, 3 | snackbarOpen: false, 4 | notifyType: enums.Notify_Type.success, 5 | message: "", 6 | 7 | // confirm 8 | continueWithOperation: () => Promise.resolve(), 9 | continueWithoutOperation: () => Promise.resolve(), 10 | continueWithOperationLabel: "Yes", 11 | continueWithoutOperationLabel: "No", 12 | showCancelButton: true, 13 | withExtraCare: false, 14 | extraCareText: "Yes, I know what I am doing." 15 | } 16 | 17 | 18 | const setNotification = (state, action) => { 19 | action = utils.deepCopy(action); 20 | let type = action.type; 21 | delete action[type]; 22 | switch(type) { 23 | case actions.ActionTypes.NOTIFY_WARNING_DIALOG: 24 | case actions.ActionTypes.NOTIFY_SUCCESS_DIALOG: 25 | case actions.ActionTypes.NOTIFY_ERROR_DIALOG: 26 | case actions.ActionTypes.POP_UP_CONFIRM: 27 | return Object.assign({}, initState, { 28 | dialogOpen: true, 29 | ...action 30 | }); 31 | case actions.ActionTypes.NOTIFY_SUCCESS_SNACKBAR: 32 | case actions.ActionTypes.NOTIFY_WARNING_SNACKBAR: 33 | case actions.ActionTypes.NOTIFY_ERROR_SNACKBAR: 34 | return Object.assign({}, initState, { 35 | snackbarOpen: true, 36 | ...action 37 | }); 38 | case actions.ActionTypes.NOTIFY_DIALOG_CLOSE: 39 | return Object.assign({}, state, { 40 | dialogOpen: false, 41 | }); 42 | case actions.ActionTypes.NOTIFY_SNACKBAR_CLOSE: 43 | return Object.assign({}, state, { 44 | snackbarOpen: false, 45 | }); 46 | default: 47 | return state; 48 | } 49 | } 50 | 51 | export default function reducer(state=initState, action) { 52 | switch(action.type) { 53 | case actions.ActionTypes.NOTIFY_WARNING_DIALOG: 54 | case actions.ActionTypes.NOTIFY_WARNING_SNACKBAR: 55 | case actions.ActionTypes.NOTIFY_SUCCESS_DIALOG: 56 | case actions.ActionTypes.NOTIFY_SUCCESS_SNACKBAR: 57 | case actions.ActionTypes.NOTIFY_ERROR_DIALOG: 58 | case actions.ActionTypes.NOTIFY_ERROR_SNACKBAR: 59 | case actions.ActionTypes.NOTIFY_DIALOG_CLOSE: 60 | case actions.ActionTypes.NOTIFY_SNACKBAR_CLOSE: 61 | case actions.ActionTypes.POP_UP_CONFIRM: 62 | return setNotification(state, action); 63 | default: 64 | return state; 65 | } 66 | } -------------------------------------------------------------------------------- /src/common/reducers/User/index.js: -------------------------------------------------------------------------------- 1 | const initState = core.createUser(); 2 | 3 | /** 4 | * Reducer that sets OSF access infomation 5 | * @param {Object} action.osfAccess - OSF Access information 6 | * @return A new userState 7 | */ 8 | function setOsfAccess(state, action) { 9 | return Object.assign({}, state, { 10 | osfAccess: action.osfAccess 11 | }); 12 | } 13 | 14 | /** 15 | * Reducer that loads fetched user data from dynamoDB 16 | * @param {Object} action.userState - fetched user data from dynamoDB 17 | * @return A new userState 18 | */ 19 | function loadUserState(state, action) { 20 | return Object.assign({}, state, { 21 | ...action.userState 22 | }); 23 | } 24 | 25 | 26 | export default function userReducer(state = initState, action) { 27 | switch (action.type) { 28 | case actions.ActionTypes.LOAD_USER: 29 | return loadUserState(state, action); 30 | 31 | // cloud access information 32 | case actions.ActionTypes.SET_OSF_ACCESS: 33 | return setOsfAccess(state, action); 34 | 35 | default: 36 | return state; 37 | } 38 | } 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/common/reducers/User/tests/user.test.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jspsych/jsPsych-Redux-GUI/f6fee6f3f1678b15f244404645335f5ea1d7b6b4/src/common/reducers/User/tests/user.test.js -------------------------------------------------------------------------------- /src/common/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from "redux"; 2 | import experitmentReducer from './Experiment'; 3 | import userReducer from './User'; 4 | import notificationsReducer from './Notifications'; 5 | import authenticationsReducer from './Authentications'; 6 | 7 | const combinedReducers = combineReducers({ 8 | experimentState: experitmentReducer, 9 | userState: userReducer, 10 | notifications: notificationsReducer, 11 | authentications: authenticationsReducer 12 | }); 13 | 14 | export default combinedReducers; 15 | -------------------------------------------------------------------------------- /src/common/utils/index.js: -------------------------------------------------------------------------------- 1 | import Prefixer from 'inline-style-prefixer'; 2 | import { DragDropContext } from 'react-dnd'; 3 | import HTML5Backend from 'react-dnd-html5-backend'; 4 | import { cloneDeep, isEqual } from 'lodash'; 5 | import short_uuid from 'short-uuid'; 6 | 7 | import * as notifications from '../containers/Notifications/NotificationsContainer.js'; 8 | import * as loginWindows from '../containers/Authentications/AuthenticationsContainer.js'; 9 | import * as commonFlows from '../containers/commonFlows.js'; 10 | 11 | if (!Array.prototype.move) { 12 | Array.prototype.move = function(from,to){ 13 | this.splice(to,0,this.splice(from,1)[0]); 14 | return this; 15 | }; 16 | } 17 | 18 | // CSS prefixer 19 | const _prefixer = new Prefixer(); 20 | export const prefixer = (style={}, multiple=false) => { 21 | if (!multiple) return _prefixer.prefix(style); 22 | let res = {}; 23 | for (let key of Object.keys(style)) { 24 | res[key] = _prefixer.prefix(style[key]); 25 | } 26 | return res; 27 | } 28 | 29 | // Backend flows or related 30 | export { notifications, loginWindows, commonFlows }; 31 | 32 | // React-DnD 33 | export const withDnDContext = DragDropContext(HTML5Backend); 34 | 35 | // utility functions 36 | export const deepCopy = cloneDeep; 37 | 38 | export const deepEqual = isEqual; 39 | 40 | export function getUUID() { 41 | var translator = short_uuid(); 42 | //var decimalTranslator = short("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"); 43 | let res = translator.new(); 44 | return res; 45 | } 46 | 47 | export const toNull = (s) => ((s === '') ? null : s); 48 | 49 | export const toEmptyString = (s) => ((s === null || s === undefined) ? '' : s); 50 | 51 | export const toEmptyArray = (s) => (!s ? [] : s); 52 | 53 | export function isValueEmpty(val) { 54 | return val === '' || val === null || val === undefined || (Array.isArray(val) && val.length === 0) || 55 | (typeof val === 'object' && Object.keys(val).length === 0); 56 | } 57 | 58 | export function injectJsPsychUniversalPluginParameters(obj={}) { 59 | return Object.assign(obj, window.jsPsych.plugins.universalPluginParameters); 60 | } 61 | 62 | /** 63 | * typeof {(object|string)} ParamPathNode 64 | * @property {ParamPathNode} next 65 | * @property {number} position - index 66 | * @property {string} key 67 | */ 68 | export function locateNestedParameterValue(parameters, path) { 69 | let parameterValue = parameters; 70 | if (typeof path === 'object') { 71 | // find the complex type jsPsych plugin parameter 72 | parameterValue = parameterValue[path.key]; 73 | path = path.next; 74 | while (path) { 75 | let tmp = parameterValue.value[path.position]; 76 | parameterValue = parameterValue.value[path.position][path.key]; 77 | path = path.next; 78 | } 79 | } else { 80 | parameterValue = parameterValue[path]; 81 | } 82 | 83 | return parameterValue; 84 | } 85 | 86 | /** 87 | * @function isJspsychValueObjectEmpty 88 | * @param {JspsychValueObject} obj 89 | * @desc Should originally be a method of the JspsychValueObject class that determines if the object is truly empty. But since 90 | * AWS.DynamoDB does not store functions, this method is taken out separatly. 91 | * @returns {boolean} 92 | */ 93 | export const isJspsychValueObjectEmpty = (obj) => { 94 | switch (obj.mode) { 95 | case enums.ParameterMode.USE_FUNC: 96 | return !obj.func.code; 97 | case enums.ParameterMode.USE_TV: 98 | return !obj.timelineVariable; 99 | case enums.ParameterMode.USE_VAL: 100 | default: 101 | return !obj.value; 102 | } 103 | } 104 | 105 | export const isString = (type) => (type === enums.TimelineVariableInputType.TEXT || type === enums.TimelineVariableInputType.LONG_TEXT); 106 | 107 | export const isFunction = (type) => (type === enums.TimelineVariableInputType.FUNCTION); 108 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | require('./client'); -------------------------------------------------------------------------------- /src/server/dev-server.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import Express from 'express'; 3 | 4 | import webpack from 'webpack'; 5 | import webpackDevMiddleware from 'webpack-dev-middleware'; 6 | import webpackHotMiddleware from 'webpack-hot-middleware'; 7 | import webpackConfig from '../../config/webpack.config.development'; 8 | 9 | // import template from '../../public/template'; 10 | 11 | const app = new Express(); 12 | const port = 3000; 13 | 14 | // Use this middleware to set up hot module reloading via webpack. 15 | const compiler = webpack(webpackConfig); 16 | app.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: webpackConfig.output.publicPath })); 17 | app.use(webpackHotMiddleware(compiler)); 18 | app.use(Express.static(path.join(__dirname + '/../../public'))); 19 | 20 | // app.use(function(req, res) { 21 | // res.send(template({})); 22 | // }); 23 | 24 | var server = app.listen(port, function() { 25 | var port = server.address().port; 26 | 27 | console.log("Example app listening at http://localhost:%s", port); 28 | }) --------------------------------------------------------------------------------