├── .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 |
76 | )
77 | } else {
78 | return (
79 |
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 |
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 |
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 |
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 |
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 |
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 | })
--------------------------------------------------------------------------------