From 21dbba43826c540f5291ac7c1d5d68cbaa309337 Mon Sep 17 00:00:00 2001 From: Cassie Tarakajian <ctarakajian@gmail.com> Date: Mon, 4 Jan 2021 14:35:14 -0500 Subject: [PATCH 1/4] Add prettier to packages --- .eslintrc | 7 ++-- client/modules/IDE/components/FileNode.jsx | 2 +- package-lock.json | 42 ++++++++++++++++++++-- package.json | 3 ++ 4 files changed, 48 insertions(+), 6 deletions(-) diff --git a/.eslintrc b/.eslintrc index 1122f7e2e2..b2be2ffa01 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,5 +1,5 @@ { - "extends": "airbnb", + "extends": ["airbnb", "prettier"], "parser": "babel-eslint", "env": { "browser": true, @@ -60,10 +60,13 @@ }, "allowChildren": false } + ], + "prettier/prettier": [ + "error" ] }, "plugins": [ - "react", "jsx-a11y", "import" + "react", "jsx-a11y", "import", "prettier" ], "settings": { "import/parser": "babel-eslint", diff --git a/client/modules/IDE/components/FileNode.jsx b/client/modules/IDE/components/FileNode.jsx index f4b8166d31..c2d0271eea 100644 --- a/client/modules/IDE/components/FileNode.jsx +++ b/client/modules/IDE/components/FileNode.jsx @@ -271,7 +271,7 @@ class FileNode extends React.Component { </button> </div> } - <button + <button aria-label={this.state.updatedName} className="sidebar__file-item-name" onClick={this.handleFileClick} diff --git a/package-lock.json b/package-lock.json index 54cd215242..4c54315595 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7358,6 +7358,12 @@ "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.6.5.tgz", "integrity": "sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA==", "dev": true + }, + "prettier": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz", + "integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==", + "dev": true } } }, @@ -14866,6 +14872,12 @@ "eslint-restricted-globals": "^0.1.1" } }, + "eslint-config-prettier": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-7.0.0.tgz", + "integrity": "sha512-8Y8lGLVPPZdaNA7JXqnvETVC7IiVRgAP6afQu9gOQRn90YY3otMNh+x7Vr2vMePQntF+5erdSUBqSzCmU/AxaQ==", + "dev": true + }, "eslint-import-resolver-node": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.3.tgz", @@ -15148,6 +15160,15 @@ "jsx-ast-utils": "^2.2.1" } }, + "eslint-plugin-prettier": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.3.0.tgz", + "integrity": "sha512-tMTwO8iUWlSRZIwS9k7/E4vrTsfvsrcM5p1eftyuqWH25nKsz/o6/54I7jwQ/3zobISyC7wMy9ZsFwgTxOcOpQ==", + "dev": true, + "requires": { + "prettier-linter-helpers": "^1.0.0" + } + }, "eslint-plugin-react": { "version": "7.18.3", "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.18.3.tgz", @@ -15735,6 +15756,12 @@ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" }, + "fast-diff": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", + "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", + "dev": true + }, "fast-glob": { "version": "2.2.7", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-2.2.7.tgz", @@ -31208,11 +31235,20 @@ "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=" }, "prettier": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz", - "integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.2.1.tgz", + "integrity": "sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q==", "dev": true }, + "prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "requires": { + "fast-diff": "^1.1.2" + } + }, "pretty-bytes": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-3.0.1.tgz", diff --git a/package.json b/package.json index dff0d41522..579f564a1a 100644 --- a/package.json +++ b/package.json @@ -106,8 +106,10 @@ "cssnano": "^4.1.10", "eslint": "^4.19.1", "eslint-config-airbnb": "^16.1.0", + "eslint-config-prettier": "^7.0.0", "eslint-plugin-import": "^2.20.2", "eslint-plugin-jsx-a11y": "^6.2.3", + "eslint-plugin-prettier": "^3.3.0", "eslint-plugin-react": "^7.18.3", "file-loader": "^2.0.0", "husky": "^4.2.5", @@ -121,6 +123,7 @@ "postcss-focus": "^4.0.0", "postcss-loader": "^3.0.0", "postcss-reporter": "^6.0.1", + "prettier": "^2.2.1", "react-test-renderer": "^16.12.0", "rimraf": "^2.7.1", "sass-loader": "^6.0.7", From 9ea8081496425b730065f2f63427cea679f06ce8 Mon Sep 17 00:00:00 2001 From: Cassie Tarakajian <ctarakajian@gmail.com> Date: Wed, 27 Jan 2021 12:42:51 -0500 Subject: [PATCH 2/4] [#861] Add .prettierrc to match ESLint config --- .prettierrc | 18 + client/common/Button.jsx | 74 +++- client/common/Button.stories.jsx | 41 +- client/common/icons.jsx | 29 +- client/common/icons.stories.jsx | 4 +- client/components/AddRemoveButton.jsx | 6 +- client/components/Dropdown.jsx | 65 +-- client/components/Nav.jsx | 307 +++++++++----- client/components/NavBasic.jsx | 23 +- client/components/OverlayManager.jsx | 3 +- client/components/PreviewNav.jsx | 31 +- client/components/__test__/Nav.test.jsx | 4 +- .../components/createRedirectWithUsername.jsx | 6 +- client/components/forceProtocol.jsx | 28 +- client/components/mobile/ActionStrip.jsx | 40 +- client/components/mobile/Explorer.jsx | 7 +- client/components/mobile/FloatingNav.jsx | 28 +- client/components/mobile/Footer.jsx | 5 +- client/components/mobile/Header.jsx | 70 +-- client/components/mobile/IDEWrapper.jsx | 4 +- client/components/mobile/IconButton.jsx | 24 +- client/components/mobile/MobileScreen.jsx | 9 +- client/components/mobile/PreferencePicker.jsx | 38 +- client/components/mobile/Sidebar.jsx | 22 +- client/components/mobile/Tab.jsx | 9 +- client/components/mobile/TabSwitcher.jsx | 10 +- client/components/useAsModal.jsx | 18 +- client/i18n-test.js | 27 +- client/i18n.js | 11 +- client/index.jsx | 2 +- client/modules/App/App.jsx | 14 +- client/modules/App/components/DevTools.jsx | 5 +- client/modules/App/components/Overlay.jsx | 27 +- .../modules/App/components/ThemeProvider.jsx | 9 +- client/modules/IDE/actions/assets.js | 6 +- client/modules/IDE/actions/collections.js | 21 +- client/modules/IDE/actions/files.js | 85 ++-- client/modules/IDE/actions/ide.js | 6 +- client/modules/IDE/actions/preferences.js | 7 +- client/modules/IDE/actions/project.js | 137 ++++-- client/modules/IDE/actions/projects.js | 3 +- client/modules/IDE/actions/sorting.js | 2 +- client/modules/IDE/actions/uploader.js | 48 ++- client/modules/IDE/components/About.jsx | 46 +- .../IDE/components/AddToCollectionList.jsx | 53 ++- .../components/AddToCollectionSketchList.jsx | 63 ++- client/modules/IDE/components/AssetList.jsx | 69 +-- client/modules/IDE/components/AssetSize.jsx | 4 +- .../CollectionList/CollectionList.jsx | 185 +++++--- .../CollectionList/CollectionListRow.jsx | 150 ++++--- client/modules/IDE/components/Console.jsx | 82 ++-- .../modules/IDE/components/CopyableInput.jsx | 27 +- .../modules/IDE/components/EditableInput.jsx | 7 +- client/modules/IDE/components/Editor.jsx | 181 +++++--- .../IDE/components/EditorAccessibility.jsx | 43 +- client/modules/IDE/components/ErrorModal.jsx | 21 +- client/modules/IDE/components/Feedback.jsx | 20 +- client/modules/IDE/components/FileNode.jsx | 172 +++++--- .../modules/IDE/components/FileNode.test.jsx | 18 +- .../modules/IDE/components/FileUploader.jsx | 19 +- .../IDE/components/KeyboardShortcutModal.jsx | 49 ++- client/modules/IDE/components/NewFileForm.jsx | 23 +- .../modules/IDE/components/NewFileModal.jsx | 20 +- .../modules/IDE/components/NewFolderForm.jsx | 24 +- .../modules/IDE/components/NewFolderModal.jsx | 11 +- .../Preferences/PreferenceCreators.jsx | 29 +- .../IDE/components/Preferences/index.jsx | 168 ++++++-- .../modules/IDE/components/PreviewFrame.jsx | 134 ++++-- .../IDE/components/QuickAddList/Icons.jsx | 27 +- .../components/QuickAddList/QuickAddList.jsx | 47 +- .../IDE/components/Searchbar/Collection.jsx | 7 +- .../IDE/components/Searchbar/Searchbar.jsx | 20 +- .../IDE/components/Searchbar/Sketch.jsx | 6 +- client/modules/IDE/components/ShareModal.jsx | 10 +- client/modules/IDE/components/Sidebar.jsx | 42 +- client/modules/IDE/components/SketchList.jsx | 384 ++++++++++------- client/modules/IDE/components/Toast.jsx | 10 +- client/modules/IDE/components/Toolbar.jsx | 38 +- .../modules/IDE/components/Toolbar.test.jsx | 73 ++-- .../IDE/components/UploadFileModal.jsx | 32 +- .../IDE/hooks/useHandleMessageEvent.js | 12 +- client/modules/IDE/pages/FullView.jsx | 20 +- client/modules/IDE/pages/IDEView.jsx | 91 ++-- client/modules/IDE/pages/MobileIDEView.jsx | 291 +++++++++---- client/modules/IDE/reducers/assets.js | 2 +- .../IDE/reducers/editorAccessibility.js | 7 +- client/modules/IDE/reducers/files.js | 54 ++- client/modules/IDE/reducers/ide.js | 17 +- client/modules/IDE/reducers/preferences.js | 5 +- client/modules/IDE/reducers/project.js | 3 +- client/modules/IDE/reducers/projects.js | 3 +- client/modules/IDE/reducers/sorting.js | 6 +- client/modules/IDE/selectors/collections.js | 18 +- client/modules/IDE/selectors/projects.js | 17 +- client/modules/IDE/selectors/users.js | 10 +- client/modules/Mobile/MobileDashboardView.jsx | 153 ++++--- client/modules/Mobile/MobilePreferences.jsx | 117 +++-- client/modules/Mobile/MobileSketchView.jsx | 58 ++- client/modules/Mobile/MobileViewContent.jsx | 3 +- client/modules/User/actions.js | 160 ++++--- client/modules/User/components/APIKeyForm.jsx | 58 ++- client/modules/User/components/APIKeyList.jsx | 6 +- .../modules/User/components/AccountForm.jsx | 92 ++-- client/modules/User/components/Collection.jsx | 264 ++++++++---- .../User/components/CollectionCreate.jsx | 42 +- .../User/components/DashboardTabSwitcher.jsx | 42 +- client/modules/User/components/LoginForm.jsx | 27 +- .../User/components/NewPasswordForm.jsx | 27 +- .../User/components/ResetPasswordForm.jsx | 26 +- .../User/components/ResponsiveForm.jsx | 31 +- client/modules/User/components/SignupForm.jsx | 52 ++- .../User/components/SocialAuthButton.jsx | 16 +- .../components/SocialAuthButton.stories.jsx | 8 +- client/modules/User/pages/AccountView.jsx | 53 ++- client/modules/User/pages/CollectionView.jsx | 12 +- client/modules/User/pages/DashboardView.jsx | 64 +-- .../User/pages/EmailVerificationView.jsx | 49 +-- client/modules/User/pages/LoginView.jsx | 9 +- client/modules/User/pages/NewPasswordView.jsx | 12 +- .../modules/User/pages/ResetPasswordView.jsx | 18 +- client/modules/User/pages/SignupView.jsx | 9 +- client/modules/User/reducers.js | 12 +- client/routes.jsx | 83 +++- client/store.js | 11 +- client/test-utils.js | 7 +- client/theme.js | 55 ++- client/utils/auth.js | 9 +- client/utils/consoleUtils.js | 4 +- client/utils/custom-hooks.js | 25 +- client/utils/generateRandomName.js | 15 +- client/utils/getConfig.js | 3 +- client/utils/isSecurePage.js | 5 +- client/utils/metaKey.js | 9 +- client/utils/reduxFormUtils.js | 11 +- client/utils/responsive.jsx | 16 +- server/config/passport.js | 344 ++++++++------- server/controllers/aws.controller.js | 104 +++-- .../addProjectToCollection.js | 31 +- .../collectionForUserExists.js | 11 +- .../collection.controller/createCollection.js | 22 +- .../collection.controller/listCollections.js | 23 +- .../collection.controller/removeCollection.js | 6 +- .../removeProjectFromCollection.js | 24 +- .../collection.controller/updateCollection.js | 38 +- server/controllers/embed.controller.js | 49 +-- server/controllers/file.controller.js | 104 +++-- server/controllers/project.controller.js | 153 ++++--- .../__test__/createProject.test.js | 65 ++- .../__test__/deleteProject.test.js | 21 +- .../__test__/getProjectsForUser.test.js | 33 +- .../project.controller/createProject.js | 29 +- .../project.controller/deleteProject.js | 44 +- .../project.controller/getProjectsForUser.js | 12 +- server/controllers/session.controller.js | 12 +- server/controllers/user.controller.js | 228 ++++++---- .../user.controller/__tests__/apiKey.test.js | 46 +- server/controllers/user.controller/apiKey.js | 29 +- server/domain-objects/Project.js | 22 +- .../domain-objects/__test__/Project.test.js | 64 +-- server/domain-objects/createDefaultFiles.js | 6 +- server/migrations/emailConsolidation.js | 88 ++-- server/models/__mocks__/project.js | 4 +- server/models/__mocks__/user.js | 3 +- server/models/__test__/project.test.js | 6 +- server/models/collection.js | 2 +- server/models/project.js | 11 +- server/models/user.js | 182 +++++--- server/routes/aws.routes.js | 12 +- server/routes/collection.routes.js | 36 +- server/routes/file.routes.js | 12 +- server/routes/passport.routes.js | 60 +-- server/routes/project.routes.js | 12 +- server/routes/server.routes.js | 81 ++-- server/routes/user.routes.js | 6 +- server/scripts/examples-gg-latest.js | 226 +++++----- server/scripts/examples-ml5.js | 69 +-- server/scripts/examples.js | 401 ++++++++++-------- server/scripts/update-syntax-highlighting.js | 25 +- server/server.js | 97 +++-- server/utils/filePath.js | 10 +- server/utils/fileUtils.js | 66 ++- server/utils/isAuthenticated.js | 1 - server/utils/mail.js | 13 +- server/utils/previewGeneration.js | 23 +- server/utils/requestsOfType.js | 14 +- server/views/404Page.js | 49 ++- server/views/consolidationMailLayout.js | 5 +- server/views/mail.js | 42 +- server/views/mailLayout.js | 5 +- 189 files changed, 5546 insertions(+), 3411 deletions(-) create mode 100644 .prettierrc diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000000..51e47c9b88 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,18 @@ +{ + "arrowParens": "always", + "bracketSpacing": true, + "htmlWhitespaceSensitivity": "css", + "insertPragma": false, + "jsxBracketSameLine": false, + "jsxSingleQuote": false, + "parser": "babel", + "printWidth": 80, + "proseWrap": "never", + "requirePragma": false, + "semi": true, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "none", + "useTabs": false, + "quoteProps": "as-needed" + } \ No newline at end of file diff --git a/client/common/Button.jsx b/client/common/Button.jsx index 20bd9c2537..09815ff997 100644 --- a/client/common/Button.jsx +++ b/client/common/Button.jsx @@ -8,7 +8,7 @@ import { remSize, prop } from '../theme'; const kinds = { block: 'block', icon: 'icon', - inline: 'inline', + inline: 'inline' }; // The '&&&' will increase the specificity of the @@ -34,7 +34,7 @@ const StyledButton = styled.button` svg * { fill: ${prop('Button.default.foreground')}; } - + &:hover:not(:disabled) { color: ${prop('Button.hover.foreground')}; background-color: ${prop('Button.hover.background')}; @@ -115,7 +115,7 @@ const StyledIconButton = styled.button` border-radius: 50%; padding: ${remSize(8)} ${remSize(25)}; line-height: 1; - + &:hover:not(:disabled) { color: ${prop('Button.hover.foreground')}; background-color: ${prop('Button.hover.background')}; @@ -150,10 +150,24 @@ const StyledIconButton = styled.button` * A Button performs an primary action */ const Button = ({ - children, href, kind, iconBefore, iconAfter, 'aria-label': ariaLabel, to, type, ...props + children, + href, + kind, + iconBefore, + iconAfter, + 'aria-label': ariaLabel, + to, + type, + ...props }) => { const hasChildren = React.Children.count(children) > 0; - const content = <>{iconBefore}{hasChildren && <span>{children}</span>}{iconAfter}</>; + const content = ( + <> + {iconBefore} + {hasChildren && <span>{children}</span>} + {iconAfter} + </> + ); let StyledComponent = StyledButton; if (kind === kinds.inline) { @@ -177,22 +191,36 @@ const Button = ({ } if (to) { - return <StyledComponent kind={kind} as={Link} aria-label={ariaLabel} to={to} {...props}>{content}</StyledComponent>; + return ( + <StyledComponent + kind={kind} + as={Link} + aria-label={ariaLabel} + to={to} + {...props} + > + {content} + </StyledComponent> + ); } - return <StyledComponent kind={kind} aria-label={ariaLabel} type={type} {...props}>{content}</StyledComponent>; + return ( + <StyledComponent kind={kind} aria-label={ariaLabel} type={type} {...props}> + {content} + </StyledComponent> + ); }; Button.defaultProps = { - 'children': null, - 'disabled': false, - 'iconAfter': null, - 'iconBefore': null, - 'kind': kinds.block, - 'href': null, + "children": null, + "disabled": false, + "iconAfter": null, + "iconBefore": null, + "kind": kinds.block, + "href": null, 'aria-label': null, - 'to': null, - 'type': 'button', + "to": null, + "type": 'button' }; Button.kinds = kinds; @@ -202,27 +230,27 @@ Button.propTypes = { * The visible part of the button, telling the user what * the action is */ - 'children': PropTypes.element, + "children": PropTypes.element, /** If the button can be activated or not */ - 'disabled': PropTypes.bool, + "disabled": PropTypes.bool, /** * SVG icon to place after child content */ - 'iconAfter': PropTypes.element, + "iconAfter": PropTypes.element, /** * SVG icon to place before child content */ - 'iconBefore': PropTypes.element, + "iconBefore": PropTypes.element, /** * The kind of button - determines how it appears visually */ - 'kind': PropTypes.oneOf(Object.values(kinds)), + "kind": PropTypes.oneOf(Object.values(kinds)), /** * Specifying an href will use an <a> to link to the URL */ - 'href': PropTypes.string, + "href": PropTypes.string, /* * An ARIA Label used for accessibility */ @@ -230,11 +258,11 @@ Button.propTypes = { /** * Specifying a to URL will use a react-router Link */ - 'to': PropTypes.string, + "to": PropTypes.string, /** * If using a button, then type is defines the type of button */ - 'type': PropTypes.oneOf(['button', 'submit']), + "type": PropTypes.oneOf(['button', 'submit']) }; export default Button; diff --git a/client/common/Button.stories.jsx b/client/common/Button.stories.jsx index bf1a02c8ac..a583d8b46d 100644 --- a/client/common/Button.stories.jsx +++ b/client/common/Button.stories.jsx @@ -21,42 +21,45 @@ export const AllFeatures = () => ( ); export const SubmitButton = () => ( - <Button type="submit" label="submit">This is a submit button</Button> + <Button type="submit" label="submit"> + This is a submit button + </Button> ); -export const DefaultTypeButton = () => <Button label="login" onClick={action('onClick')}>Log In</Button>; +export const DefaultTypeButton = () => ( + <Button label="login" onClick={action('onClick')}> + Log In + </Button> +); -export const DisabledButton = () => <Button disabled label="login" onClick={action('onClick')}>Log In</Button>; +export const DisabledButton = () => ( + <Button disabled label="login" onClick={action('onClick')}> + Log In + </Button> +); export const AnchorButton = () => ( - <Button href="http://p5js.org" label="submit">Actually an anchor</Button> + <Button href="http://p5js.org" label="submit"> + Actually an anchor + </Button> ); export const ReactRouterLink = () => ( - <Button to="./somewhere" label="submit">Actually a Link</Button> + <Button to="./somewhere" label="submit"> + Actually a Link + </Button> ); export const ButtonWithIconBefore = () => ( - <Button - iconBefore={<GithubIcon aria-label="Github logo" />} - > - Create - </Button> + <Button iconBefore={<GithubIcon aria-label="Github logo" />}>Create</Button> ); export const ButtonWithIconAfter = () => ( - <Button - iconAfter={<GithubIcon aria-label="Github logo" />} - > - Create - </Button> + <Button iconAfter={<GithubIcon aria-label="Github logo" />}>Create</Button> ); export const InlineButtonWithIconAfter = () => ( - <Button - iconAfter={<DropdownArrowIcon />} - kind={Button.kinds.inline} - > + <Button iconAfter={<DropdownArrowIcon />} kind={Button.kinds.inline}> File name </Button> ); diff --git a/client/common/icons.jsx b/client/common/icons.jsx index d4a458bca8..e4bb1d2de1 100644 --- a/client/common/icons.jsx +++ b/client/common/icons.jsx @@ -23,7 +23,6 @@ import CircleTerminal from '../images/circle-terminal.svg'; import CircleFolder from '../images/circle-folder.svg'; import CircleInfo from '../images/circle-info.svg'; - // HOC that adds the right web accessibility props // https://www.scottohara.me/blog/2019/05/22/contextual-images-svgs-and-a11y.html @@ -34,13 +33,17 @@ function withLabel(SvgComponent) { const StyledIcon = styled(SvgComponent)` &&& { color: ${prop('Icon.default')}; - & g, & path, & polygon { + & g, + & path, + & polygon { opacity: 1; fill: ${prop('Icon.default')}; } &:hover { color: ${prop('Icon.hover')}; - & g, & path, & polygon { + & g, + & path, + & polygon { opacity: 1; fill: ${prop('Icon.hover')}; } @@ -50,18 +53,16 @@ function withLabel(SvgComponent) { const { 'aria-label': ariaLabel } = props; if (ariaLabel) { - return (<StyledIcon - {...props} - aria-label={ariaLabel} - role="img" - focusable="false" - />); + return ( + <StyledIcon + {...props} + aria-label={ariaLabel} + role="img" + focusable="false" + /> + ); } - return (<StyledIcon - {...props} - aria-hidden - focusable="false" - />); + return <StyledIcon {...props} aria-hidden focusable="false" />; }; Icon.propTypes = { diff --git a/client/common/icons.stories.jsx b/client/common/icons.stories.jsx index 3c3307ab47..3df22e3196 100644 --- a/client/common/icons.stories.jsx +++ b/client/common/icons.stories.jsx @@ -12,7 +12,5 @@ export const AllIcons = () => { const names = Object.keys(icons); const SelectedIcon = icons[select('name', names, names[0])]; - return ( - <SelectedIcon /> - ); + return <SelectedIcon />; }; diff --git a/client/components/AddRemoveButton.jsx b/client/components/AddRemoveButton.jsx index ea600cf91d..23fb9082f5 100644 --- a/client/components/AddRemoveButton.jsx +++ b/client/components/AddRemoveButton.jsx @@ -2,12 +2,14 @@ import React from 'react'; import PropTypes from 'prop-types'; import { withTranslation } from 'react-i18next'; - import AddIcon from '../images/plus.svg'; import RemoveIcon from '../images/minus.svg'; const AddRemoveButton = ({ type, onClick, t }) => { - const alt = type === 'add' ? t('AddRemoveButton.AltAddARIA') : t('AddRemoveButton.AltRemoveARIA'); + const alt = + type === 'add' + ? t('AddRemoveButton.AltAddARIA') + : t('AddRemoveButton.AltRemoveARIA'); const Icon = type === 'add' ? AddIcon : RemoveIcon; return ( diff --git a/client/components/Dropdown.jsx b/client/components/Dropdown.jsx index 0cd29574ec..1bf7f5d0e4 100644 --- a/client/components/Dropdown.jsx +++ b/client/components/Dropdown.jsx @@ -13,11 +13,11 @@ const DropdownWrapper = styled.ul` color: ${prop('primaryTextColor')}; position: absolute; - right: ${props => (props.right ? 0 : 'initial')}; - left: ${props => (props.left ? 0 : 'initial')}; + right: ${(props) => (props.right ? 0 : 'initial')}; + left: ${(props) => (props.left ? 0 : 'initial')}; - ${props => (props.align === 'right' && 'right: 0;')} - ${props => (props.align === 'left' && 'left: 0;')} + ${(props) => props.align === 'right' && 'right: 0;'} + ${(props) => props.align === 'left' && 'left: 0;'} text-align: left; @@ -28,15 +28,20 @@ const DropdownWrapper = styled.ul` z-index: 2; border-radius: ${remSize(6)}; - & li:first-child { border-radius: ${remSize(5)} ${remSize(5)} 0 0; } - & li:last-child { border-radius: 0 0 ${remSize(5)} ${remSize(5)}; } + & li:first-child { + border-radius: ${remSize(5)} ${remSize(5)} 0 0; + } + & li:last-child { + border-radius: 0 0 ${remSize(5)} ${remSize(5)}; + } & li:hover { - background-color: ${prop('Button.hover.background')}; color: ${prop('Button.hover.foreground')}; - * { color: ${prop('Button.hover.foreground')}; } + * { + color: ${prop('Button.hover.foreground')}; + } } li { @@ -60,7 +65,9 @@ const DropdownWrapper = styled.ul` justify-content: flex-start; } - & button span { padding: 0px } + & button span { + padding: 0px; + } } `; @@ -68,32 +75,32 @@ const DropdownWrapper = styled.ul` // const MaybeIcon = (Element, label) => Element && <Element aria-label={label} />; const Dropdown = ({ items, align }) => ( - <DropdownWrapper align={align} > + <DropdownWrapper align={align}> {/* className="nav__items-left" */} - {items && items.map(({ - title, icon, href, action - }) => ( - <li key={`nav-${title && title.toLowerCase()}`}> - {/* {MaybeIcon(icon, `Navigate to ${title}`)} */} - {href - ? <IconButton to={href}>{title}</IconButton> - : <IconButton onClick={() => action()}>{title}</IconButton>} - - </li> - )) - } + {items && + items.map(({ title, icon, href, action }) => ( + <li key={`nav-${title && title.toLowerCase()}`}> + {/* {MaybeIcon(icon, `Navigate to ${title}`)} */} + {href ? ( + <IconButton to={href}>{title}</IconButton> + ) : ( + <IconButton onClick={() => action()}>{title}</IconButton> + )} + </li> + ))} </DropdownWrapper> ); - Dropdown.propTypes = { align: PropTypes.oneOf(['left', 'right']), - items: PropTypes.arrayOf(PropTypes.shape({ - action: PropTypes.func, - icon: PropTypes.func, - href: PropTypes.string, - title: PropTypes.string - })), + items: PropTypes.arrayOf( + PropTypes.shape({ + action: PropTypes.func, + icon: PropTypes.func, + href: PropTypes.string, + title: PropTypes.string + }) + ) }; Dropdown.defaultProps = { diff --git a/client/components/Nav.jsx b/client/components/Nav.jsx index 993a9ff828..1bb435e72e 100644 --- a/client/components/Nav.jsx +++ b/client/components/Nav.jsx @@ -9,7 +9,10 @@ import { languageKeyToLabel } from '../i18n'; import * as IDEActions from '../modules/IDE/actions/ide'; import * as toastActions from '../modules/IDE/actions/toast'; import * as projectActions from '../modules/IDE/actions/project'; -import { setAllAccessibleOutput, setLanguage } from '../modules/IDE/actions/preferences'; +import { + setAllAccessibleOutput, + setLanguage +} from '../modules/IDE/actions/preferences'; import { logoutUser } from '../modules/User/actions'; import getConfig from '../utils/getConfig'; @@ -200,7 +203,11 @@ class Nav extends React.PureComponent { handleShare() { const { username } = this.props.params; - this.props.showShareModal(this.props.project.id, this.props.project.name, username); + this.props.showShareModal( + this.props.project.id, + this.props.project.name, + username + ); this.setDropdown('none'); } @@ -242,11 +249,20 @@ class Nav extends React.PureComponent { return ( <ul className="nav__items-left"> <li className="nav__item-logo"> - <LogoIcon role="img" aria-label={this.props.t('Common.p5logoARIA')} focusable="false" className="svg__logo" /> + <LogoIcon + role="img" + aria-label={this.props.t('Common.p5logoARIA')} + focusable="false" + className="svg__logo" + /> </li> <li className="nav__item nav__item--no-icon"> <Link to="/" className="nav__back-link"> - <CaretLeftIcon className="nav__back-icon" focusable="false" aria-hidden="true" /> + <CaretLeftIcon + className="nav__back-icon" + focusable="false" + aria-hidden="true" + /> <span className="nav__item-header"> {this.props.t('Nav.BackEditor')} </span> @@ -257,11 +273,17 @@ class Nav extends React.PureComponent { } renderProjectMenu(navDropdownState) { - const replaceCommand = metaKey === 'Ctrl' ? `${metaKeyName}+H` : `${metaKeyName}+⌥+F`; + const replaceCommand = + metaKey === 'Ctrl' ? `${metaKeyName}+H` : `${metaKeyName}+⌥+F`; return ( <ul className="nav__items-left"> <li className="nav__item-logo"> - <LogoIcon role="img" aria-label={this.props.t('Common.p5logoARIA')} focusable="false" className="svg__logo" /> + <LogoIcon + role="img" + aria-label={this.props.t('Common.p5logoARIA')} + focusable="false" + className="svg__logo" + /> </li> <li className={navDropdownState.file}> <button @@ -274,8 +296,14 @@ class Nav extends React.PureComponent { } }} > - <span className="nav__item-header">{this.props.t('Nav.File.Title')}</span> - <TriangleIcon className="nav__item-header-triangle" focusable="false" aria-hidden="true" /> + <span className="nav__item-header"> + {this.props.t('Nav.File.Title')} + </span> + <TriangleIcon + className="nav__item-header-triangle" + focusable="false" + aria-hidden="true" + /> </button> <ul className="nav__dropdown"> <li className="nav__dropdown-item"> @@ -287,61 +315,69 @@ class Nav extends React.PureComponent { {this.props.t('Nav.File.New')} </button> </li> - { getConfig('LOGIN_ENABLED') && (!this.props.project.owner || this.props.isUserOwner) && - <li className="nav__dropdown-item"> - <button - onClick={this.handleSave} - onFocus={this.handleFocusForFile} - onBlur={this.handleBlur} - > - {this.props.t('Common.Save')} - <span className="nav__keyboard-shortcut">{metaKeyName}+S</span> - </button> - </li> } - { this.props.project.id && this.props.user.authenticated && - <li className="nav__dropdown-item"> - <button - onClick={this.handleDuplicate} - onFocus={this.handleFocusForFile} - onBlur={this.handleBlur} - > - {this.props.t('Nav.File.Duplicate')} - </button> - </li> } - { this.props.project.id && - <li className="nav__dropdown-item"> - <button - onClick={this.handleShare} - onFocus={this.handleFocusForFile} - onBlur={this.handleBlur} - > - {this.props.t('Nav.File.Share')} - </button> - </li> } - { this.props.project.id && - <li className="nav__dropdown-item"> - <button - onClick={this.handleDownload} - onFocus={this.handleFocusForFile} - onBlur={this.handleBlur} - > - {this.props.t('Nav.File.Download')} - </button> - </li> } - { this.props.user.authenticated && - <li className="nav__dropdown-item"> - <Link - to={`/${this.props.user.username}/sketches`} - onFocus={this.handleFocusForFile} - onBlur={this.handleBlur} - onClick={this.setDropdownForNone} - > - {this.props.t('Nav.File.Open')} - </Link> - </li> } + {getConfig('LOGIN_ENABLED') && + (!this.props.project.owner || this.props.isUserOwner) && ( + <li className="nav__dropdown-item"> + <button + onClick={this.handleSave} + onFocus={this.handleFocusForFile} + onBlur={this.handleBlur} + > + {this.props.t('Common.Save')} + <span className="nav__keyboard-shortcut"> + {metaKeyName}+S + </span> + </button> + </li> + )} + {this.props.project.id && this.props.user.authenticated && ( + <li className="nav__dropdown-item"> + <button + onClick={this.handleDuplicate} + onFocus={this.handleFocusForFile} + onBlur={this.handleBlur} + > + {this.props.t('Nav.File.Duplicate')} + </button> + </li> + )} + {this.props.project.id && ( + <li className="nav__dropdown-item"> + <button + onClick={this.handleShare} + onFocus={this.handleFocusForFile} + onBlur={this.handleBlur} + > + {this.props.t('Nav.File.Share')} + </button> + </li> + )} + {this.props.project.id && ( + <li className="nav__dropdown-item"> + <button + onClick={this.handleDownload} + onFocus={this.handleFocusForFile} + onBlur={this.handleBlur} + > + {this.props.t('Nav.File.Download')} + </button> + </li> + )} + {this.props.user.authenticated && ( + <li className="nav__dropdown-item"> + <Link + to={`/${this.props.user.username}/sketches`} + onFocus={this.handleFocusForFile} + onBlur={this.handleBlur} + onClick={this.setDropdownForNone} + > + {this.props.t('Nav.File.Open')} + </Link> + </li> + )} {getConfig('UI_COLLECTIONS_ENABLED') && this.props.user.authenticated && - this.props.project.id && + this.props.project.id && ( <li className="nav__dropdown-item"> <Link to={`/${this.props.user.username}/sketches/${this.props.project.id}/add-to-collection`} @@ -351,18 +387,20 @@ class Nav extends React.PureComponent { > {this.props.t('Nav.File.AddToCollection')} </Link> - </li>} - { getConfig('EXAMPLES_ENABLED') && - <li className="nav__dropdown-item"> - <Link - to="/p5/sketches" - onFocus={this.handleFocusForFile} - onBlur={this.handleBlur} - onClick={this.setDropdownForNone} - > - {this.props.t('Nav.File.Examples')} - </Link> - </li> } + </li> + )} + {getConfig('EXAMPLES_ENABLED') && ( + <li className="nav__dropdown-item"> + <Link + to="/p5/sketches" + onFocus={this.handleFocusForFile} + onBlur={this.handleBlur} + onClick={this.setDropdownForNone} + > + {this.props.t('Nav.File.Examples')} + </Link> + </li> + )} </ul> </li> <li className={navDropdownState.edit}> @@ -376,10 +414,16 @@ class Nav extends React.PureComponent { } }} > - <span className="nav__item-header">{this.props.t('Nav.Edit.Title')}</span> - <TriangleIcon className="nav__item-header-triangle" focusable="false" aria-hidden="true" /> + <span className="nav__item-header"> + {this.props.t('Nav.Edit.Title')} + </span> + <TriangleIcon + className="nav__item-header-triangle" + focusable="false" + aria-hidden="true" + /> </button> - <ul className="nav__dropdown" > + <ul className="nav__dropdown"> <li className="nav__dropdown-item"> <button onClick={() => { @@ -420,7 +464,9 @@ class Nav extends React.PureComponent { onBlur={this.handleBlur} > {this.props.t('Nav.Edit.FindPrevious')} - <span className="nav__keyboard-shortcut">{'\u21E7'}+{metaKeyName}+G</span> + <span className="nav__keyboard-shortcut"> + {'\u21E7'}+{metaKeyName}+G + </span> </button> </li> <li className="nav__dropdown-item"> @@ -446,8 +492,14 @@ class Nav extends React.PureComponent { } }} > - <span className="nav__item-header">{this.props.t('Nav.Sketch.Title')}</span> - <TriangleIcon className="nav__item-header-triangle" focusable="false" aria-hidden="true" /> + <span className="nav__item-header"> + {this.props.t('Nav.Sketch.Title')} + </span> + <TriangleIcon + className="nav__item-header-triangle" + focusable="false" + aria-hidden="true" + /> </button> <ul className="nav__dropdown"> <li className="nav__dropdown-item"> @@ -475,7 +527,9 @@ class Nav extends React.PureComponent { onBlur={this.handleBlur} > {this.props.t('Nav.Sketch.Run')} - <span className="nav__keyboard-shortcut">{metaKeyName}+Enter</span> + <span className="nav__keyboard-shortcut"> + {metaKeyName}+Enter + </span> </button> </li> <li className="nav__dropdown-item"> @@ -485,7 +539,9 @@ class Nav extends React.PureComponent { onBlur={this.handleBlur} > {this.props.t('Nav.Sketch.Stop')} - <span className="nav__keyboard-shortcut">{'\u21E7'}+{metaKeyName}+Enter</span> + <span className="nav__keyboard-shortcut"> + {'\u21E7'}+{metaKeyName}+Enter + </span> </button> </li> {/* <li className="nav__dropdown-item"> @@ -521,8 +577,14 @@ class Nav extends React.PureComponent { } }} > - <span className="nav__item-header">{this.props.t('Nav.Help.Title')}</span> - <TriangleIcon className="nav__item-header-triangle" focusable="false" aria-hidden="true" /> + <span className="nav__item-header"> + {this.props.t('Nav.Help.Title')} + </span> + <TriangleIcon + className="nav__item-header-triangle" + focusable="false" + aria-hidden="true" + /> </button> <ul className="nav__dropdown"> <li className="nav__dropdown-item"> @@ -542,7 +604,8 @@ class Nav extends React.PureComponent { onFocus={this.handleFocusForHelp} onBlur={this.handleBlur} onClick={this.setDropdownForNone} - >{this.props.t('Nav.Help.Reference')} + > + {this.props.t('Nav.Help.Reference')} </a> </li> <li className="nav__dropdown-item"> @@ -575,18 +638,25 @@ class Nav extends React.PureComponent { } }} > - <span className="nav__item-header"> {languageKeyToLabel(this.props.language)}</span> - <TriangleIcon className="nav__item-header-triangle" focusable="false" aria-hidden="true" /> + <span className="nav__item-header"> + {' '} + {languageKeyToLabel(this.props.language)} + </span> + <TriangleIcon + className="nav__item-header-triangle" + focusable="false" + aria-hidden="true" + /> </button> <ul className="nav__dropdown"> - <li className="nav__dropdown-item"> <button onFocus={this.handleFocusForLang} onBlur={this.handleBlur} value="en-US" - onClick={e => this.handleLangSelection(e)} - >English + onClick={(e) => this.handleLangSelection(e)} + > + English </button> </li> <li className="nav__dropdown-item"> @@ -594,7 +664,7 @@ class Nav extends React.PureComponent { onFocus={this.handleFocusForLang} onBlur={this.handleBlur} value="es-419" - onClick={e => this.handleLangSelection(e)} + onClick={(e) => this.handleLangSelection(e)} > Español </button> @@ -604,7 +674,7 @@ class Nav extends React.PureComponent { onFocus={this.handleFocusForLang} onBlur={this.handleBlur} value="ja" - onClick={e => this.handleLangSelection(e)} + onClick={(e) => this.handleLangSelection(e)} > 日本語 </button> @@ -615,20 +685,24 @@ class Nav extends React.PureComponent { ); } - renderUnauthenticatedUserMenu(navDropdownState) { return ( <ul className="nav__items-right" title="user-menu"> - {getConfig('TRANSLATIONS_ENABLED') && this.renderLanguageMenu(navDropdownState)} + {getConfig('TRANSLATIONS_ENABLED') && + this.renderLanguageMenu(navDropdownState)} <li className="nav__item"> <Link to="/login" className="nav__auth-button"> - <span className="nav__item-header">{this.props.t('Nav.Login')}</span> + <span className="nav__item-header"> + {this.props.t('Nav.Login')} + </span> </Link> </li> <span className="nav__item-or">{this.props.t('Nav.LoginOr')}</span> <li className="nav__item"> <Link to="/signup" className="nav__auth-button"> - <span className="nav__item-header">{this.props.t('Nav.SignUp')}</span> + <span className="nav__item-header"> + {this.props.t('Nav.SignUp')} + </span> </Link> </li> </ul> @@ -638,7 +712,8 @@ class Nav extends React.PureComponent { renderAuthenticatedUserMenu(navDropdownState) { return ( <ul className="nav__items-right" title="user-menu"> - {getConfig('TRANSLATIONS_ENABLED') && this.renderLanguageMenu(navDropdownState)} + {getConfig('TRANSLATIONS_ENABLED') && + this.renderLanguageMenu(navDropdownState)} <li className={navDropdownState.account}> <button className="nav__item-header" @@ -651,8 +726,14 @@ class Nav extends React.PureComponent { } }} > - <span>{this.props.t('Nav.Auth.Hello')}, {this.props.user.username}!</span> - <TriangleIcon className="nav__item-header-triangle" focusable="false" aria-hidden="true" /> + <span> + {this.props.t('Nav.Auth.Hello')}, {this.props.user.username}! + </span> + <TriangleIcon + className="nav__item-header-triangle" + focusable="false" + aria-hidden="true" + /> </button> <ul className="nav__dropdown"> <li className="nav__dropdown-item"> @@ -665,7 +746,7 @@ class Nav extends React.PureComponent { {this.props.t('Nav.Auth.MySketches')} </Link> </li> - {getConfig('UI_COLLECTIONS_ENABLED') && + {getConfig('UI_COLLECTIONS_ENABLED') && ( <li className="nav__dropdown-item"> <Link to={`/${this.props.user.username}/collections`} @@ -676,7 +757,7 @@ class Nav extends React.PureComponent { {this.props.t('Nav.Auth.MyCollections')} </Link> </li> - } + )} <li className="nav__dropdown-item"> <Link to={`/${this.props.user.username}/assets`} @@ -738,34 +819,40 @@ class Nav extends React.PureComponent { render() { const navDropdownState = { file: classNames({ - 'nav__item': true, + "nav__item": true, 'nav__item--open': this.state.dropdownOpen === 'file' }), edit: classNames({ - 'nav__item': true, + "nav__item": true, 'nav__item--open': this.state.dropdownOpen === 'edit' }), sketch: classNames({ - 'nav__item': true, + "nav__item": true, 'nav__item--open': this.state.dropdownOpen === 'sketch' }), help: classNames({ - 'nav__item': true, + "nav__item": true, 'nav__item--open': this.state.dropdownOpen === 'help' }), account: classNames({ - 'nav__item': true, + "nav__item": true, 'nav__item--open': this.state.dropdownOpen === 'account' }), lang: classNames({ - 'nav__item': true, + "nav__item": true, 'nav__item--open': this.state.dropdownOpen === 'lang' }) }; return ( <header> - <nav className="nav" title="main-navigation" ref={(node) => { this.node = node; }}> + <nav + className="nav" + title="main-navigation" + ref={(node) => { + this.node = node; + }} + > {this.renderLeftLayout(navDropdownState)} {this.renderUserMenu(navDropdownState)} </nav> @@ -843,7 +930,7 @@ function mapStateToProps(state) { project: state.project, user: state.user, unsavedChanges: state.ide.unsavedChanges, - rootFile: state.files.filter(file => file.name === 'root')[0], + rootFile: state.files.filter((file) => file.name === 'root')[0], language: state.preferences.language, isUserOwner: getIsUserOwner(state) }; @@ -858,5 +945,7 @@ const mapDispatchToProps = { setLanguage }; -export default withTranslation()(withRouter(connect(mapStateToProps, mapDispatchToProps)(Nav))); +export default withTranslation()( + withRouter(connect(mapStateToProps, mapDispatchToProps)(Nav)) +); export { Nav as NavComponent }; diff --git a/client/components/NavBasic.jsx b/client/components/NavBasic.jsx index 632fc5a9e3..2eda24dcf6 100644 --- a/client/components/NavBasic.jsx +++ b/client/components/NavBasic.jsx @@ -8,16 +8,27 @@ import ArrowIcon from '../images/triangle-arrow-left.svg'; class NavBasic extends React.PureComponent { static defaultProps = { onBack: null - } + }; render() { return ( - <nav className="nav" title="main-navigation" ref={(node) => { this.node = node; }}> + <nav + className="nav" + title="main-navigation" + ref={(node) => { + this.node = node; + }} + > <ul className="nav__items-left"> <li className="nav__item-logo"> - <LogoIcon role="img" aria-label={this.props.t('Common.p5logoARIA')} focusable="false" className="svg__logo" /> + <LogoIcon + role="img" + aria-label={this.props.t('Common.p5logoARIA')} + focusable="false" + className="svg__logo" + /> </li> - { this.props.onBack && ( + {this.props.onBack && ( <li className="nav__item"> <button onClick={this.props.onBack}> <span className="nav__item-header"> @@ -25,8 +36,8 @@ class NavBasic extends React.PureComponent { </span> Back to the editor </button> - </li>) - } + </li> + )} </ul> </nav> ); diff --git a/client/components/OverlayManager.jsx b/client/components/OverlayManager.jsx index 1b3b040c7f..86a956a407 100644 --- a/client/components/OverlayManager.jsx +++ b/client/components/OverlayManager.jsx @@ -16,10 +16,9 @@ const OverlayManager = ({ overlay, hideOverlay }) => { return jsx && createPortal(jsx, document.body); }; - OverlayManager.propTypes = { overlay: PropTypes.string, - hideOverlay: PropTypes.func.isRequired, + hideOverlay: PropTypes.func.isRequired }; OverlayManager.defaultProps = { overlay: null }; diff --git a/client/components/PreviewNav.jsx b/client/components/PreviewNav.jsx index 8c43005094..a45dea90c2 100644 --- a/client/components/PreviewNav.jsx +++ b/client/components/PreviewNav.jsx @@ -10,15 +10,34 @@ const PreviewNav = ({ owner, project, t }) => ( <nav className="nav preview-nav"> <div className="nav__items-left"> <div className="nav__item-logo"> - <LogoIcon role="img" aria-label={t('Common.p5logoARIA')} focusable="false" className="svg__logo" /> + <LogoIcon + role="img" + aria-label={t('Common.p5logoARIA')} + focusable="false" + className="svg__logo" + /> </div> - <Link className="nav__item" to={`/${owner.username}/sketches/${project.id}`}>{project.name}</Link> + <Link + className="nav__item" + to={`/${owner.username}/sketches/${project.id}`} + > + {project.name} + </Link> <p className="toolbar__project-owner">{t('PreviewNav.ByUser')}</p> - <Link className="nav__item" to={`/${owner.username}/sketches/`}>{owner.username}</Link> + <Link className="nav__item" to={`/${owner.username}/sketches/`}> + {owner.username} + </Link> </div> <div className="nav__items-right"> - <Link to={`/${owner.username}/sketches/${project.id}`} aria-label={t('PreviewNav.EditSketchARIA')} > - <CodeIcon className="preview-nav__editor-svg" focusable="false" aria-hidden="true" /> + <Link + to={`/${owner.username}/sketches/${project.id}`} + aria-label={t('PreviewNav.EditSketchARIA')} + > + <CodeIcon + className="preview-nav__editor-svg" + focusable="false" + aria-hidden="true" + /> </Link> </div> </nav> @@ -30,7 +49,7 @@ PreviewNav.propTypes = { }).isRequired, project: PropTypes.shape({ name: PropTypes.string.isRequired, - id: PropTypes.string.isRequired, + id: PropTypes.string.isRequired }).isRequired, t: PropTypes.func.isRequired }; diff --git a/client/components/__test__/Nav.test.jsx b/client/components/__test__/Nav.test.jsx index e4dd0c0acd..43e888c716 100644 --- a/client/components/__test__/Nav.test.jsx +++ b/client/components/__test__/Nav.test.jsx @@ -1,8 +1,6 @@ - import React from 'react'; import { render } from '@testing-library/react'; - import { NavComponent } from '../Nav'; describe('Nav', () => { @@ -36,7 +34,7 @@ describe('Nav', () => { showFind: jest.fn(), findNext: jest.fn(), findPrev: jest.fn(), - showReplace: jest.fn(), + showReplace: jest.fn() }, startSketch: jest.fn(), stopSketch: jest.fn(), diff --git a/client/components/createRedirectWithUsername.jsx b/client/components/createRedirectWithUsername.jsx index fe76b5cd20..760cd4fcd3 100644 --- a/client/components/createRedirectWithUsername.jsx +++ b/client/components/createRedirectWithUsername.jsx @@ -16,12 +16,14 @@ const RedirectToUser = ({ username, url = '/:username/sketches' }) => { function mapStateToProps(state) { return { - username: state.user ? state.user.username : null, + username: state.user ? state.user.username : null }; } const ConnectedRedirectToUser = connect(mapStateToProps)(RedirectToUser); -const createRedirectWithUsername = url => props => <ConnectedRedirectToUser {...props} url={url} />; +const createRedirectWithUsername = (url) => (props) => ( + <ConnectedRedirectToUser {...props} url={url} /> +); export default createRedirectWithUsername; diff --git a/client/components/forceProtocol.jsx b/client/components/forceProtocol.jsx index 3009d08502..db2c024725 100644 --- a/client/components/forceProtocol.jsx +++ b/client/components/forceProtocol.jsx @@ -1,16 +1,19 @@ import React from 'react'; import { format, parse } from 'url'; -const findCurrentProtocol = () => ( - parse(window.location.href).protocol -); +const findCurrentProtocol = () => parse(window.location.href).protocol; -const redirectToProtocol = (protocol, { appendSource, disable = false } = {}) => { +const redirectToProtocol = ( + protocol, + { appendSource, disable = false } = {} +) => { const currentProtocol = findCurrentProtocol(); if (protocol !== currentProtocol) { if (disable === true) { - console.info(`forceProtocol: would have redirected from "${currentProtocol}" to "${protocol}"`); + console.info( + `forceProtocol: would have redirected from "${currentProtocol}" to "${protocol}"` + ); } else { const url = parse(window.location.href, true /* parse query string */); url.protocol = protocol; @@ -30,9 +33,13 @@ const redirectToProtocol = (protocol, { appendSource, disable = false } = {}) => * disable: if true, the redirection will not happen but what should * have happened will be logged to the console */ -const forceProtocol = ({ targetProtocol = 'https', sourceProtocol, disable = false }) => WrappedComponent => ( +const forceProtocol = ({ + targetProtocol = 'https', + sourceProtocol, + disable = false +}) => (WrappedComponent) => class ForceProtocol extends React.Component { - static propTypes = {} + static propTypes = {}; componentDidMount() { redirectToProtocol(targetProtocol, { appendSource: true, disable }); @@ -47,12 +54,11 @@ const forceProtocol = ({ targetProtocol = 'https', sourceProtocol, disable = fal render() { return <WrappedComponent {...this.props} />; } - } -); + }; const protocols = { http: 'http:', - https: 'https:', + https: 'https:' }; const findSourceProtocol = (state, location) => { @@ -72,5 +78,5 @@ export { findCurrentProtocol, findSourceProtocol, redirectToProtocol, - protocols, + protocols }; diff --git a/client/components/mobile/ActionStrip.jsx b/client/components/mobile/ActionStrip.jsx index 65b7ea8c41..cd46b9fec4 100644 --- a/client/components/mobile/ActionStrip.jsx +++ b/client/components/mobile/ActionStrip.jsx @@ -7,42 +7,50 @@ import IconButton from './IconButton'; const BottomBarContent = styled.div` padding: ${remSize(8)}; display: grid; - grid-template-columns: repeat(8,1fr); + grid-template-columns: repeat(8, 1fr); svg { max-height: ${remSize(32)}; } - path { fill: ${prop('primaryTextColor')} !important } + path { + fill: ${prop('primaryTextColor')} !important; + } .inverted { - path { fill: ${prop('backgroundColor')} !important } - rect { fill: ${prop('primaryTextColor')} !important } + path { + fill: ${prop('backgroundColor')} !important; + } + rect { + fill: ${prop('primaryTextColor')} !important; + } } `; const ActionStrip = ({ actions }) => ( <BottomBarContent> - {actions.map(({ - icon, aria, action, inverted - }) => - (<IconButton + {actions.map(({ icon, aria, action, inverted }) => ( + <IconButton inverted={inverted} className={inverted && 'inverted'} icon={icon} aria-label={aria} key={`bottom-bar-${aria}`} onClick={action} - />))} - </BottomBarContent>); + /> + ))} + </BottomBarContent> +); ActionStrip.propTypes = { - actions: PropTypes.arrayOf(PropTypes.shape({ - icon: PropTypes.component, - aria: PropTypes.string.isRequired, - action: PropTypes.func.isRequired, - inverted: PropTypes.bool - })).isRequired + actions: PropTypes.arrayOf( + PropTypes.shape({ + icon: PropTypes.component, + aria: PropTypes.string.isRequired, + action: PropTypes.func.isRequired, + inverted: PropTypes.bool + }) + ).isRequired }; export default ActionStrip; diff --git a/client/components/mobile/Explorer.jsx b/client/components/mobile/Explorer.jsx index f4d501ceb0..1620eb0d43 100644 --- a/client/components/mobile/Explorer.jsx +++ b/client/components/mobile/Explorer.jsx @@ -5,12 +5,15 @@ import PropTypes from 'prop-types'; import Sidebar from './Sidebar'; import ConnectedFileNode from '../../modules/IDE/components/FileNode'; - const Explorer = ({ id, canEdit, onPressClose }) => { const { t } = useTranslation(); return ( <Sidebar title={t('Explorer.Files')} onPressClose={onPressClose}> - <ConnectedFileNode id={id} canEdit={canEdit} onClickFile={() => onPressClose()} /> + <ConnectedFileNode + id={id} + canEdit={canEdit} + onClickFile={() => onPressClose()} + /> </Sidebar> ); }; diff --git a/client/components/mobile/FloatingNav.jsx b/client/components/mobile/FloatingNav.jsx index de19c4ff0b..2931343f21 100644 --- a/client/components/mobile/FloatingNav.jsx +++ b/client/components/mobile/FloatingNav.jsx @@ -13,27 +13,29 @@ const FloatingContainer = styled.div` text-align: right; z-index: 3; - svg { width: ${remSize(32)}; }; - svg > path { fill: ${prop('Button.default.background')} !important }; + svg { + width: ${remSize(32)}; + } + svg > path { + fill: ${prop('Button.default.background')} !important; + } `; const FloatingNav = ({ items }) => ( <FloatingContainer> - { items.map(({ icon, onPress }) => - ( - <IconButton - onClick={onPress} - icon={icon} - /> - ))} + {items.map(({ icon, onPress }) => ( + <IconButton onClick={onPress} icon={icon} /> + ))} </FloatingContainer> ); FloatingNav.propTypes = { - items: PropTypes.arrayOf(PropTypes.shape({ - icon: PropTypes.element, - onPress: PropTypes.func - })) + items: PropTypes.arrayOf( + PropTypes.shape({ + icon: PropTypes.element, + onPress: PropTypes.func + }) + ) }; FloatingNav.defaultProps = { diff --git a/client/components/mobile/Footer.jsx b/client/components/mobile/Footer.jsx index ff19eee14d..73944925a5 100644 --- a/client/components/mobile/Footer.jsx +++ b/client/components/mobile/Footer.jsx @@ -2,7 +2,6 @@ import React from 'react'; import styled from 'styled-components'; import { prop, grays } from '../../theme'; - const background = prop('MobilePanel.default.background'); const textColor = prop('primaryTextColor'); @@ -13,5 +12,7 @@ export default styled.div` background: ${background}; color: ${textColor}; - & > * + * { border-top: dashed 1px ${prop('Separator')} } + & > * + * { + border-top: dashed 1px ${prop('Separator')}; + } `; diff --git a/client/components/mobile/Header.jsx b/client/components/mobile/Header.jsx index 6492a44d41..2fffdfef53 100644 --- a/client/components/mobile/Header.jsx +++ b/client/components/mobile/Header.jsx @@ -3,22 +3,26 @@ import styled from 'styled-components'; import PropTypes from 'prop-types'; import { prop, remSize } from '../../theme'; - -const background = ({ transparent, inverted }) => prop(transparent === true - ? 'backgroundColor' - : `MobilePanel.default.${inverted === true ? 'foreground' : 'background'}`); - -const textColor = ({ transparent, inverted }) => prop((transparent === false && inverted === true) - ? 'MobilePanel.default.background' - : 'primaryTextColor'); - +const background = ({ transparent, inverted }) => + prop( + transparent === true + ? 'backgroundColor' + : `MobilePanel.default.${inverted === true ? 'foreground' : 'background'}` + ); + +const textColor = ({ transparent, inverted }) => + prop( + transparent === false && inverted === true + ? 'MobilePanel.default.background' + : 'primaryTextColor' + ); const HeaderDiv = styled.div` - ${props => props.fixed && 'position: fixed;'} + ${(props) => props.fixed && 'position: fixed;'} width: 100%; - background: ${props => background(props)}; + background: ${(props) => background(props)}; color: ${textColor}; - padding: ${props => remSize(props.slim === true ? 2 : 12)}; + padding: ${(props) => remSize(props.slim === true ? 2 : 12)}; padding-left: ${remSize(16)}; padding-right: ${remSize(16)}; z-index: 1; @@ -34,59 +38,73 @@ const HeaderDiv = styled.div` padding: ${remSize(4)}; } - & svg path { fill: ${textColor} !important; } + & svg path { + fill: ${textColor} !important; + } .editor__unsaved-changes svg { width: ${remSize(16)}; padding: 0; - vertical-align: top + vertical-align: top; } `; const IconContainer = styled.div` - margin-left: ${props => (props.leftButton ? remSize(32) : remSize(4))}; + margin-left: ${(props) => (props.leftButton ? remSize(32) : remSize(4))}; list-style: none; display: flex; flex-direction: horizontal; `; - const TitleContainer = styled.div` margin-left: ${remSize(4)}; margin-right: auto; - ${props => props.padded && `h2{ + ${(props) => + props.padded && + `h2{ padding-top: ${remSize(10)}; padding-bottom: ${remSize(10)}; }`} `; const Header = ({ - title, subtitle, leftButton, children, - transparent, inverted, slim, fixed + title, + subtitle, + leftButton, + children, + transparent, + inverted, + slim, + fixed }) => ( - <HeaderDiv transparent={transparent} slim={slim} inverted={inverted} fixed={fixed}> + <HeaderDiv + transparent={transparent} + slim={slim} + inverted={inverted} + fixed={fixed} + > {leftButton} <TitleContainer padded={subtitle === null}> {title && <h2>{title}</h2>} {subtitle && <h3>{subtitle}</h3>} </TitleContainer> - <IconContainer> - {children} - </IconContainer> + <IconContainer>{children}</IconContainer> </HeaderDiv> ); - Header.propTypes = { title: PropTypes.oneOfType([PropTypes.string, PropTypes.element]), subtitle: PropTypes.string, leftButton: PropTypes.element, - children: PropTypes.oneOfType([PropTypes.element, PropTypes.arrayOf(PropTypes.element)]), + children: PropTypes.oneOfType([ + PropTypes.element, + PropTypes.arrayOf(PropTypes.element) + ]), transparent: PropTypes.bool, inverted: PropTypes.bool, slim: PropTypes.bool, - fixed: PropTypes.bool, + fixed: PropTypes.bool }; Header.defaultProps = { diff --git a/client/components/mobile/IDEWrapper.jsx b/client/components/mobile/IDEWrapper.jsx index 5e66ee0186..034122f456 100644 --- a/client/components/mobile/IDEWrapper.jsx +++ b/client/components/mobile/IDEWrapper.jsx @@ -7,5 +7,7 @@ import { remSize } from '../../theme'; export default styled.div` z-index: 0; margin-top: ${remSize(16)}; - .CodeMirror-sizer > * { padding-bottom: ${remSize(320)}; }; + .CodeMirror-sizer > * { + padding-bottom: ${remSize(320)}; + } `; diff --git a/client/components/mobile/IconButton.jsx b/client/components/mobile/IconButton.jsx index 7085f8a148..b1f3cf6ea5 100644 --- a/client/components/mobile/IconButton.jsx +++ b/client/components/mobile/IconButton.jsx @@ -5,23 +5,25 @@ import Button from '../../common/Button'; import { remSize } from '../../theme'; const ButtonWrapper = styled(Button)` -width: ${remSize(48)}; -> svg { - width: 100%; - height: 100%; -} + width: ${remSize(48)}; + > svg { + width: 100%; + height: 100%; + } `; const IconButton = (props) => { const { icon, ...otherProps } = props; const Icon = icon; - return (<ButtonWrapper - iconBefore={icon && <Icon />} - kind={Button.kinds.inline} - focusable="false" - {...otherProps} - />); + return ( + <ButtonWrapper + iconBefore={icon && <Icon />} + kind={Button.kinds.inline} + focusable="false" + {...otherProps} + /> + ); }; IconButton.propTypes = { diff --git a/client/components/mobile/MobileScreen.jsx b/client/components/mobile/MobileScreen.jsx index 471ffd943a..d52eae8052 100644 --- a/client/components/mobile/MobileScreen.jsx +++ b/client/components/mobile/MobileScreen.jsx @@ -12,19 +12,22 @@ const ScreenWrapper = styled.div` width: 92%; top: unset; min-width: unset; - bottom: ${remSize(64)} + bottom: ${remSize(64)}; } `; const Screen = ({ children, fullscreen, slimheader }) => ( - <ScreenWrapper className={fullscreen && 'fullscreen-preview'} slimheader={slimheader}> + <ScreenWrapper + className={fullscreen && 'fullscreen-preview'} + slimheader={slimheader} + > {children} </ScreenWrapper> ); Screen.defaultProps = { fullscreen: false, - slimheader: false, + slimheader: false }; Screen.propTypes = { diff --git a/client/components/mobile/PreferencePicker.jsx b/client/components/mobile/PreferencePicker.jsx index 0e2e085ad2..0a6746d65c 100644 --- a/client/components/mobile/PreferencePicker.jsx +++ b/client/components/mobile/PreferencePicker.jsx @@ -3,12 +3,17 @@ import PropTypes from 'prop-types'; import styled from 'styled-components'; import { prop, remSize } from '../../theme'; - -const PreferenceTitle = styled.h4.attrs(props => ({ ...props, className: 'preference__title' }))` +const PreferenceTitle = styled.h4.attrs((props) => ({ + ...props, + className: 'preference__title' +}))` color: ${prop('primaryTextColor')}; `; -const Preference = styled.div.attrs(props => ({ ...props, className: 'preference' }))` +const Preference = styled.div.attrs((props) => ({ + ...props, + className: 'preference' +}))` flex-wrap: nowrap !important; align-items: baseline !important; justify-items: space-between; @@ -18,14 +23,12 @@ const OptionLabel = styled.label.attrs({ className: 'preference__option' })` font-size: ${remSize(14)} !important; `; -const PreferencePicker = ({ - title, value, onSelect, options, -}) => ( +const PreferencePicker = ({ title, value, onSelect, options }) => ( <Preference> <PreferenceTitle>{title}</PreferenceTitle> <div className="preference__options"> - {options.map(option => ( - <React.Fragment key={`${option.name}-${option.id}`} > + {options.map((option) => ( + <React.Fragment key={`${option.name}-${option.id}`}> <input type="radio" onChange={() => onSelect(option.value)} @@ -43,7 +46,8 @@ const PreferencePicker = ({ > {option.label} </OptionLabel> - </React.Fragment>))} + </React.Fragment> + ))} </div> </Preference> ); @@ -55,13 +59,15 @@ PreferencePicker.defaultProps = { PreferencePicker.propTypes = { title: PropTypes.string.isRequired, value: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]).isRequired, - options: PropTypes.arrayOf(PropTypes.shape({ - id: PropTypes.string, - name: PropTypes.string, - label: PropTypes.string, - ariaLabel: PropTypes.string, - })), - onSelect: PropTypes.func.isRequired, + options: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string, + name: PropTypes.string, + label: PropTypes.string, + ariaLabel: PropTypes.string + }) + ), + onSelect: PropTypes.func.isRequired }; export default PreferencePicker; diff --git a/client/components/mobile/Sidebar.jsx b/client/components/mobile/Sidebar.jsx index 7ae608cc69..743af6be1a 100644 --- a/client/components/mobile/Sidebar.jsx +++ b/client/components/mobile/Sidebar.jsx @@ -7,7 +7,6 @@ import Header from './Header'; import IconButton from './IconButton'; import { ExitIcon } from '../../common/icons'; - const SidebarWrapper = styled.div` height: 100%; width: ${remSize(180)}; @@ -17,15 +16,20 @@ const SidebarWrapper = styled.div` left: 0; background: ${prop('backgroundColor')}; - box-shadow: 0 6px 6px 0 rgba(0,0,0,0.10); + box-shadow: 0 6px 6px 0 rgba(0, 0, 0, 0.1); `; const Sidebar = ({ title, onPressClose, children }) => ( <SidebarWrapper> - {title && - <Header slim title={title} fixed={false}> - <IconButton onClick={onPressClose} icon={ExitIcon} aria-label="Return to ide view" /> - </Header>} + {title && ( + <Header slim title={title} fixed={false}> + <IconButton + onClick={onPressClose} + icon={ExitIcon} + aria-label="Return to ide view" + /> + </Header> + )} {children} </SidebarWrapper> ); @@ -33,7 +37,10 @@ const Sidebar = ({ title, onPressClose, children }) => ( Sidebar.propTypes = { title: PropTypes.string, onPressClose: PropTypes.func, - children: PropTypes.oneOfType([PropTypes.element, PropTypes.arrayOf(PropTypes.element)]), + children: PropTypes.oneOfType([ + PropTypes.element, + PropTypes.arrayOf(PropTypes.element) + ]) }; Sidebar.defaultProps = { @@ -42,5 +49,4 @@ Sidebar.defaultProps = { onPressClose: () => {} }; - export default Sidebar; diff --git a/client/components/mobile/Tab.jsx b/client/components/mobile/Tab.jsx index 20a158b476..cb29c43690 100644 --- a/client/components/mobile/Tab.jsx +++ b/client/components/mobile/Tab.jsx @@ -6,10 +6,13 @@ import { prop, remSize } from '../../theme'; export default styled(Link)` box-sizing: border-box; - background: transparent; - /* border-top: ${remSize(4)} solid ${props => prop(props.selected ? 'colors.p5jsPink' : 'MobilePanel.default.background')}; */ - border-top: ${remSize(4)} solid ${props => (props.selected ? prop('TabHighlight') : 'transparent')}; + /* border-top: ${remSize(4)} solid ${(props) => + prop( + props.selected ? 'colors.p5jsPink' : 'MobilePanel.default.background' + )}; */ + border-top: ${remSize(4)} solid + ${(props) => (props.selected ? prop('TabHighlight') : 'transparent')}; color: ${prop('primaryTextColor')}; diff --git a/client/components/mobile/TabSwitcher.jsx b/client/components/mobile/TabSwitcher.jsx index 15a3bb0e6c..1d769dbdb0 100644 --- a/client/components/mobile/TabSwitcher.jsx +++ b/client/components/mobile/TabSwitcher.jsx @@ -6,10 +6,12 @@ import { prop, remSize } from '../../theme'; export default styled.div` display: flex; justify-content: space-between; - - h3 { text-align: center; width: 100%; } + + h3 { + text-align: center; + width: 100%; + } border-top: 1px solid ${prop('Separator')}; - background: ${props => prop('backgroundColor')}; + background: ${(props) => prop('backgroundColor')}; `; - diff --git a/client/components/useAsModal.jsx b/client/components/useAsModal.jsx index 350d1de2ed..2fbfd5a96c 100644 --- a/client/components/useAsModal.jsx +++ b/client/components/useAsModal.jsx @@ -7,7 +7,7 @@ const BackgroundOverlay = styled.div` z-index: 2; width: 100% !important; height: 100% !important; - + background: black; opacity: 0.3; `; @@ -15,15 +15,15 @@ const BackgroundOverlay = styled.div` export default (Element, hasOverlay = false) => { const [visible, toggle, setRef] = useModalBehavior(); - const wrapper = () => (visible && - <div> - {hasOverlay && <BackgroundOverlay />} - <div ref={setRef}> - { (typeof (Element) === 'function') - ? Element(toggle) - : Element} + const wrapper = () => + visible && ( + <div> + {hasOverlay && <BackgroundOverlay />} + <div ref={setRef}> + {typeof Element === 'function' ? Element(toggle) : Element} + </div> </div> - </div>); + ); return [toggle, wrapper]; }; diff --git a/client/i18n-test.js b/client/i18n-test.js index fcef18a7f2..7e496be29b 100644 --- a/client/i18n-test.js +++ b/client/i18n-test.js @@ -3,24 +3,21 @@ import { initReactI18next } from 'react-i18next'; import translations from '../translations/locales/en-US/translations.json'; -i18n - .use(initReactI18next) - .init({ - lng: 'en-US', - fallbackLng: 'en-US', +i18n.use(initReactI18next).init({ + lng: 'en-US', + fallbackLng: 'en-US', - // have a common namespace used around the full app - ns: ['translations'], - defaultNS: 'translations', + // have a common namespace used around the full app + ns: ['translations'], + defaultNS: 'translations', - debug: false, + debug: false, - interpolation: { - escapeValue: false, // not needed for react!! - }, - - resources: { 'en-US': { translations } }, - }); + interpolation: { + escapeValue: false // not needed for react!! + }, + resources: { 'en-US': { translations } } +}); export default i18n; diff --git a/client/i18n.js b/client/i18n.js index 819fd34160..102a0e8b9e 100644 --- a/client/i18n.js +++ b/client/i18n.js @@ -11,7 +11,7 @@ export function languageKeyToLabel(lang) { const languageMap = { 'en-US': 'English', 'es-419': 'Español', - 'ja': '日本語' + "ja": '日本語' }; return languageMap[lang]; } @@ -20,7 +20,7 @@ export function languageKeyToDateLocale(lang) { const languageMap = { 'en-US': enUS, 'es-419': es, - 'ja': ja + "ja": ja }; return languageMap[lang]; } @@ -31,10 +31,11 @@ export function currentDateLocale() { const options = { loadPath: '/locales/{{lng}}/translations.json', - requestOptions: { // used for fetch, can also be a function (payload) => ({ method: 'GET' }) + requestOptions: { + // used for fetch, can also be a function (payload) => ({ method: 'GET' }) mode: 'no-cors' }, - allowMultiLoading: false, // set loadPath: '/locales/resources.json?lng={{lng}}&ns={{ns}}' to adapt to multiLoading + allowMultiLoading: false // set loadPath: '/locales/resources.json?lng={{lng}}&ns={{ns}}' to adapt to multiLoading }; i18n @@ -51,7 +52,7 @@ i18n useSuspense: true, whitelist: availableLanguages, interpolation: { - escapeValue: false, // react already safes from xss + escapeValue: false // react already safes from xss }, saveMissing: false, // if a key is not found AND this flag is set to true, i18next will call the handler missingKeyHandler missingKeyHandler: false // function(lng, ns, key, fallbackValue) { } custom logic about how to handle the missing keys diff --git a/client/index.jsx b/client/index.jsx index bf8c22636c..4f90a2cc8d 100644 --- a/client/index.jsx +++ b/client/index.jsx @@ -31,7 +31,7 @@ const App = () => ( const HotApp = hot(App); render( - <Suspense fallback={(<Loader />)}> + <Suspense fallback={<Loader />}> <HotApp /> </Suspense>, document.getElementById('root') diff --git a/client/modules/App/App.jsx b/client/modules/App/App.jsx index 3b74e1fca6..c6b7a581bb 100644 --- a/client/modules/App/App.jsx +++ b/client/modules/App/App.jsx @@ -41,7 +41,9 @@ class App extends React.Component { render() { return ( <div className="app"> - {this.state.isMounted && !window.devToolsExtension && getConfig('NODE_ENV') === 'development' && <DevTools />} + {this.state.isMounted && + !window.devToolsExtension && + getConfig('NODE_ENV') === 'development' && <DevTools />} {this.props.children} </div> ); @@ -53,13 +55,13 @@ App.propTypes = { location: PropTypes.shape({ pathname: PropTypes.string, state: PropTypes.shape({ - skipSavingPath: PropTypes.bool, - }), + skipSavingPath: PropTypes.bool + }) }).isRequired, setPreviousPath: PropTypes.func.isRequired, setLanguage: PropTypes.func.isRequired, language: PropTypes.string, - theme: PropTypes.string, + theme: PropTypes.string }; App.defaultProps = { @@ -68,9 +70,9 @@ App.defaultProps = { theme: 'light' }; -const mapStateToProps = state => ({ +const mapStateToProps = (state) => ({ theme: state.preferences.theme, - language: state.preferences.language, + language: state.preferences.language }); const mapDispatchToProps = { setPreviousPath, setLanguage }; diff --git a/client/modules/App/components/DevTools.jsx b/client/modules/App/components/DevTools.jsx index 32f987d51f..f551a2ee65 100644 --- a/client/modules/App/components/DevTools.jsx +++ b/client/modules/App/components/DevTools.jsx @@ -4,10 +4,7 @@ import LogMonitor from 'redux-devtools-log-monitor'; import DockMonitor from 'redux-devtools-dock-monitor'; const devTools = ( - <DockMonitor - toggleVisibilityKey="ctrl-h" - changePositionKey="ctrl-w" - > + <DockMonitor toggleVisibilityKey="ctrl-h" changePositionKey="ctrl-w"> <LogMonitor /> </DockMonitor> ); diff --git a/client/modules/App/components/Overlay.jsx b/client/modules/App/components/Overlay.jsx index 3ebeca4522..39f492fc6f 100644 --- a/client/modules/App/components/Overlay.jsx +++ b/client/modules/App/components/Overlay.jsx @@ -51,7 +51,8 @@ class Overlay extends React.Component { close() { // Only close if it is the last (and therefore the topmost overlay) const overlays = document.getElementsByClassName('overlay'); - if (this.node.parentElement.parentElement !== overlays[overlays.length - 1]) return; + if (this.node.parentElement.parentElement !== overlays[overlays.length - 1]) + return; if (!this.props.closeOverlay) { browserHistory.push(this.props.previousPath); @@ -61,27 +62,29 @@ class Overlay extends React.Component { } render() { - const { - ariaLabel, - title, - children, - actions, - isFixedHeight, - } = this.props; + const { ariaLabel, title, children, actions, isFixedHeight } = this.props; return ( - <div className={`overlay ${isFixedHeight ? 'overlay--is-fixed-height' : ''}`}> + <div + className={`overlay ${isFixedHeight ? 'overlay--is-fixed-height' : ''}`} + > <div className="overlay__content"> <section role="main" aria-label={ariaLabel} - ref={(node) => { this.node = node; }} + ref={(node) => { + this.node = node; + }} className="overlay__body" > <header className="overlay__header"> <h2 className="overlay__title">{title}</h2> <div className="overlay__actions"> {actions} - <button className="overlay__close-button" onClick={this.close} aria-label={this.props.t('Overlay.AriaLabel', { title })}> + <button + className="overlay__close-button" + onClick={this.close} + aria-label={this.props.t('Overlay.AriaLabel', { title })} + > <ExitIcon focusable="false" aria-hidden="true" /> </button> </div> @@ -112,7 +115,7 @@ Overlay.defaultProps = { closeOverlay: null, ariaLabel: 'modal', previousPath: '/', - isFixedHeight: false, + isFixedHeight: false }; export default withTranslation()(Overlay); diff --git a/client/modules/App/components/ThemeProvider.jsx b/client/modules/App/components/ThemeProvider.jsx index eb6dcc9be5..a168d856d9 100644 --- a/client/modules/App/components/ThemeProvider.jsx +++ b/client/modules/App/components/ThemeProvider.jsx @@ -6,21 +6,18 @@ import { ThemeProvider } from 'styled-components'; import theme, { Theme } from '../../../theme'; const Provider = ({ children, currentTheme }) => ( - <ThemeProvider theme={{ ...theme[currentTheme] }}> - {children} - </ThemeProvider> + <ThemeProvider theme={{ ...theme[currentTheme] }}>{children}</ThemeProvider> ); Provider.propTypes = { children: PropTypes.node.isRequired, - currentTheme: PropTypes.oneOf(Object.keys(Theme)).isRequired, + currentTheme: PropTypes.oneOf(Object.keys(Theme)).isRequired }; function mapStateToProps(state) { return { - currentTheme: state.preferences.theme, + currentTheme: state.preferences.theme }; } - export default connect(mapStateToProps)(Provider); diff --git a/client/modules/IDE/actions/assets.js b/client/modules/IDE/actions/assets.js index 79df42850e..8c7a046121 100644 --- a/client/modules/IDE/actions/assets.js +++ b/client/modules/IDE/actions/assets.js @@ -13,7 +13,8 @@ function setAssets(assets, totalSize) { export function getAssets() { return (dispatch) => { dispatch(startLoader()); - apiClient.get('/S3/objects') + apiClient + .get('/S3/objects') .then((response) => { dispatch(setAssets(response.data.assets, response.data.totalSize)); dispatch(stopLoader()); @@ -36,7 +37,8 @@ export function deleteAsset(assetKey) { export function deleteAssetRequest(assetKey) { return (dispatch) => { - apiClient.delete(`/S3/${assetKey}`) + apiClient + .delete(`/S3/${assetKey}`) .then((response) => { dispatch(deleteAsset(assetKey)); }) diff --git a/client/modules/IDE/actions/collections.js b/client/modules/IDE/actions/collections.js index 1d2a8e2b83..69da0f23ba 100644 --- a/client/modules/IDE/actions/collections.js +++ b/client/modules/IDE/actions/collections.js @@ -4,7 +4,6 @@ import * as ActionTypes from '../../../constants'; import { startLoader, stopLoader } from './loader'; import { setToastText, showToast } from './toast'; - const TOAST_DISPLAY_TIME_MS = 1500; // eslint-disable-next-line @@ -18,7 +17,8 @@ export function getCollections(username) { url = '/collections'; } console.log(url); - apiClient.get(url) + apiClient + .get(url) .then((response) => { dispatch({ type: ActionTypes.SET_COLLECTIONS, @@ -41,7 +41,8 @@ export function createCollection(collection) { return (dispatch) => { dispatch(startLoader()); const url = '/collections'; - return apiClient.post(url, collection) + return apiClient + .post(url, collection) .then((response) => { dispatch({ type: ActionTypes.CREATE_COLLECTION @@ -73,7 +74,8 @@ export function addToCollection(collectionId, projectId) { return (dispatch) => { dispatch(startLoader()); const url = `/collections/${collectionId}/${projectId}`; - return apiClient.post(url) + return apiClient + .post(url) .then((response) => { dispatch({ type: ActionTypes.ADD_TO_COLLECTION, @@ -105,7 +107,8 @@ export function removeFromCollection(collectionId, projectId) { return (dispatch) => { dispatch(startLoader()); const url = `/collections/${collectionId}/${projectId}`; - return apiClient.delete(url) + return apiClient + .delete(url) .then((response) => { dispatch({ type: ActionTypes.REMOVE_FROM_COLLECTION, @@ -136,7 +139,8 @@ export function removeFromCollection(collectionId, projectId) { export function editCollection(collectionId, { name, description }) { return (dispatch) => { const url = `/collections/${collectionId}`; - return apiClient.patch(url, { name, description }) + return apiClient + .patch(url, { name, description }) .then((response) => { dispatch({ type: ActionTypes.EDIT_COLLECTION, @@ -159,12 +163,13 @@ export function editCollection(collectionId, { name, description }) { export function deleteCollection(collectionId) { return (dispatch) => { const url = `/collections/${collectionId}`; - return apiClient.delete(url) + return apiClient + .delete(url) .then((response) => { dispatch({ type: ActionTypes.DELETE_COLLECTION, payload: response.data, - collectionId, + collectionId }); return response.data; }) diff --git a/client/modules/IDE/actions/files.js b/client/modules/IDE/actions/files.js index faf3e18134..68ed5a5329 100644 --- a/client/modules/IDE/actions/files.js +++ b/client/modules/IDE/actions/files.js @@ -2,23 +2,31 @@ import objectID from 'bson-objectid'; import blobUtil from 'blob-util'; import apiClient from '../../../utils/apiClient'; import * as ActionTypes from '../../../constants'; -import { setUnsavedChanges, closeNewFolderModal, closeNewFileModal } from './ide'; +import { + setUnsavedChanges, + closeNewFolderModal, + closeNewFileModal +} from './ide'; import { setProjectSavedTime } from './project'; import { createError } from './ide'; - function appendToFilename(filename, string) { const dotIndex = filename.lastIndexOf('.'); if (dotIndex === -1) return filename + string; - return filename.substring(0, dotIndex) + string + filename.substring(dotIndex); + return ( + filename.substring(0, dotIndex) + string + filename.substring(dotIndex) + ); } function createUniqueName(name, parentId, files) { - const siblingFiles = files.find(file => file.id === parentId) - .children.map(childFileId => files.find(file => file.id === childFileId)); + const siblingFiles = files + .find((file) => file.id === parentId) + .children.map((childFileId) => + files.find((file) => file.id === childFileId) + ); let testName = name; let index = 1; - let existingName = siblingFiles.find(file => name === file.name); + let existingName = siblingFiles.find((file) => name === file.name); while (existingName) { testName = appendToFilename(name, `-${index}`); @@ -53,8 +61,9 @@ export function submitFile(formProps, files, parentId, projectId) { parentId, children: [] }; - return apiClient.post(`/projects/${projectId}/files`, postParams) - .then(response => ({ + return apiClient + .post(`/projects/${projectId}/files`, postParams) + .then((response) => ({ file: response.data.updatedFile, updatedAt: response.data.project.updatedAt })); @@ -80,18 +89,20 @@ export function handleCreateFile(formProps) { const { parentId } = state.ide; const projectId = state.project.id; return new Promise((resolve) => { - submitFile(formProps, files, parentId, projectId).then((response) => { - const { file, updatedAt } = response; - dispatch(createFile(file, parentId)); - if (updatedAt) dispatch(setProjectSavedTime(updatedAt)); - dispatch(closeNewFileModal()); - dispatch(setUnsavedChanges(true)); - resolve(); - }).catch((error) => { - const { response } = error; - dispatch(createError(response.data)); - resolve({ error }); - }); + submitFile(formProps, files, parentId, projectId) + .then((response) => { + const { file, updatedAt } = response; + dispatch(createFile(file, parentId)); + if (updatedAt) dispatch(setProjectSavedTime(updatedAt)); + dispatch(closeNewFileModal()); + dispatch(setUnsavedChanges(true)); + resolve(); + }) + .catch((error) => { + const { response } = error; + dispatch(createError(response.data)); + resolve({ error }); + }); }); }; } @@ -105,8 +116,9 @@ export function submitFolder(formProps, files, parentId, projectId) { parentId, fileType: 'folder' }; - return apiClient.post(`/projects/${projectId}/files`, postParams) - .then(response => ({ + return apiClient + .post(`/projects/${projectId}/files`, postParams) + .then((response) => ({ file: response.data.updatedFile, updatedAt: response.data.project.updatedAt })); @@ -134,18 +146,20 @@ export function handleCreateFolder(formProps) { const { parentId } = state.ide; const projectId = state.project.id; return new Promise((resolve) => { - submitFolder(formProps, files, parentId, projectId).then((response) => { - const { file, updatedAt } = response; - dispatch(createFile(file, parentId)); - if (updatedAt) dispatch(setProjectSavedTime(updatedAt)); - dispatch(closeNewFolderModal()); - dispatch(setUnsavedChanges(true)); - resolve(); - }).catch((error) => { - const { response } = error; - dispatch(createError(response.data)); - resolve({ error }); - }); + submitFolder(formProps, files, parentId, projectId) + .then((response) => { + const { file, updatedAt } = response; + dispatch(createFile(file, parentId)); + if (updatedAt) dispatch(setProjectSavedTime(updatedAt)); + dispatch(closeNewFolderModal()); + dispatch(setUnsavedChanges(true)); + resolve(); + }) + .catch((error) => { + const { response } = error; + dispatch(createError(response.data)); + resolve({ error }); + }); }); }; } @@ -170,7 +184,8 @@ export function deleteFile(id, parentId) { parentId } }; - apiClient.delete(`/projects/${state.project.id}/files/${id}`, deleteConfig) + apiClient + .delete(`/projects/${state.project.id}/files/${id}`, deleteConfig) .then((response) => { dispatch(setProjectSavedTime(response.data.project.updatedAt)); dispatch({ diff --git a/client/modules/IDE/actions/ide.js b/client/modules/IDE/actions/ide.js index e18d9d747c..19fc12d471 100644 --- a/client/modules/IDE/actions/ide.js +++ b/client/modules/IDE/actions/ide.js @@ -54,7 +54,9 @@ export function setSelectedFile(fileId) { export function resetSelectedFile(previousId) { return (dispatch, getState) => { const state = getState(); - const newId = state.files.find(file => file.name !== 'root' && file.id !== previousId).id; + const newId = state.files.find( + (file) => file.name !== 'root' && file.id !== previousId + ).id; dispatch({ type: ActionTypes.SET_SELECTED_FILE, selectedFile: newId @@ -215,7 +217,7 @@ export function resetInfiniteLoops() { export function justOpenedProject() { return { - type: ActionTypes.JUST_OPENED_PROJECT, + type: ActionTypes.JUST_OPENED_PROJECT }; } diff --git a/client/modules/IDE/actions/preferences.js b/client/modules/IDE/actions/preferences.js index 96f29c6739..68a4a23111 100644 --- a/client/modules/IDE/actions/preferences.js +++ b/client/modules/IDE/actions/preferences.js @@ -3,9 +3,9 @@ import apiClient from '../../../utils/apiClient'; import * as ActionTypes from '../../../constants'; function updatePreferences(formParams, dispatch) { - apiClient.put('/preferences', formParams) - .then(() => { - }) + apiClient + .put('/preferences', formParams) + .then(() => {}) .catch((error) => { const { response } = error; dispatch({ @@ -247,4 +247,3 @@ export function setLanguage(value, { persistPreference = true } = {}) { } }; } - diff --git a/client/modules/IDE/actions/project.js b/client/modules/IDE/actions/project.js index eacb4194b7..e2931f3ff4 100644 --- a/client/modules/IDE/actions/project.js +++ b/client/modules/IDE/actions/project.js @@ -54,7 +54,8 @@ export function setNewProject(project) { export function getProject(id, username) { return (dispatch, getState) => { dispatch(justOpenedProject()); - apiClient.get(`/${username}/projects/${id}`) + apiClient + .get(`/${username}/projects/${id}`) .then((response) => { dispatch(setProject(response.data)); dispatch(setUnsavedChanges(false)); @@ -72,7 +73,7 @@ export function getProject(id, username) { export function persistState() { return (dispatch, getState) => { dispatch({ - type: ActionTypes.PERSIST_STATE, + type: ActionTypes.PERSIST_STATE }); const state = getState(); saveState(state); @@ -82,7 +83,7 @@ export function persistState() { export function clearPersistedState() { return (dispatch) => { dispatch({ - type: ActionTypes.CLEAR_PERSISTED_STATE, + type: ActionTypes.CLEAR_PERSISTED_STATE }); clearState(); }; @@ -110,8 +111,12 @@ export function projectSaveSuccess() { function getSynchedProject(currentState, responseProject) { let hasChanges = false; const synchedProject = Object.assign({}, responseProject); - const currentFiles = currentState.files.map(({ name, children, content }) => ({ name, children, content })); - const responseFiles = responseProject.files.map(({ name, children, content }) => ({ name, children, content })); + const currentFiles = currentState.files.map( + ({ name, children, content }) => ({ name, children, content }) + ); + const responseFiles = responseProject.files.map( + ({ name, children, content }) => ({ name, children, content }) + ); if (!isEqual(currentFiles, responseFiles)) { synchedProject.files = currentState.files; hasChanges = true; @@ -126,29 +131,43 @@ function getSynchedProject(currentState, responseProject) { }; } -export function saveProject(selectedFile = null, autosave = false, mobile = false) { +export function saveProject( + selectedFile = null, + autosave = false, + mobile = false +) { return (dispatch, getState) => { const state = getState(); if (state.project.isSaving) { return Promise.resolve(); } dispatch(startSavingProject()); - if (state.user.id && state.project.owner && state.project.owner.id !== state.user.id) { + if ( + state.user.id && + state.project.owner && + state.project.owner.id !== state.user.id + ) { return Promise.reject(); } const formParams = Object.assign({}, state.project); formParams.files = [...state.files]; if (selectedFile) { - const fileToUpdate = formParams.files.find(file => file.id === selectedFile.id); + const fileToUpdate = formParams.files.find( + (file) => file.id === selectedFile.id + ); fileToUpdate.content = selectedFile.content; } if (state.project.id) { - return apiClient.put(`/projects/${state.project.id}`, formParams) + return apiClient + .put(`/projects/${state.project.id}`, formParams) .then((response) => { dispatch(endSavingProject()); dispatch(setUnsavedChanges(false)); - const { hasChanges, synchedProject } = getSynchedProject(getState(), response.data); + const { hasChanges, synchedProject } = getSynchedProject( + getState(), + response.data + ); if (hasChanges) { dispatch(setUnsavedChanges(true)); } @@ -158,7 +177,10 @@ export function saveProject(selectedFile = null, autosave = false, mobile = fals if (state.ide.justOpenedProject && state.preferences.autosave) { dispatch(showToast(5500)); dispatch(setToastText('Toast.SketchSaved')); - setTimeout(() => dispatch(setToastText('Toast.AutosaveEnabled')), 1500); + setTimeout( + () => dispatch(setToastText('Toast.AutosaveEnabled')), + 1500 + ); dispatch(resetJustOpenedProject()); } else { dispatch(showToast(1500)); @@ -181,14 +203,20 @@ export function saveProject(selectedFile = null, autosave = false, mobile = fals }); } - return apiClient.post('/projects', formParams) + return apiClient + .post('/projects', formParams) .then((response) => { dispatch(endSavingProject()); - const { hasChanges, synchedProject } = getSynchedProject(getState(), response.data); + const { hasChanges, synchedProject } = getSynchedProject( + getState(), + response.data + ); dispatch(setNewProject(synchedProject)); dispatch(setUnsavedChanges(false)); - browserHistory.push(`/${response.data.user.username}/sketches/${response.data.id}`); + browserHistory.push( + `/${response.data.user.username}/sketches/${response.data.id}` + ); if (hasChanges) { dispatch(setUnsavedChanges(true)); @@ -199,7 +227,10 @@ export function saveProject(selectedFile = null, autosave = false, mobile = fals if (state.preferences.autosave) { dispatch(showToast(5500)); dispatch(setToastText('Toast.SketchSaved')); - setTimeout(() => dispatch(setToastText('Toast.AutosaveEnabled')), 1500); + setTimeout( + () => dispatch(setToastText('Toast.AutosaveEnabled')), + 1500 + ); dispatch(resetJustOpenedProject()); } else { dispatch(showToast(1500)); @@ -248,7 +279,7 @@ export function newProject() { function generateNewIdsForChildren(file, files) { const newChildren = []; file.children.forEach((childId) => { - const child = files.find(childFile => childFile.id === childId); + const child = files.find((childFile) => childFile.id === childId); const newId = objectID().toHexString(); child.id = newId; child._id = newId; @@ -264,45 +295,59 @@ export function cloneProject(project) { const state = getState(); const files = project ? project.files : state.files; const projectName = project ? project.name : state.project.name; - const newFiles = files.map(file => ({ ...file })); + const newFiles = files.map((file) => ({ ...file })); // generate new IDS for all files - const rootFile = newFiles.find(file => file.name === 'root'); + const rootFile = newFiles.find((file) => file.name === 'root'); const newRootFileId = objectID().toHexString(); rootFile.id = newRootFileId; rootFile._id = newRootFileId; generateNewIdsForChildren(rootFile, newFiles); // duplicate all files hosted on S3 - each(newFiles, (file, callback) => { - if (file.url && (file.url.includes(S3_BUCKET_URL_BASE) || file.url.includes(S3_BUCKET))) { - const formParams = { - url: file.url - }; - apiClient.post('/S3/copy', formParams) - .then((response) => { + each( + newFiles, + (file, callback) => { + if ( + file.url && + (file.url.includes(S3_BUCKET_URL_BASE) || + file.url.includes(S3_BUCKET)) + ) { + const formParams = { + url: file.url + }; + apiClient.post('/S3/copy', formParams).then((response) => { file.url = response.data.url; callback(null); }); - } else { - callback(null); - } - }, (err) => { - // if not errors in duplicating the files on S3, then duplicate it - const formParams = Object.assign({}, { name: `${projectName} copy` }, { files: newFiles }); - apiClient.post('/projects', formParams) - .then((response) => { - browserHistory.push(`/${response.data.user.username}/sketches/${response.data.id}`); - dispatch(setNewProject(response.data)); - }) - .catch((error) => { - const { response } = error; - dispatch({ - type: ActionTypes.PROJECT_SAVE_FAIL, - error: response.data + } else { + callback(null); + } + }, + (err) => { + // if not errors in duplicating the files on S3, then duplicate it + const formParams = Object.assign( + {}, + { name: `${projectName} copy` }, + { files: newFiles } + ); + apiClient + .post('/projects', formParams) + .then((response) => { + browserHistory.push( + `/${response.data.user.username}/sketches/${response.data.id}` + ); + dispatch(setNewProject(response.data)); + }) + .catch((error) => { + const { response } = error; + dispatch({ + type: ActionTypes.PROJECT_SAVE_FAIL, + error: response.data + }); }); - }); - }); + } + ); }; } @@ -328,7 +373,8 @@ export function setProjectSavedTime(updatedAt) { export function changeProjectName(id, newName) { return (dispatch, getState) => { const state = getState(); - apiClient.put(`/projects/${id}`, { name: newName }) + apiClient + .put(`/projects/${id}`, { name: newName }) .then((response) => { if (response.status === 200) { dispatch({ @@ -355,7 +401,8 @@ export function changeProjectName(id, newName) { export function deleteProject(id) { return (dispatch, getState) => { - apiClient.delete(`/projects/${id}`) + apiClient + .delete(`/projects/${id}`) .then(() => { const state = getState(); if (id === state.project.id) { diff --git a/client/modules/IDE/actions/projects.js b/client/modules/IDE/actions/projects.js index d41747b5fb..fabda631a0 100644 --- a/client/modules/IDE/actions/projects.js +++ b/client/modules/IDE/actions/projects.js @@ -12,7 +12,8 @@ export function getProjects(username) { } else { url = '/projects'; } - apiClient.get(url) + apiClient + .get(url) .then((response) => { dispatch({ type: ActionTypes.SET_PROJECTS, diff --git a/client/modules/IDE/actions/sorting.js b/client/modules/IDE/actions/sorting.js index f28cf325f1..b9aa0354cb 100644 --- a/client/modules/IDE/actions/sorting.js +++ b/client/modules/IDE/actions/sorting.js @@ -30,7 +30,7 @@ export function setSearchTerm(scope, searchTerm) { return { type: ActionTypes.SET_SEARCH_TERM, query: searchTerm, - scope, + scope }; } diff --git a/client/modules/IDE/actions/uploader.js b/client/modules/IDE/actions/uploader.js index 24dcbfcf42..c8bb3aca6a 100644 --- a/client/modules/IDE/actions/uploader.js +++ b/client/modules/IDE/actions/uploader.js @@ -3,8 +3,11 @@ import getConfig from '../../../utils/getConfig'; import { createFile } from './files'; import { TEXT_FILE_REGEX } from '../../../../server/utils/fileUtils'; -const s3BucketHttps = getConfig('S3_BUCKET_URL_BASE') || - `https://s3-${getConfig('AWS_REGION')}.amazonaws.com/${getConfig('S3_BUCKET')}/`; +const s3BucketHttps = + getConfig('S3_BUCKET_URL_BASE') || + `https://s3-${getConfig('AWS_REGION')}.amazonaws.com/${getConfig( + 'S3_BUCKET' + )}/`; const MAX_LOCAL_FILE_SIZE = 80000; // bytes, aka 80 KB function localIntercept(file, options = {}) { @@ -43,27 +46,29 @@ export function dropzoneAcceptCallback(userId, file, done) { return () => { // if a user would want to edit this file as text, local interceptor if (file.name.match(TEXT_FILE_REGEX) && file.size < MAX_LOCAL_FILE_SIZE) { - localIntercept(file).then((result) => { + localIntercept(file) + .then((result) => { file.content = result; // eslint-disable-line - done('Uploading plaintext file locally.'); - file.previewElement.classList.remove('dz-error'); - file.previewElement.classList.add('dz-success'); - file.previewElement.classList.add('dz-processing'); - file.previewElement.querySelector('.dz-upload').style.width = '100%'; - }) + done('Uploading plaintext file locally.'); + file.previewElement.classList.remove('dz-error'); + file.previewElement.classList.add('dz-success'); + file.previewElement.classList.add('dz-processing'); + file.previewElement.querySelector('.dz-upload').style.width = '100%'; + }) .catch((result) => { done(`Failed to download file ${file.name}: ${result}`); console.warn(file); }); } else { file.postData = []; // eslint-disable-line - apiClient.post('/S3/sign', { - name: file.name, - type: file.type, - size: file.size, - userId - // _csrf: document.getElementById('__createPostToken').value - }) + apiClient + .post('/S3/sign', { + name: file.name, + type: file.type, + size: file.size, + userId + // _csrf: document.getElementById('__createPostToken').value + }) .then((response) => { file.custom_status = 'ready'; // eslint-disable-line file.postData = response.data; // eslint-disable-line @@ -74,7 +79,11 @@ export function dropzoneAcceptCallback(userId, file, done) { .catch((error) => { const { response } = error; file.custom_status = 'rejected'; // eslint-disable-line - if (response.data && response.data.responseText && response.data.responseText.message) { + if ( + response.data && + response.data.responseText && + response.data.responseText.message + ) { done(response.data.responseText.message); } done('Error: Reached upload limit.'); @@ -95,7 +104,10 @@ export function dropzoneSendingCallback(file, xhr, formData) { export function dropzoneCompleteCallback(file) { return (dispatch, getState) => { // eslint-disable-line - if ((!file.name.match(TEXT_FILE_REGEX) || file.size >= MAX_LOCAL_FILE_SIZE) && file.status !== 'error') { + if ( + (!file.name.match(TEXT_FILE_REGEX) || file.size >= MAX_LOCAL_FILE_SIZE) && + file.status !== 'error' + ) { let inputHidden = '<input type="hidden" name="attachments[]" value="'; const json = { url: `${s3BucketHttps}${file.postData.key}`, diff --git a/client/modules/IDE/components/About.jsx b/client/modules/IDE/components/About.jsx index dcf9073f0a..3ea4c5b101 100644 --- a/client/modules/IDE/components/About.jsx +++ b/client/modules/IDE/components/About.jsx @@ -13,7 +13,12 @@ function About(props) { <title> {t('About.TitleHelmet')} </title> </Helmet> <div className="about__content-column"> - <SquareLogoIcon className="about__logo" role="img" aria-label={t('Common.p5logoARIA')} focusable="false" /> + <SquareLogoIcon + className="about__logo" + role="img" + aria-label={t('Common.p5logoARIA')} + focusable="false" + /> </div> <div className="about__content-column"> <h3 className="about__content-column-title">{t('About.NewP5')}</h3> @@ -23,7 +28,11 @@ function About(props) { target="_blank" rel="noopener noreferrer" > - <AsteriskIcon className="about__content-column-asterisk" aria-hidden="true" focusable="false" /> + <AsteriskIcon + className="about__content-column-asterisk" + aria-hidden="true" + focusable="false" + /> {t('About.Examples')} </a> </p> @@ -33,7 +42,11 @@ function About(props) { target="_blank" rel="noopener noreferrer" > - <AsteriskIcon className="about__content-column-asterisk" aria-hidden="true" focusable="false" /> + <AsteriskIcon + className="about__content-column-asterisk" + aria-hidden="true" + focusable="false" + /> {t('About.Learn')} </a> </p> @@ -46,7 +59,11 @@ function About(props) { target="_blank" rel="noopener noreferrer" > - <AsteriskIcon className="about__content-column-asterisk" aria-hidden="true" focusable="false" /> + <AsteriskIcon + className="about__content-column-asterisk" + aria-hidden="true" + focusable="false" + /> {t('About.Libraries')} </a> </p> @@ -56,7 +73,11 @@ function About(props) { target="_blank" rel="noopener noreferrer" > - <AsteriskIcon className="about__content-column-asterisk" aria-hidden="true" focusable="false" /> + <AsteriskIcon + className="about__content-column-asterisk" + aria-hidden="true" + focusable="false" + /> {t('Nav.Help.Reference')} </a> </p> @@ -66,7 +87,11 @@ function About(props) { target="_blank" rel="noopener noreferrer" > - <AsteriskIcon className="about__content-column-asterisk" aria-hidden="true" focusable="false" /> + <AsteriskIcon + className="about__content-column-asterisk" + aria-hidden="true" + focusable="false" + /> {t('About.Forum')} </a> </p> @@ -77,7 +102,8 @@ function About(props) { href="https://github.com/processing/p5.js-web-editor" target="_blank" rel="noopener noreferrer" - >{t('About.Contribute')} + > + {t('About.Contribute')} </a> </p> <p className="about__footer-list"> @@ -85,7 +111,8 @@ function About(props) { href="https://github.com/processing/p5.js-web-editor/issues/new" target="_blank" rel="noopener noreferrer" - >{t('About.Report')} + > + {t('About.Report')} </a> </p> <p className="about__footer-list"> @@ -93,7 +120,8 @@ function About(props) { href="https://twitter.com/p5xjs?lang=en" target="_blank" rel="noopener noreferrer" - >Twitter + > + Twitter </a> </p> </div> diff --git a/client/modules/IDE/components/AddToCollectionList.jsx b/client/modules/IDE/components/AddToCollectionList.jsx index f43691240d..26addfaa34 100644 --- a/client/modules/IDE/components/AddToCollectionList.jsx +++ b/client/modules/IDE/components/AddToCollectionList.jsx @@ -15,7 +15,7 @@ import Loader from '../../App/components/loader'; import QuickAddList from './QuickAddList'; const projectInCollection = (project, collection) => - collection.items.find(item => item.projectId === project.id) != null; + collection.items.find((item) => item.projectId === project.id) != null; class CollectionList extends React.Component { constructor(props) { @@ -28,7 +28,7 @@ class CollectionList extends React.Component { this.props.getCollections(this.props.username); this.state = { - hasLoadedData: false, + hasLoadedData: false }; } @@ -36,7 +36,7 @@ class CollectionList extends React.Component { if (prevProps.loading === true && this.props.loading === false) { // eslint-disable-next-line react/no-did-update-set-state this.setState({ - hasLoadedData: true, + hasLoadedData: true }); } } @@ -45,24 +45,26 @@ class CollectionList extends React.Component { if (this.props.username === this.props.user.username) { return this.props.t('AddToCollectionList.Title'); } - return this.props.t('AddToCollectionList.AnothersTitle', { anotheruser: this.props.username }); + return this.props.t('AddToCollectionList.AnothersTitle', { + anotheruser: this.props.username + }); } handleCollectionAdd = (collection) => { this.props.addToCollection(collection.id, this.props.project.id); - } + }; handleCollectionRemove = (collection) => { this.props.removeFromCollection(collection.id, this.props.project.id); - } + }; render() { const { collections, project } = this.props; const hasCollections = collections.length > 0; - const collectionWithSketchStatus = collections.map(collection => ({ + const collectionWithSketchStatus = collections.map((collection) => ({ ...collection, url: `/${collection.owner.username}/collections/${collection.id}`, - isAdded: projectInCollection(project, collection), + isAdded: projectInCollection(project, collection) })); let content = null; @@ -103,7 +105,7 @@ const ProjectShape = PropTypes.shape({ updatedAt: PropTypes.string.isRequired, user: PropTypes.shape({ username: PropTypes.string.isRequired - }).isRequired, + }).isRequired }); const ItemsShape = PropTypes.shape({ @@ -122,14 +124,16 @@ CollectionList.propTypes = { getProject: PropTypes.func.isRequired, addToCollection: PropTypes.func.isRequired, removeFromCollection: PropTypes.func.isRequired, - collections: PropTypes.arrayOf(PropTypes.shape({ - id: PropTypes.string.isRequired, - name: PropTypes.string.isRequired, - description: PropTypes.string, - createdAt: PropTypes.string.isRequired, - updatedAt: PropTypes.string.isRequired, - items: PropTypes.arrayOf(ItemsShape), - })).isRequired, + collections: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + description: PropTypes.string, + createdAt: PropTypes.string.isRequired, + updatedAt: PropTypes.string.isRequired, + items: PropTypes.arrayOf(ItemsShape) + }) + ).isRequired, username: PropTypes.string, loading: PropTypes.bool.isRequired, project: PropTypes.shape({ @@ -156,15 +160,24 @@ function mapStateToProps(state, ownProps) { sorting: state.sorting, loading: state.loading, project: ownProps.project || state.project, - projectId: ownProps && ownProps.params ? ownProps.prams.project_id : null, + projectId: ownProps && ownProps.params ? ownProps.prams.project_id : null }; } function mapDispatchToProps(dispatch) { return bindActionCreators( - Object.assign({}, CollectionsActions, ProjectsActions, ProjectActions, ToastActions, SortingActions), + Object.assign( + {}, + CollectionsActions, + ProjectsActions, + ProjectActions, + ToastActions, + SortingActions + ), dispatch ); } -export default withTranslation()(connect(mapStateToProps, mapDispatchToProps)(CollectionList)); +export default withTranslation()( + connect(mapStateToProps, mapDispatchToProps)(CollectionList) +); diff --git a/client/modules/IDE/components/AddToCollectionSketchList.jsx b/client/modules/IDE/components/AddToCollectionSketchList.jsx index 339ead18af..175abde107 100644 --- a/client/modules/IDE/components/AddToCollectionSketchList.jsx +++ b/client/modules/IDE/components/AddToCollectionSketchList.jsx @@ -19,14 +19,17 @@ class SketchList extends React.Component { this.props.getProjects(this.props.username); this.state = { - isInitialDataLoad: true, + isInitialDataLoad: true }; } componentWillReceiveProps(nextProps) { - if (this.props.sketches !== nextProps.sketches && Array.isArray(nextProps.sketches)) { + if ( + this.props.sketches !== nextProps.sketches && + Array.isArray(nextProps.sketches) + ) { this.setState({ - isInitialDataLoad: false, + isInitialDataLoad: false }); } } @@ -35,25 +38,29 @@ class SketchList extends React.Component { if (this.props.username === this.props.user.username) { return this.props.t('AddToCollectionSketchList.Title'); } - return this.props.t('AddToCollectionSketchList.AnothersTitle', { anotheruser: this.props.username }); + return this.props.t('AddToCollectionSketchList.AnothersTitle', { + anotheruser: this.props.username + }); } handleCollectionAdd = (sketch) => { this.props.addToCollection(this.props.collection.id, sketch.id); - } + }; handleCollectionRemove = (sketch) => { this.props.removeFromCollection(this.props.collection.id, sketch.id); - } + }; - inCollection = sketch => this.props.collection.items.find(item => item.project.id === sketch.id) != null + inCollection = (sketch) => + this.props.collection.items.find((item) => item.project.id === sketch.id) != + null; render() { const hasSketches = this.props.sketches.length > 0; - const sketchesWithAddedStatus = this.props.sketches.map(sketch => ({ + const sketchesWithAddedStatus = this.props.sketches.map((sketch) => ({ ...sketch, isAdded: this.inCollection(sketch), - url: `/${this.props.username}/sketches/${sketch.id}`, + url: `/${this.props.username}/sketches/${sketch.id}` })); let content = null; @@ -91,20 +98,24 @@ SketchList.propTypes = { authenticated: PropTypes.bool.isRequired }).isRequired, getProjects: PropTypes.func.isRequired, - sketches: PropTypes.arrayOf(PropTypes.shape({ - id: PropTypes.string.isRequired, - name: PropTypes.string.isRequired, - createdAt: PropTypes.string.isRequired, - updatedAt: PropTypes.string.isRequired - })).isRequired, + sketches: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + createdAt: PropTypes.string.isRequired, + updatedAt: PropTypes.string.isRequired + }) + ).isRequired, collection: PropTypes.shape({ id: PropTypes.string.isRequired, name: PropTypes.string.isRequired, - items: PropTypes.arrayOf(PropTypes.shape({ - project: PropTypes.shape({ - id: PropTypes.string.isRequired, - }), - })), + items: PropTypes.arrayOf( + PropTypes.shape({ + project: PropTypes.shape({ + id: PropTypes.string.isRequired + }) + }) + ) }).isRequired, username: PropTypes.string, loading: PropTypes.bool.isRequired, @@ -133,9 +144,17 @@ function mapStateToProps(state) { function mapDispatchToProps(dispatch) { return bindActionCreators( - Object.assign({}, ProjectsActions, CollectionsActions, ToastActions, SortingActions), + Object.assign( + {}, + ProjectsActions, + CollectionsActions, + ToastActions, + SortingActions + ), dispatch ); } -export default withTranslation()(connect(mapStateToProps, mapDispatchToProps)(SketchList)); +export default withTranslation()( + connect(mapStateToProps, mapDispatchToProps)(SketchList) +); diff --git a/client/modules/IDE/components/AssetList.jsx b/client/modules/IDE/components/AssetList.jsx index 8a6244193b..a77a0d6d66 100644 --- a/client/modules/IDE/components/AssetList.jsx +++ b/client/modules/IDE/components/AssetList.jsx @@ -22,7 +22,7 @@ class AssetListRowBase extends React.Component { onFocusComponent = () => { this.setState({ isFocused: true }); - } + }; onBlurComponent = () => { this.setState({ isFocused: false }); @@ -31,19 +31,19 @@ class AssetListRowBase extends React.Component { this.closeOptions(); } }, 200); - } + }; openOptions = () => { this.setState({ optionsOpen: true }); - } + }; closeOptions = () => { this.setState({ optionsOpen: false }); - } + }; toggleOptions = () => { if (this.state.optionsOpen) { @@ -51,12 +51,12 @@ class AssetListRowBase extends React.Component { } else { this.openOptions(); } - } + }; handleDropdownOpen = () => { this.closeOptions(); this.openOptions(); - } + }; handleAssetDelete = () => { const { key, name } = this.props.asset; @@ -64,7 +64,7 @@ class AssetListRowBase extends React.Component { if (window.confirm(this.props.t('Common.DeleteConfirmation', { name }))) { this.props.deleteAssetRequest(key); } - } + }; render() { const { asset, username, t } = this.props; @@ -78,7 +78,11 @@ class AssetListRowBase extends React.Component { </th> <td>{prettyBytes(asset.size)}</td> <td> - { asset.sketchId && <Link to={`/${username}/sketches/${asset.sketchId}`}>{asset.sketchName}</Link> } + {asset.sketchId && ( + <Link to={`/${username}/sketches/${asset.sketchId}`}> + {asset.sketchName} + </Link> + )} </td> <td className="asset-table__dropdown-column"> <button @@ -90,10 +94,8 @@ class AssetListRowBase extends React.Component { > <DownFilledTriangleIcon focusable="false" aria-hidden="true" /> </button> - {optionsOpen && - <ul - className="asset-table__action-dialogue" - > + {optionsOpen && ( + <ul className="asset-table__action-dialogue"> <li> <button className="asset-table__action-option" @@ -115,7 +117,8 @@ class AssetListRowBase extends React.Component { {t('AssetList.OpenNewTab')} </Link> </li> - </ul>} + </ul> + )} </td> </tr> ); @@ -146,7 +149,10 @@ function mapDispatchToPropsAssetListRow(dispatch) { return bindActionCreators(AssetActions, dispatch); } -const AssetListRow = connect(mapStateToPropsAssetListRow, mapDispatchToPropsAssetListRow)(AssetListRowBase); +const AssetListRow = connect( + mapStateToPropsAssetListRow, + mapDispatchToPropsAssetListRow +)(AssetListRowBase); class AssetList extends React.Component { constructor(props) { @@ -169,7 +175,11 @@ class AssetList extends React.Component { renderEmptyTable() { if (!this.props.loading && this.props.assetList.length === 0) { - return (<p className="asset-table__empty">{this.props.t('AssetList.NoUploadedAssets')}</p>); + return ( + <p className="asset-table__empty"> + {this.props.t('AssetList.NoUploadedAssets')} + </p> + ); } return null; } @@ -183,7 +193,7 @@ class AssetList extends React.Component { </Helmet> {this.renderLoader()} {this.renderEmptyTable()} - {this.hasAssets() && + {this.hasAssets() && ( <table className="asset-table"> <thead> <tr> @@ -194,9 +204,12 @@ class AssetList extends React.Component { </tr> </thead> <tbody> - {assetList.map(asset => <AssetListRow asset={asset} key={asset.key} t={t} />)} + {assetList.map((asset) => ( + <AssetListRow asset={asset} key={asset.key} t={t} /> + ))} </tbody> - </table>} + </table> + )} </article> ); } @@ -206,13 +219,15 @@ AssetList.propTypes = { user: PropTypes.shape({ username: PropTypes.string }).isRequired, - assetList: PropTypes.arrayOf(PropTypes.shape({ - key: PropTypes.string.isRequired, - name: PropTypes.string.isRequired, - url: PropTypes.string.isRequired, - sketchName: PropTypes.string, - sketchId: PropTypes.string - })).isRequired, + assetList: PropTypes.arrayOf( + PropTypes.shape({ + key: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + url: PropTypes.string.isRequired, + sketchName: PropTypes.string, + sketchId: PropTypes.string + }) + ).isRequired, getAssets: PropTypes.func.isRequired, loading: PropTypes.bool.isRequired, t: PropTypes.func.isRequired @@ -230,4 +245,6 @@ function mapDispatchToProps(dispatch) { return bindActionCreators(Object.assign({}, AssetActions), dispatch); } -export default withTranslation()(connect(mapStateToProps, mapDispatchToProps)(AssetList)); +export default withTranslation()( + connect(mapStateToProps, mapDispatchToProps)(AssetList) +); diff --git a/client/modules/IDE/components/AssetSize.jsx b/client/modules/IDE/components/AssetSize.jsx index 50764dd40a..bf168c17f7 100644 --- a/client/modules/IDE/components/AssetSize.jsx +++ b/client/modules/IDE/components/AssetSize.jsx @@ -41,13 +41,13 @@ const AssetSize = ({ totalSize }) => { }; AssetSize.propTypes = { - totalSize: PropTypes.number.isRequired, + totalSize: PropTypes.number.isRequired }; function mapStateToProps(state) { return { user: state.user, - totalSize: state.user.totalSize || state.assets.totalSize, + totalSize: state.user.totalSize || state.assets.totalSize }; } diff --git a/client/modules/IDE/components/CollectionList/CollectionList.jsx b/client/modules/IDE/components/CollectionList/CollectionList.jsx index bd1f30e10e..6fd5193004 100644 --- a/client/modules/IDE/components/CollectionList/CollectionList.jsx +++ b/client/modules/IDE/components/CollectionList/CollectionList.jsx @@ -22,7 +22,6 @@ import CollectionListRow from './CollectionListRow'; import ArrowUpIcon from '../../../../images/sort-arrow-up.svg'; import ArrowDownIcon from '../../../../images/sort-arrow-down.svg'; - class CollectionList extends React.Component { constructor(props) { super(props); @@ -36,7 +35,7 @@ class CollectionList extends React.Component { this.state = { hasLoadedData: false, - addingSketchesToCollectionId: null, + addingSketchesToCollectionId: null }; } @@ -44,7 +43,7 @@ class CollectionList extends React.Component { if (prevProps.loading === true && this.props.loading === false) { // eslint-disable-next-line react/no-did-update-set-state this.setState({ - hasLoadedData: true, + hasLoadedData: true }); } } @@ -53,23 +52,28 @@ class CollectionList extends React.Component { if (this.props.username === this.props.user.username) { return this.props.t('CollectionList.Title'); } - return this.props.t('CollectionList.AnothersTitle', { anotheruser: this.props.username }); + return this.props.t('CollectionList.AnothersTitle', { + anotheruser: this.props.username + }); } showAddSketches = (collectionId) => { this.setState({ - addingSketchesToCollectionId: collectionId, + addingSketchesToCollectionId: collectionId }); - } + }; hideAddSketches = () => { this.setState({ - addingSketchesToCollectionId: null, + addingSketchesToCollectionId: null }); - } + }; hasCollections() { - return (!this.props.loading || this.state.hasLoadedData) && this.props.collections.length > 0; + return ( + (!this.props.loading || this.state.hasLoadedData) && + this.props.collections.length > 0 + ); } _renderLoader() { @@ -79,7 +83,11 @@ class CollectionList extends React.Component { _renderEmptyTable() { if (!this.props.loading && this.props.collections.length === 0) { - return (<p className="sketches-table__empty">{this.props.t('CollectionList.NoCollections')}</p>); + return ( + <p className="sketches-table__empty"> + {this.props.t('CollectionList.NoCollections')} + </p> + ); } return null; } @@ -89,17 +97,25 @@ class CollectionList extends React.Component { let buttonLabel; if (field !== fieldName) { if (field === 'name') { - buttonLabel = this.props.t('CollectionList.ButtonLabelAscendingARIA', { displayName }); + buttonLabel = this.props.t('CollectionList.ButtonLabelAscendingARIA', { + displayName + }); } else { - buttonLabel = this.props.t('CollectionList.ButtonLabelDescendingARIA', { displayName }); + buttonLabel = this.props.t('CollectionList.ButtonLabelDescendingARIA', { + displayName + }); } } else if (direction === SortingActions.DIRECTION.ASC) { - buttonLabel = this.props.t('CollectionList.ButtonLabelDescendingARIA', { displayName }); + buttonLabel = this.props.t('CollectionList.ButtonLabelDescendingARIA', { + displayName + }); } else { - buttonLabel = this.props.t('CollectionList.ButtonLabelAscendingARIA', { displayName }); + buttonLabel = this.props.t('CollectionList.ButtonLabelAscendingARIA', { + displayName + }); } return buttonLabel; - } + }; _renderFieldHeader = (fieldName, displayName) => { const { field, direction } = this.props.sorting; @@ -116,19 +132,36 @@ class CollectionList extends React.Component { aria-label={buttonLabel} > <span className={headerClass}>{displayName}</span> - {field === fieldName && direction === SortingActions.DIRECTION.ASC && - <ArrowUpIcon role="img" aria-label={this.props.t('CollectionList.DirectionAscendingARIA')} focusable="false" /> - } - {field === fieldName && direction === SortingActions.DIRECTION.DESC && - <ArrowDownIcon role="img" aria-label={this.props.t('CollectionList.DirectionDescendingARIA')} focusable="false" /> - } + {field === fieldName && + direction === SortingActions.DIRECTION.ASC && ( + <ArrowUpIcon + role="img" + aria-label={this.props.t( + 'CollectionList.DirectionAscendingARIA' + )} + focusable="false" + /> + )} + {field === fieldName && + direction === SortingActions.DIRECTION.DESC && ( + <ArrowDownIcon + role="img" + aria-label={this.props.t( + 'CollectionList.DirectionDescendingARIA' + )} + focusable="false" + /> + )} </button> </th> ); - } + }; render() { - const username = this.props.username !== undefined ? this.props.username : this.props.user.username; + const username = + this.props.username !== undefined + ? this.props.username + : this.props.user.username; const { mobile } = this.props; return ( @@ -139,20 +172,41 @@ class CollectionList extends React.Component { {this._renderLoader()} {this._renderEmptyTable()} - {this.hasCollections() && - <table className="sketches-table" summary={this.props.t('CollectionList.TableSummary')}> + {this.hasCollections() && ( + <table + className="sketches-table" + summary={this.props.t('CollectionList.TableSummary')} + > <thead> <tr> - {this._renderFieldHeader('name', this.props.t('CollectionList.HeaderName'))} - {this._renderFieldHeader('createdAt', this.props.t('CollectionList.HeaderCreatedAt', { context: mobile ? 'mobile' : '' }))} - {this._renderFieldHeader('updatedAt', this.props.t('CollectionList.HeaderUpdatedAt', { context: mobile ? 'mobile' : '' }))} - {this._renderFieldHeader('numItems', this.props.t('CollectionList.HeaderNumItems', { context: mobile ? 'mobile' : '' }))} + {this._renderFieldHeader( + 'name', + this.props.t('CollectionList.HeaderName') + )} + {this._renderFieldHeader( + 'createdAt', + this.props.t('CollectionList.HeaderCreatedAt', { + context: mobile ? 'mobile' : '' + }) + )} + {this._renderFieldHeader( + 'updatedAt', + this.props.t('CollectionList.HeaderUpdatedAt', { + context: mobile ? 'mobile' : '' + }) + )} + {this._renderFieldHeader( + 'numItems', + this.props.t('CollectionList.HeaderNumItems', { + context: mobile ? 'mobile' : '' + }) + )} <th scope="col"></th> </tr> </thead> <tbody> - {this.props.collections.map(collection => - (<CollectionListRow + {this.props.collections.map((collection) => ( + <CollectionListRow mobile={mobile} key={collection.id} collection={collection} @@ -160,24 +214,26 @@ class CollectionList extends React.Component { username={username} project={this.props.project} onAddSketches={() => this.showAddSketches(collection.id)} - />))} + /> + ))} </tbody> - </table>} - { - this.state.addingSketchesToCollectionId && ( - <Overlay - title={this.props.t('CollectionList.AddSketch')} - actions={<SketchSearchbar />} - closeOverlay={this.hideAddSketches} - isFixedHeight - > - <AddToCollectionSketchList - username={this.props.username} - collection={find(this.props.collections, { id: this.state.addingSketchesToCollectionId })} - /> - </Overlay> - ) - } + </table> + )} + {this.state.addingSketchesToCollectionId && ( + <Overlay + title={this.props.t('CollectionList.AddSketch')} + actions={<SketchSearchbar />} + closeOverlay={this.hideAddSketches} + isFixedHeight + > + <AddToCollectionSketchList + username={this.props.username} + collection={find(this.props.collections, { + id: this.state.addingSketchesToCollectionId + })} + /> + </Overlay> + )} </article> ); } @@ -191,13 +247,15 @@ CollectionList.propTypes = { projectId: PropTypes.string, getCollections: PropTypes.func.isRequired, getProject: PropTypes.func.isRequired, - collections: PropTypes.arrayOf(PropTypes.shape({ - id: PropTypes.string.isRequired, - name: PropTypes.string.isRequired, - description: PropTypes.string, - createdAt: PropTypes.string.isRequired, - updatedAt: PropTypes.string.isRequired, - })).isRequired, + collections: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + description: PropTypes.string, + createdAt: PropTypes.string.isRequired, + updatedAt: PropTypes.string.isRequired + }) + ).isRequired, username: PropTypes.string, loading: PropTypes.bool.isRequired, toggleDirectionForField: PropTypes.func.isRequired, @@ -213,7 +271,7 @@ CollectionList.propTypes = { }) }), t: PropTypes.func.isRequired, - mobile: PropTypes.bool, + mobile: PropTypes.bool }; CollectionList.defaultProps = { @@ -233,15 +291,24 @@ function mapStateToProps(state, ownProps) { sorting: state.sorting, loading: state.loading, project: state.project, - projectId: ownProps && ownProps.params ? ownProps.params.project_id : null, + projectId: ownProps && ownProps.params ? ownProps.params.project_id : null }; } function mapDispatchToProps(dispatch) { return bindActionCreators( - Object.assign({}, CollectionsActions, ProjectsActions, ProjectActions, ToastActions, SortingActions), + Object.assign( + {}, + CollectionsActions, + ProjectsActions, + ProjectActions, + ToastActions, + SortingActions + ), dispatch ); } -export default withTranslation()(connect(mapStateToProps, mapDispatchToProps)(CollectionList)); +export default withTranslation()( + connect(mapStateToProps, mapDispatchToProps)(CollectionList) +); diff --git a/client/modules/IDE/components/CollectionList/CollectionListRow.jsx b/client/modules/IDE/components/CollectionList/CollectionListRow.jsx index 348968a0a3..bb5282027c 100644 --- a/client/modules/IDE/components/CollectionList/CollectionListRow.jsx +++ b/client/modules/IDE/components/CollectionList/CollectionListRow.jsx @@ -14,7 +14,9 @@ import DownFilledTriangleIcon from '../../../../images/down-filled-triangle.svg' class CollectionListRowBase extends React.Component { static projectInCollection(project, collection) { - return collection.items.find(item => item.project.id === project.id) != null; + return ( + collection.items.find((item) => item.project.id === project.id) != null + ); } constructor(props) { @@ -23,14 +25,14 @@ class CollectionListRowBase extends React.Component { optionsOpen: false, isFocused: false, renameOpen: false, - renameValue: '', + renameValue: '' }; this.renameInput = React.createRef(); } onFocusComponent = () => { this.setState({ isFocused: true }); - } + }; onBlurComponent = () => { this.setState({ isFocused: false }); @@ -39,19 +41,19 @@ class CollectionListRowBase extends React.Component { this.closeAll(); } }, 200); - } + }; openOptions = () => { this.setState({ optionsOpen: true }); - } + }; closeOptions = () => { this.setState({ optionsOpen: false }); - } + }; toggleOptions = () => { if (this.state.optionsOpen) { @@ -59,64 +61,75 @@ class CollectionListRowBase extends React.Component { } else { this.openOptions(); } - } + }; closeAll = () => { this.setState({ optionsOpen: false, - renameOpen: false, + renameOpen: false }); - } + }; handleAddSketches = () => { this.closeAll(); this.props.onAddSketches(); - } + }; handleDropdownOpen = () => { this.closeAll(); this.openOptions(); - } + }; handleCollectionDelete = () => { this.closeAll(); - if (window.confirm(this.props.t('Common.DeleteConfirmation', { name: this.props.collection.name }))) { + if ( + window.confirm( + this.props.t('Common.DeleteConfirmation', { + name: this.props.collection.name + }) + ) + ) { this.props.deleteCollection(this.props.collection.id); } - } + }; handleRenameOpen = () => { this.closeAll(); - this.setState({ - renameOpen: true, - renameValue: this.props.collection.name, - }, () => this.renameInput.current.focus()); - } + this.setState( + { + renameOpen: true, + renameValue: this.props.collection.name + }, + () => this.renameInput.current.focus() + ); + }; handleRenameChange = (e) => { this.setState({ renameValue: e.target.value }); - } + }; handleRenameEnter = (e) => { if (e.key === 'Enter') { this.updateName(); this.closeAll(); } - } + }; handleRenameBlur = () => { this.updateName(); this.closeAll(); - } + }; updateName = () => { const isValid = this.state.renameValue.trim().length !== 0; if (isValid) { - this.props.editCollection(this.props.collection.id, { name: this.state.renameValue.trim() }); + this.props.editCollection(this.props.collection.id, { + name: this.state.renameValue.trim() + }); } - } + }; renderActions = () => { const { optionsOpen } = this.state; @@ -129,14 +142,14 @@ class CollectionListRowBase extends React.Component { onClick={this.toggleOptions} onBlur={this.onBlurComponent} onFocus={this.onFocusComponent} - aria-label={this.props.t('CollectionListRow.ToggleCollectionOptionsARIA')} + aria-label={this.props.t( + 'CollectionListRow.ToggleCollectionOptionsARIA' + )} > <DownFilledTriangleIcon title="Menu" /> </button> - {optionsOpen && - <ul - className="sketch-list__action-dialogue" - > + {optionsOpen && ( + <ul className="sketch-list__action-dialogue"> <li> <button className="sketch-list__action-option" @@ -147,7 +160,7 @@ class CollectionListRowBase extends React.Component { {this.props.t('CollectionListRow.AddSketch')} </button> </li> - {userIsOwner && + {userIsOwner && ( <li> <button className="sketch-list__action-option" @@ -157,8 +170,9 @@ class CollectionListRowBase extends React.Component { > {this.props.t('CollectionListRow.Delete')} </button> - </li>} - {userIsOwner && + </li> + )} + {userIsOwner && ( <li> <button className="sketch-list__action-option" @@ -168,12 +182,13 @@ class CollectionListRowBase extends React.Component { > {this.props.t('CollectionListRow.Rename')} </button> - </li>} + </li> + )} </ul> - } + )} </React.Fragment> ); - } + }; renderCollectionName = () => { const { collection, username } = this.props; @@ -181,43 +196,51 @@ class CollectionListRowBase extends React.Component { return ( <React.Fragment> - <Link to={{ pathname: `/${username}/collections/${collection.id}`, state: { skipSavingPath: true } }}> + <Link + to={{ + pathname: `/${username}/collections/${collection.id}`, + state: { skipSavingPath: true } + }} + > {renameOpen ? '' : collection.name} </Link> - {renameOpen - && + {renameOpen && ( <input value={renameValue} onChange={this.handleRenameChange} onKeyUp={this.handleRenameEnter} onBlur={this.handleRenameBlur} - onClick={e => e.stopPropagation()} + onClick={(e) => e.stopPropagation()} ref={this.renameInput} /> - } + )} </React.Fragment> ); - } + }; render() { const { collection, mobile } = this.props; return ( - <tr - className="sketches-table__row" - key={collection.id} - > + <tr className="sketches-table__row" key={collection.id}> <th scope="row"> <span className="sketches-table__name"> {this.renderCollectionName()} </span> </th> - <td>{mobile && 'Created: '}{dates.format(collection.createdAt)}</td> - <td>{mobile && 'Updated: '}{dates.format(collection.updatedAt)}</td> - <td>{mobile && '# sketches: '}{(collection.items || []).length}</td> - <td className="sketch-list__dropdown-column"> - {this.renderActions()} + <td> + {mobile && 'Created: '} + {dates.format(collection.createdAt)} + </td> + <td> + {mobile && 'Updated: '} + {dates.format(collection.updatedAt)} + </td> + <td> + {mobile && '# sketches: '} + {(collection.items || []).length} </td> + <td className="sketch-list__dropdown-column">{this.renderActions()}</td> </tr> ); } @@ -228,15 +251,17 @@ CollectionListRowBase.propTypes = { id: PropTypes.string.isRequired, name: PropTypes.string.isRequired, owner: PropTypes.shape({ - username: PropTypes.string.isRequired, + username: PropTypes.string.isRequired }).isRequired, createdAt: PropTypes.string.isRequired, updatedAt: PropTypes.string.isRequired, - items: PropTypes.arrayOf(PropTypes.shape({ - project: PropTypes.shape({ - id: PropTypes.string.isRequired + items: PropTypes.arrayOf( + PropTypes.shape({ + project: PropTypes.shape({ + id: PropTypes.string.isRequired + }) }) - })) + ) }).isRequired, username: PropTypes.string.isRequired, user: PropTypes.shape({ @@ -251,11 +276,22 @@ CollectionListRowBase.propTypes = { }; CollectionListRowBase.defaultProps = { - mobile: false, + mobile: false }; function mapDispatchToPropsSketchListRow(dispatch) { - return bindActionCreators(Object.assign({}, CollectionsActions, ProjectActions, IdeActions, ToastActions), dispatch); + return bindActionCreators( + Object.assign( + {}, + CollectionsActions, + ProjectActions, + IdeActions, + ToastActions + ), + dispatch + ); } -export default withTranslation()(connect(null, mapDispatchToPropsSketchListRow)(CollectionListRowBase)); +export default withTranslation()( + connect(null, mapDispatchToPropsSketchListRow)(CollectionListRowBase) +); diff --git a/client/modules/IDE/components/Console.jsx b/client/modules/IDE/components/Console.jsx index eaee94929a..14e4e8d922 100644 --- a/client/modules/IDE/components/Console.jsx +++ b/client/modules/IDE/components/Console.jsx @@ -8,8 +8,10 @@ import { useSelector, useDispatch } from 'react-redux'; import classNames from 'classnames'; import { Console as ConsoleFeed } from 'console-feed'; import { - CONSOLE_FEED_WITHOUT_ICONS, CONSOLE_FEED_LIGHT_STYLES, - CONSOLE_FEED_DARK_STYLES, CONSOLE_FEED_CONTRAST_STYLES + CONSOLE_FEED_WITHOUT_ICONS, + CONSOLE_FEED_LIGHT_STYLES, + CONSOLE_FEED_DARK_STYLES, + CONSOLE_FEED_CONTRAST_STYLES } from '../../../styles/components/_console-feed.scss'; import warnLightUrl from '../../../images/console-warn-light.svg?byUrl'; import warnDarkUrl from '../../../images/console-warn-dark.svg?byUrl'; @@ -56,7 +58,7 @@ const getConsoleFeedStyle = (theme, times, fontSize) => { BASE_FONT_SIZE: fontSize, ARROW_FONT_SIZE: fontSize, LOG_ICON_WIDTH: fontSize, - LOG_ICON_HEIGHT: 1.45 * fontSize, + LOG_ICON_HEIGHT: 1.45 * fontSize }; if (times > 1) { @@ -64,23 +66,41 @@ const getConsoleFeedStyle = (theme, times, fontSize) => { } switch (theme) { case 'light': - return Object.assign(CONSOLE_FEED_LIGHT_STYLES, CONSOLE_FEED_LIGHT_ICONS, CONSOLE_FEED_SIZES, style); + return Object.assign( + CONSOLE_FEED_LIGHT_STYLES, + CONSOLE_FEED_LIGHT_ICONS, + CONSOLE_FEED_SIZES, + style + ); case 'dark': - return Object.assign(CONSOLE_FEED_DARK_STYLES, CONSOLE_FEED_DARK_ICONS, CONSOLE_FEED_SIZES, style); + return Object.assign( + CONSOLE_FEED_DARK_STYLES, + CONSOLE_FEED_DARK_ICONS, + CONSOLE_FEED_SIZES, + style + ); case 'contrast': - return Object.assign(CONSOLE_FEED_CONTRAST_STYLES, CONSOLE_FEED_CONTRAST_ICONS, CONSOLE_FEED_SIZES, style); + return Object.assign( + CONSOLE_FEED_CONTRAST_STYLES, + CONSOLE_FEED_CONTRAST_ICONS, + CONSOLE_FEED_SIZES, + style + ); default: return ''; } }; const Console = ({ t }) => { - const consoleEvents = useSelector(state => state.console); - const isExpanded = useSelector(state => state.ide.consoleIsExpanded); - const { theme, fontSize } = useSelector(state => state.preferences); + const consoleEvents = useSelector((state) => state.console); + const isExpanded = useSelector((state) => state.ide.consoleIsExpanded); + const { theme, fontSize } = useSelector((state) => state.preferences); const { - collapseConsole, expandConsole, clearConsole, dispatchConsoleEvent + collapseConsole, + expandConsole, + clearConsole, + dispatchConsoleEvent } = bindActionCreators({ ...IDEActions, ...ConsoleActions }, useDispatch()); useDidUpdate(() => { @@ -90,7 +110,9 @@ const Console = ({ t }) => { const cm = useRef({}); - useDidUpdate(() => { cm.current.scrollTop = cm.current.scrollHeight; }); + useDidUpdate(() => { + cm.current.scrollTop = cm.current.scrollHeight; + }); const consoleClass = classNames({ 'preview-console': true, @@ -98,11 +120,15 @@ const Console = ({ t }) => { }); return ( - <section className={consoleClass} > + <section className={consoleClass}> <header className="preview-console__header"> <h2 className="preview-console__header-title">{t('Console.Title')}</h2> <div className="preview-console__header-buttons"> - <button className="preview-console__clear" onClick={clearConsole} aria-label={t('Console.ClearARIA')}> + <button + className="preview-console__clear" + onClick={clearConsole} + aria-label={t('Console.ClearARIA')} + > {t('Console.Clear')} </button> <button @@ -112,7 +138,11 @@ const Console = ({ t }) => { > <DownArrowIcon focusable="false" aria-hidden="true" /> </button> - <button className="preview-console__expand" onClick={expandConsole} aria-label={t('Console.OpenARIA')} > + <button + className="preview-console__expand" + onClick={expandConsole} + aria-label={t('Console.OpenARIA')} + > <UpArrowIcon focusable="false" aria-hidden="true" /> </button> </div> @@ -121,15 +151,18 @@ const Console = ({ t }) => { {consoleEvents.map((consoleEvent) => { const { method, times } = consoleEvent; return ( - <div key={consoleEvent.id} className={`preview-console__message preview-console__message--${method}`}> - { times > 1 && - <div - className="preview-console__logged-times" - style={{ fontSize, borderRadius: fontSize / 2 }} - > - {times} - </div> - } + <div + key={consoleEvent.id} + className={`preview-console__message preview-console__message--${method}`} + > + {times > 1 && ( + <div + className="preview-console__logged-times" + style={{ fontSize, borderRadius: fontSize / 2 }} + > + {times} + </div> + )} <ConsoleFeed styles={getConsoleFeedStyle(theme, times, fontSize)} logs={[consoleEvent]} @@ -143,8 +176,7 @@ const Console = ({ t }) => { }; Console.propTypes = { - t: PropTypes.func.isRequired, + t: PropTypes.func.isRequired }; - export default withTranslation()(Console); diff --git a/client/modules/IDE/components/CopyableInput.jsx b/client/modules/IDE/components/CopyableInput.jsx index e471a5e983..1fa9f6bec3 100644 --- a/client/modules/IDE/components/CopyableInput.jsx +++ b/client/modules/IDE/components/CopyableInput.jsx @@ -33,11 +33,7 @@ class CopyableInput extends React.Component { } render() { - const { - label, - value, - hasPreviewLink - } = this.props; + const { label, value, hasPreviewLink } = this.props; const copyableInputClass = classNames({ 'copyable-input': true, 'copyable-input--with-preview': hasPreviewLink @@ -47,24 +43,29 @@ class CopyableInput extends React.Component { <div className="copyable-input__value-container tooltipped-no-delay" aria-label={this.props.t('CopyableInput.CopiedARIA')} - ref={(element) => { this.tooltip = element; }} + ref={(element) => { + this.tooltip = element; + }} onMouseLeave={this.onMouseLeaveHandler} > - <label className="copyable-input__label" htmlFor={`copyable-input__value-${label}`}> - <div className="copyable-input__label-container"> - {label} - </div> + <label + className="copyable-input__label" + htmlFor={`copyable-input__value-${label}`} + > + <div className="copyable-input__label-container">{label}</div> <input type="text" className="copyable-input__value" id={`copyable-input__value-${label}`} value={value} - ref={(element) => { this.input = element; }} + ref={(element) => { + this.input = element; + }} readOnly /> </label> </div> - {hasPreviewLink && + {hasPreviewLink && ( <a target="_blank" rel="noopener noreferrer" @@ -74,7 +75,7 @@ class CopyableInput extends React.Component { > <ShareIcon focusable="false" aria-hidden="true" /> </a> - } + )} </div> ); } diff --git a/client/modules/IDE/components/EditableInput.jsx b/client/modules/IDE/components/EditableInput.jsx index d71c11919d..871c6470a5 100644 --- a/client/modules/IDE/components/EditableInput.jsx +++ b/client/modules/IDE/components/EditableInput.jsx @@ -4,7 +4,6 @@ import { useTranslation } from 'react-i18next'; import i18next from 'i18next'; import EditIcon from '../../../images/pencil.svg'; - // TODO I think this needs a description prop so that it's accessible function EditableInput({ validate, @@ -12,7 +11,7 @@ function EditableInput({ emptyPlaceholder, InputComponent, inputProps, - onChange, + onChange }) { const [isEditing, setIsEditing] = React.useState(false); const [currentValue, setCurrentValue] = React.useState(value || ''); @@ -90,7 +89,7 @@ EditableInput.defaultProps = { InputComponent: 'input', inputProps: {}, validate: () => true, - value: '', + value: '' }; EditableInput.propTypes = { @@ -100,7 +99,7 @@ EditableInput.propTypes = { inputProps: PropTypes.object, // eslint-disable-line onChange: PropTypes.func.isRequired, validate: PropTypes.func, - value: PropTypes.string, + value: PropTypes.string }; export default EditableInput; diff --git a/client/modules/IDE/components/Editor.jsx b/client/modules/IDE/components/Editor.jsx index 6fa79fd236..2594564c80 100644 --- a/client/modules/IDE/components/Editor.jsx +++ b/client/modules/IDE/components/Editor.jsx @@ -36,7 +36,7 @@ import '../../../utils/p5-javascript'; import '../../../utils/webGL-clike'; import Timer from '../components/Timer'; import EditorAccessibility from '../components/EditorAccessibility'; -import { metaKey, } from '../../../utils/metaKey'; +import { metaKey } from '../../../utils/metaKey'; import '../../../utils/codemirror-search'; @@ -76,7 +76,7 @@ class Editor extends React.Component { this.props.clearLintMessage(); annotations.forEach((x) => { if (x.from.line > -1) { - this.props.updateLintMessage(x.severity, (x.from.line + 1), x.message); + this.props.updateLintMessage(x.severity, x.from.line + 1, x.message); } }); if (this.props.lintMessages.length > 0 && this.props.lintWarning) { @@ -109,22 +109,23 @@ class Editor extends React.Component { autoCloseBrackets: this.props.autocloseBracketsQuotes, styleSelectedText: true, lint: { - onUpdateLinting: ((annotations) => { + onUpdateLinting: (annotations) => { this.props.hideRuntimeErrorWarning(); this.updateLintingMessageAccessibility(annotations); - }), + }, options: { - 'asi': true, - 'eqeqeq': false, + "asi": true, + "eqeqeq": false, '-W041': false, - 'esversion': 7 + "esversion": 7 } } }); delete this._cm.options.lint.options.errors; - const replaceCommand = metaKey === 'Ctrl' ? `${metaKey}-H` : `${metaKey}-Option-F`; + const replaceCommand = + metaKey === 'Ctrl' ? `${metaKey}-H` : `${metaKey}-Option-F`; this._cm.setOption('extraKeys', { Tab: (cm) => { // might need to specify and indent more? @@ -140,23 +141,28 @@ class Editor extends React.Component { [`${metaKey}-F`]: 'findPersistent', [`${metaKey}-G`]: 'findNext', [`Shift-${metaKey}-G`]: 'findPrev', - replaceCommand: 'replace', + replaceCommand: 'replace' }); this.initializeDocuments(this.props.files); this._cm.swapDoc(this._docs[this.props.file.id]); - this._cm.on('change', debounce(() => { - this.props.setUnsavedChanges(true); - this.props.updateFileContent(this.props.file.id, this._cm.getValue()); - if (this.props.autorefresh && this.props.isPlaying) { - this.props.clearConsole(); - this.props.startRefreshSketch(); - } - }, 1000)); + this._cm.on( + 'change', + debounce(() => { + this.props.setUnsavedChanges(true); + this.props.updateFileContent(this.props.file.id, this._cm.getValue()); + if (this.props.autorefresh && this.props.isPlaying) { + this.props.clearConsole(); + this.props.startRefreshSketch(); + } + }, 1000) + ); this._cm.on('keyup', () => { - const temp = this.props.t('Editor.KeyUpLineNumber', { lineNumber: parseInt((this._cm.getCursor().line) + 1, 10) }); + const temp = this.props.t('Editor.KeyUpLineNumber', { + lineNumber: parseInt(this._cm.getCursor().line + 1, 10) + }); document.getElementById('current-line').innerHTML = temp; }); @@ -167,7 +173,9 @@ class Editor extends React.Component { } }); - this._cm.getWrapperElement().style['font-size'] = `${this.props.fontSize}px`; + this._cm.getWrapperElement().style[ + 'font-size' + ] = `${this.props.fontSize}px`; this.props.provideController({ tidyCode: this.tidyCode, @@ -191,8 +199,10 @@ class Editor extends React.Component { } componentDidUpdate(prevProps) { - if (this.props.file.content !== prevProps.file.content && - this.props.file.content !== this._cm.getValue()) { + if ( + this.props.file.content !== prevProps.file.content && + this.props.file.content !== this._cm.getValue() + ) { const oldDoc = this._cm.swapDoc(this._docs[this.props.file.id]); this._docs[prevProps.file.id] = oldDoc; this._cm.focus(); @@ -201,7 +211,9 @@ class Editor extends React.Component { } } if (this.props.fontSize !== prevProps.fontSize) { - this._cm.getWrapperElement().style['font-size'] = `${this.props.fontSize}px`; + this._cm.getWrapperElement().style[ + 'font-size' + ] = `${this.props.fontSize}px`; } if (this.props.linewrap !== prevProps.linewrap) { this._cm.setOption('lineWrapping', this.props.linewrap); @@ -212,8 +224,13 @@ class Editor extends React.Component { if (this.props.lineNumbers !== prevProps.lineNumbers) { this._cm.setOption('lineNumbers', this.props.lineNumbers); } - if (this.props.autocloseBracketsQuotes !== prevProps.autocloseBracketsQuotes) { - this._cm.setOption('autoCloseBrackets', this.props.autocloseBracketsQuotes); + if ( + this.props.autocloseBracketsQuotes !== prevProps.autocloseBracketsQuotes + ) { + this._cm.setOption( + 'autoCloseBrackets', + this.props.autocloseBracketsQuotes + ); } if (prevProps.consoleEvents !== this.props.consoleEvents) { @@ -225,18 +242,28 @@ class Editor extends React.Component { if (this.props.runtimeErrorWarningVisible) { this.props.consoleEvents.forEach((consoleEvent) => { if (consoleEvent.method === 'error') { - if (consoleEvent.data && + if ( + consoleEvent.data && consoleEvent.data[0] && consoleEvent.data[0].indexOf && - consoleEvent.data[0].indexOf(')') > -1) { + consoleEvent.data[0].indexOf(')') > -1 + ) { const n = consoleEvent.data[0].replace(')', '').split(' '); const lineNumber = parseInt(n[n.length - 1], 10) - 1; const { source } = consoleEvent; const fileName = this.props.file.name; - const errorFromJavaScriptFile = (`${source}.js` === fileName); - const errorFromIndexHTML = ((source === fileName) && (fileName === 'index.html')); - if (!Number.isNaN(lineNumber) && (errorFromJavaScriptFile || errorFromIndexHTML)) { - this._cm.addLineClass(lineNumber, 'background', 'line-runtime-error'); + const errorFromJavaScriptFile = `${source}.js` === fileName; + const errorFromIndexHTML = + source === fileName && fileName === 'index.html'; + if ( + !Number.isNaN(lineNumber) && + (errorFromJavaScriptFile || errorFromIndexHTML) + ) { + this._cm.addLineClass( + lineNumber, + 'background', + 'line-runtime-error' + ); } } } @@ -299,14 +326,23 @@ class Editor extends React.Component { const mode = this._cm.getOption('mode'); const currentPosition = this._cm.doc.getCursor(); if (mode === 'javascript') { - this._cm.doc.setValue(beautifyJS(this._cm.doc.getValue(), beautifyOptions)); + this._cm.doc.setValue( + beautifyJS(this._cm.doc.getValue(), beautifyOptions) + ); } else if (mode === 'css') { - this._cm.doc.setValue(beautifyCSS(this._cm.doc.getValue(), beautifyOptions)); + this._cm.doc.setValue( + beautifyCSS(this._cm.doc.getValue(), beautifyOptions) + ); } else if (mode === 'htmlmixed') { - this._cm.doc.setValue(beautifyHTML(this._cm.doc.getValue(), beautifyOptions)); + this._cm.doc.setValue( + beautifyHTML(this._cm.doc.getValue(), beautifyOptions) + ); } this._cm.focus(); - this._cm.doc.setCursor({ line: currentPosition.line, ch: currentPosition.ch + INDENTATION_AMOUNT }); + this._cm.doc.setCursor({ + line: currentPosition.line, + ch: currentPosition.ch + INDENTATION_AMOUNT + }); } initializeDocuments(files) { @@ -329,18 +365,19 @@ class Editor extends React.Component { render() { const editorSectionClass = classNames({ - 'editor': true, + "editor": true, 'sidebar--contracted': !this.props.isExpanded, 'editor--options': this.props.editorOptionsVisible }); const editorHolderClass = classNames({ 'editor-holder': true, - 'editor-holder--hidden': this.props.file.fileType === 'folder' || this.props.file.url + 'editor-holder--hidden': + this.props.file.fileType === 'folder' || this.props.file.url }); return ( - <section className={editorSectionClass} > + <section className={editorSectionClass}> <header className="editor__header"> <button aria-label={this.props.t('Editor.OpenSketchARIA')} @@ -360,9 +397,13 @@ class Editor extends React.Component { <span> {this.props.file.name} <span className="editor__unsaved-changes"> - {this.props.unsavedChanges ? - <UnsavedChangesDotIcon role="img" aria-label={this.props.t('Editor.UnsavedChangesARIA')} focusable="false" /> : - null} + {this.props.unsavedChanges ? ( + <UnsavedChangesDotIcon + role="img" + aria-label={this.props.t('Editor.UnsavedChangesARIA')} + focusable="false" + /> + ) : null} </span> </span> <Timer @@ -371,11 +412,14 @@ class Editor extends React.Component { /> </div> </header> - <article ref={(element) => { this.codemirrorContainer = element; }} className={editorHolderClass} > + <article + ref={(element) => { + this.codemirrorContainer = element; + }} + className={editorHolderClass} + > </article> - <EditorAccessibility - lintMessages={this.props.lintMessages} - /> + <EditorAccessibility lintMessages={this.props.lintMessages} /> </section> ); } @@ -386,16 +430,20 @@ Editor.propTypes = { lineNumbers: PropTypes.bool.isRequired, lintWarning: PropTypes.bool.isRequired, linewrap: PropTypes.bool.isRequired, - lintMessages: PropTypes.arrayOf(PropTypes.shape({ - severity: PropTypes.string.isRequired, - line: PropTypes.number.isRequired, - message: PropTypes.string.isRequired, - id: PropTypes.number.isRequired - })).isRequired, - consoleEvents: PropTypes.arrayOf(PropTypes.shape({ - method: PropTypes.string.isRequired, - args: PropTypes.arrayOf(PropTypes.string) - })), + lintMessages: PropTypes.arrayOf( + PropTypes.shape({ + severity: PropTypes.string.isRequired, + line: PropTypes.number.isRequired, + message: PropTypes.string.isRequired, + id: PropTypes.number.isRequired + }) + ).isRequired, + consoleEvents: PropTypes.arrayOf( + PropTypes.shape({ + method: PropTypes.string.isRequired, + args: PropTypes.arrayOf(PropTypes.string) + }) + ), updateLintMessage: PropTypes.func.isRequired, clearLintMessage: PropTypes.func.isRequired, updateFileContent: PropTypes.func.isRequired, @@ -417,11 +465,13 @@ Editor.propTypes = { theme: PropTypes.string.isRequired, unsavedChanges: PropTypes.bool.isRequired, projectSavedTime: PropTypes.string.isRequired, - files: PropTypes.arrayOf(PropTypes.shape({ - id: PropTypes.string.isRequired, - name: PropTypes.string.isRequired, - content: PropTypes.string.isRequired - })).isRequired, + files: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + content: PropTypes.string.isRequired + }) + ).isRequired, isExpanded: PropTypes.bool.isRequired, collapseSidebar: PropTypes.func.isRequired, expandSidebar: PropTypes.func.isRequired, @@ -435,17 +485,16 @@ Editor.propTypes = { }; Editor.defaultProps = { - consoleEvents: [], + consoleEvents: [] }; - function mapStateToProps(state) { return { files: state.files, file: - state.files.find(file => file.isSelectedFile) || - state.files.find(file => file.name === 'sketch.js') || - state.files.find(file => file.name !== 'root'), + state.files.find((file) => file.isSelectedFile) || + state.files.find((file) => file.name === 'sketch.js') || + state.files.find((file) => file.name !== 'root'), htmlFile: getHTMLFile(state.files), ide: state.ide, preferences: state.preferences, @@ -482,4 +531,6 @@ function mapDispatchToProps(dispatch) { ); } -export default withTranslation()(connect(mapStateToProps, mapDispatchToProps)(Editor)); +export default withTranslation()( + connect(mapStateToProps, mapDispatchToProps)(Editor) +); diff --git a/client/modules/IDE/components/EditorAccessibility.jsx b/client/modules/IDE/components/EditorAccessibility.jsx index ee7864463b..1a38d6af93 100644 --- a/client/modules/IDE/components/EditorAccessibility.jsx +++ b/client/modules/IDE/components/EditorAccessibility.jsx @@ -3,30 +3,39 @@ import React from 'react'; import { withTranslation } from 'react-i18next'; class EditorAccessibility extends React.Component { - componentDidMount() { - - } + componentDidMount() {} render() { const messages = []; if (this.props.lintMessages.length > 0) { this.props.lintMessages.forEach((lintMessage, i) => { - messages.push(( + messages.push( <li key={lintMessage.id}> {lintMessage.severity} in line - {lintMessage.line} : - {lintMessage.message} - </li>)); + {lintMessage.line} :{lintMessage.message} + </li> + ); }); } else { - messages.push(<li key={0}>{this.props.t('EditorAccessibility.NoLintMessages')}</li>); + messages.push( + <li key={0}>{this.props.t('EditorAccessibility.NoLintMessages')}</li> + ); } return ( <div className="editor-accessibility"> <ul className="editor-lintmessages" title="lint messages"> {messages} </ul> - <p> {this.props.t('EditorAccessibility.CurrentLine')} - <span className="editor-linenumber" aria-live="polite" aria-atomic="true" id="current-line"> </span> + <p> + {' '} + {this.props.t('EditorAccessibility.CurrentLine')} + <span + className="editor-linenumber" + aria-live="polite" + aria-atomic="true" + id="current-line" + > + {' '} + </span> </p> </div> ); @@ -34,12 +43,14 @@ class EditorAccessibility extends React.Component { } EditorAccessibility.propTypes = { - lintMessages: PropTypes.arrayOf(PropTypes.shape({ - severity: PropTypes.string.isRequired, - line: PropTypes.number.isRequired, - message: PropTypes.string.isRequired, - id: PropTypes.number.isRequired - })).isRequired, + lintMessages: PropTypes.arrayOf( + PropTypes.shape({ + severity: PropTypes.string.isRequired, + line: PropTypes.number.isRequired, + message: PropTypes.string.isRequired, + id: PropTypes.number.isRequired + }) + ).isRequired, t: PropTypes.func.isRequired }; diff --git a/client/modules/IDE/components/ErrorModal.jsx b/client/modules/IDE/components/ErrorModal.jsx index a1d0f8abc2..c0646c8f36 100644 --- a/client/modules/IDE/components/ErrorModal.jsx +++ b/client/modules/IDE/components/ErrorModal.jsx @@ -8,9 +8,15 @@ class ErrorModal extends React.Component { return ( <p> {this.props.t('ErrorModal.MessageLogin')} - <Link to="/login" onClick={this.props.closeModal}> {this.props.t('ErrorModal.Login')}</Link> + <Link to="/login" onClick={this.props.closeModal}> + {' '} + {this.props.t('ErrorModal.Login')} + </Link> {this.props.t('ErrorModal.LoginOr')} - <Link to="/signup" onClick={this.props.closeModal}>{this.props.t('ErrorModal.SignUp')}</Link>. + <Link to="/signup" onClick={this.props.closeModal}> + {this.props.t('ErrorModal.SignUp')} + </Link> + . </p> ); } @@ -32,17 +38,16 @@ class ErrorModal extends React.Component { return ( <p> {this.props.t('ErrorModal.MessageLoggedOut')} - <Link to="/login" onClick={this.props.closeModal}>{this.props.t('ErrorModal.LogIn')}</Link>. + <Link to="/login" onClick={this.props.closeModal}> + {this.props.t('ErrorModal.LogIn')} + </Link> + . </p> ); } staleProject() { - return ( - <p> - {this.props.t('ErrorModal.SavedDifferentWindow')} - </p> - ); + return <p>{this.props.t('ErrorModal.SavedDifferentWindow')}</p>; } render() { diff --git a/client/modules/IDE/components/Feedback.jsx b/client/modules/IDE/components/Feedback.jsx index 37ed571282..bcd5efea77 100644 --- a/client/modules/IDE/components/Feedback.jsx +++ b/client/modules/IDE/components/Feedback.jsx @@ -10,11 +10,10 @@ function Feedback(props) { <title>{this.props.t('Feedback.Title')}</title> </Helmet> <div className="feedback__content-pane"> - <h2 className="feedback__content-pane-header"> - Via Github Issues - </h2> + <h2 className="feedback__content-pane-header">Via Github Issues</h2> <p className="feedback__content-pane-copy"> - {'If you\'re familiar with Github, this is our preferred method for receiving bug reports and feedback.'} + If you're familiar with Github, this is our preferred method for + receiving bug reports and feedback. </p> <p className="feedback__content-pane-copy"> <a @@ -24,14 +23,16 @@ function Feedback(props) { className="feedback__github-link" > Go to Github - <GitHubLogo className="feedback__github-logo" focusable="false" aria-hidden="true" /> + <GitHubLogo + className="feedback__github-logo" + focusable="false" + aria-hidden="true" + /> </a> </p> </div> <div className="feedback__content-pane"> - <h2 className="feedback__content-pane-header"> - Via Google Form - </h2> + <h2 className="feedback__content-pane-header">Via Google Form</h2> <p className="feedback__content-pane-copy"> You can also submit this quick form. </p> @@ -40,7 +41,8 @@ function Feedback(props) { href="https://docs.google.com/forms/d/e/1FAIpQLSexU8W2EIhXjktl-_XzwjH6vgnelHirH4Yn4liN5BXltPWqBg/viewform" target="_blank" rel="noopener noreferrer" - >Go to Form + > + Go to Form </a> </p> </div> diff --git a/client/modules/IDE/components/FileNode.jsx b/client/modules/IDE/components/FileNode.jsx index c2d0271eea..18dc7d920c 100644 --- a/client/modules/IDE/components/FileNode.jsx +++ b/client/modules/IDE/components/FileNode.jsx @@ -50,15 +50,11 @@ function FileName({ name }) { return ( <span className="sidebar__file-item-name-text"> <span>{firstLetter}</span> - {baseName.length > 2 && + {baseName.length > 2 && ( <span className="sidebar__file-item-name--ellipsis">{middleText}</span> - } - {baseName.length > 1 && - <span>{lastLetter}</span> - } - {extension && - <span>{extension}</span> - } + )} + {baseName.length > 1 && <span>{lastLetter}</span>} + {extension && <span>{extension}</span>} </span> ); } @@ -82,7 +78,7 @@ class FileNode extends React.Component { onFocusComponent = () => { this.setState({ isFocused: true }); - } + }; onBlurComponent = () => { this.setState({ isFocused: false }); @@ -91,12 +87,11 @@ class FileNode extends React.Component { this.hideFileOptions(); } }, 200); - } - + }; setUpdatedName = (updatedName) => { this.setState({ updatedName }); - } + }; saveUpdatedFileName = () => { const { updatedName } = this.state; @@ -105,69 +100,74 @@ class FileNode extends React.Component { if (updatedName !== name) { updateFileName(id, updatedName); } - } + }; handleFileClick = (event) => { event.stopPropagation(); const { isDeleting } = this.state; - const { - id, setSelectedFile, name, onClickFile - } = this.props; + const { id, setSelectedFile, name, onClickFile } = this.props; if (name !== 'root' && !isDeleting) { setSelectedFile(id); } // debugger; // eslint-disable-line - if (onClickFile) { onClickFile(); } - } + if (onClickFile) { + onClickFile(); + } + }; handleFileNameChange = (event) => { const newName = event.target.value; this.setUpdatedName(newName); - } + }; handleFileNameBlur = () => { this.validateFileName(); this.hideEditFileName(); - } + }; handleClickRename = () => { this.setUpdatedName(this.props.name); this.showEditFileName(); setTimeout(() => this.fileNameInput.focus(), 0); setTimeout(() => this.hideFileOptions(), 0); - } + }; handleClickAddFile = () => { this.props.newFile(this.props.id); setTimeout(() => this.hideFileOptions(), 0); - } + }; handleClickAddFolder = () => { this.props.newFolder(this.props.id); setTimeout(() => this.hideFileOptions(), 0); - } + }; handleClickUploadFile = () => { this.props.openUploadFileModal(this.props.id); setTimeout(this.hideFileOptions, 0); - } + }; handleClickDelete = () => { - const prompt = this.props.t('Common.DeleteConfirmation', { name: this.props.name }); + const prompt = this.props.t('Common.DeleteConfirmation', { + name: this.props.name + }); if (window.confirm(prompt)) { this.setState({ isDeleting: true }); this.props.resetSelectedFile(this.props.id); - setTimeout(() => this.props.deleteFile(this.props.id, this.props.parentId), 100); + setTimeout( + () => this.props.deleteFile(this.props.id, this.props.parentId), + 100 + ); } - } + }; handleKeyPress = (event) => { if (event.key === 'Enter') { this.hideEditFileName(); } - } + }; validateFileName = () => { const currentName = this.props.name; @@ -177,16 +177,25 @@ class FileNode extends React.Component { const hasPeriod = updatedName.match(/\.+/); const hasNoExtension = oldFileExtension && !newFileExtension; const hasExtensionIfFolder = this.props.fileType === 'folder' && hasPeriod; - const notSameExtension = oldFileExtension && newFileExtension - && oldFileExtension[0].toLowerCase() !== newFileExtension[0].toLowerCase(); + const notSameExtension = + oldFileExtension && + newFileExtension && + oldFileExtension[0].toLowerCase() !== newFileExtension[0].toLowerCase(); const hasEmptyFilename = updatedName.trim() === ''; - const hasOnlyExtension = newFileExtension && updatedName.trim() === newFileExtension[0]; - if (hasEmptyFilename || hasNoExtension || notSameExtension || hasOnlyExtension || hasExtensionIfFolder) { + const hasOnlyExtension = + newFileExtension && updatedName.trim() === newFileExtension[0]; + if ( + hasEmptyFilename || + hasNoExtension || + notSameExtension || + hasOnlyExtension || + hasExtensionIfFolder + ) { this.setUpdatedName(currentName); } else { this.saveUpdatedFileName(); } - } + }; toggleFileOptions = (event) => { event.preventDefault(); @@ -199,33 +208,38 @@ class FileNode extends React.Component { this[`fileOptions-${this.props.id}`].focus(); this.setState({ isOptionsOpen: true }); } - } + }; hideFileOptions = () => { this.setState({ isOptionsOpen: false }); - } + }; showEditFileName = () => { this.setState({ isEditingName: true }); - } + }; hideEditFileName = () => { this.setState({ isEditingName: false }); - } + }; showFolderChildren = () => { this.props.showFolderChildren(this.props.id); - } + }; hideFolderChildren = () => { this.props.hideFolderChildren(this.props.id); - } + }; - renderChild = childId => ( + renderChild = (childId) => ( <li key={childId}> - <ConnectedFileNode id={childId} parentId={this.props.id} canEdit={this.props.canEdit} onClickFile={this.props.onClickFile} /> + <ConnectedFileNode + id={childId} + parentId={this.props.id} + canEdit={this.props.canEdit} + onClickFile={this.props.onClickFile} + /> </li> - ) + ); render() { const itemClass = classNames({ @@ -244,34 +258,45 @@ class FileNode extends React.Component { const { t } = this.props; return ( - <div className={itemClass} > - { !isRoot && - <div className="file-item__content" onContextMenu={this.toggleFileOptions}> + <div className={itemClass}> + {!isRoot && ( + <div + className="file-item__content" + onContextMenu={this.toggleFileOptions} + > <span className="file-item__spacer"></span> - { isFile && + {isFile && ( <span className="sidebar__file-item-icon"> <FileIcon focusable="false" aria-hidden="true" /> </span> - } - { isFolder && + )} + {isFolder && ( <div className="sidebar__file-item--folder"> <button className="sidebar__file-item-closed" onClick={this.showFolderChildren} aria-label={t('FileNode.OpenFolderARIA')} > - <FolderRightIcon className="folder-right" focusable="false" aria-hidden="true" /> + <FolderRightIcon + className="folder-right" + focusable="false" + aria-hidden="true" + /> </button> <button className="sidebar__file-item-open" onClick={this.hideFolderChildren} aria-label={t('FileNode.CloseFolderARIA')} > - <FolderDownIcon className="folder-down" focusable="false" aria-hidden="true" /> + <FolderDownIcon + className="folder-down" + focusable="false" + aria-hidden="true" + /> </button> </div> - } - <button + )} + <button aria-label={this.state.updatedName} className="sidebar__file-item-name" onClick={this.handleFileClick} @@ -286,14 +311,18 @@ class FileNode extends React.Component { value={this.state.updatedName} maxLength="128" onChange={this.handleFileNameChange} - ref={(element) => { this.fileNameInput = element; }} + ref={(element) => { + this.fileNameInput = element; + }} onBlur={this.handleFileNameBlur} onKeyPress={this.handleKeyPress} /> <button className="sidebar__file-item-show-options" aria-label={t('FileNode.ToggleFileOptionsARIA')} - ref={(element) => { this[`fileOptions-${this.props.id}`] = element; }} + ref={(element) => { + this[`fileOptions-${this.props.id}`] = element; + }} tabIndex="0" onClick={this.toggleFileOptions} onBlur={this.onBlurComponent} @@ -303,7 +332,7 @@ class FileNode extends React.Component { </button> <div className="sidebar__file-item-options"> <ul title="file options"> - { isFolder && + {isFolder && ( <React.Fragment> <li> <button @@ -327,7 +356,7 @@ class FileNode extends React.Component { {t('FileNode.AddFile')} </button> </li> - { this.props.authenticated && + {this.props.authenticated && ( <li> <button aria-label={t('FileNode.UploadFileARIA')} @@ -338,9 +367,9 @@ class FileNode extends React.Component { {t('FileNode.UploadFile')} </button> </li> - } + )} </React.Fragment> - } + )} <li> <button onClick={this.handleClickRename} @@ -364,12 +393,12 @@ class FileNode extends React.Component { </ul> </div> </div> - } - { this.props.children && + )} + {this.props.children && ( <ul className="file-item__children"> {this.props.children.map(this.renderChild)} </ul> - } + )} </div> ); } @@ -402,13 +431,18 @@ FileNode.defaultProps = { onClickFile: null, parentId: '0', isSelectedFile: false, - isFolderClosed: false, + isFolderClosed: false }; function mapStateToProps(state, ownProps) { // this is a hack, state is updated before ownProps - const fileNode = state.files.find(file => file.id === ownProps.id) || { name: 'test', fileType: 'file' }; - return Object.assign({}, fileNode, { authenticated: state.user.authenticated }); + const fileNode = state.files.find((file) => file.id === ownProps.id) || { + name: 'test', + fileType: 'file' + }; + return Object.assign({}, fileNode, { + authenticated: state.user.authenticated + }); } function mapDispatchToProps(dispatch) { @@ -417,9 +451,9 @@ function mapDispatchToProps(dispatch) { const TranslatedFileNode = withTranslation()(FileNode); -const ConnectedFileNode = connect(mapStateToProps, mapDispatchToProps)(TranslatedFileNode); +const ConnectedFileNode = connect( + mapStateToProps, + mapDispatchToProps +)(TranslatedFileNode); -export { - TranslatedFileNode as FileNode, - ConnectedFileNode as default -}; +export { TranslatedFileNode as FileNode, ConnectedFileNode as default }; diff --git a/client/modules/IDE/components/FileNode.test.jsx b/client/modules/IDE/components/FileNode.test.jsx index 84b14aa5ee..3f6706a2c0 100644 --- a/client/modules/IDE/components/FileNode.test.jsx +++ b/client/modules/IDE/components/FileNode.test.jsx @@ -1,6 +1,12 @@ import React from 'react'; -import { fireEvent, render, screen, waitFor, within } from '../../../test-utils'; +import { + fireEvent, + render, + screen, + waitFor, + within +} from '../../../test-utils'; import { FileNode } from './FileNode'; describe('<FileNode />', () => { @@ -36,7 +42,7 @@ describe('<FileNode />', () => { showFolderChildren: jest.fn(), hideFolderChildren: jest.fn(), openUploadFileModal: jest.fn(), - setProjectName: jest.fn(), + setProjectName: jest.fn() }; render(<FileNode {...props} />); @@ -60,7 +66,9 @@ describe('<FileNode />', () => { changeName(newName); - await waitFor(() => expect(props.updateFileName).toHaveBeenCalledWith(props.id, newName)); + await waitFor(() => + expect(props.updateFileName).toHaveBeenCalledWith(props.id, newName) + ); await expectFileNameToBe(newName); }); @@ -111,7 +119,9 @@ describe('<FileNode />', () => { changeName(newName); - await waitFor(() => expect(props.updateFileName).toHaveBeenCalledWith(props.id, newName)); + await waitFor(() => + expect(props.updateFileName).toHaveBeenCalledWith(props.id, newName) + ); await expectFileNameToBe(newName); }); diff --git a/client/modules/IDE/components/FileUploader.jsx b/client/modules/IDE/components/FileUploader.jsx index f33b59ed85..ac0014f987 100644 --- a/client/modules/IDE/components/FileUploader.jsx +++ b/client/modules/IDE/components/FileUploader.jsx @@ -8,8 +8,11 @@ import * as UploaderActions from '../actions/uploader'; import getConfig from '../../../utils/getConfig'; import { fileExtensionsAndMimeTypes } from '../../../../server/utils/fileUtils'; -const s3Bucket = getConfig('S3_BUCKET_URL_BASE') || - `https://s3-${getConfig('AWS_REGION')}.amazonaws.com/${getConfig('S3_BUCKET')}/`; +const s3Bucket = + getConfig('S3_BUCKET_URL_BASE') || + `https://s3-${getConfig('AWS_REGION')}.amazonaws.com/${getConfig( + 'S3_BUCKET' + )}/`; class FileUploader extends React.Component { componentDidMount() { @@ -18,7 +21,9 @@ class FileUploader extends React.Component { } createDropzone() { - const userId = this.props.project.owner ? this.props.project.owner.id : this.props.user.id; + const userId = this.props.project.owner + ? this.props.project.owner.id + : this.props.user.id; this.uploader = new Dropzone('div#uploader', { url: s3Bucket, method: 'post', @@ -43,9 +48,7 @@ class FileUploader extends React.Component { } render() { - return ( - <div id="uploader" className="uploader dropzone"></div> - ); + return <div id="uploader" className="uploader dropzone"></div>; } } @@ -86,4 +89,6 @@ function mapDispatchToProps(dispatch) { return bindActionCreators(UploaderActions, dispatch); } -export default withTranslation()(connect(mapStateToProps, mapDispatchToProps)(FileUploader)); +export default withTranslation()( + connect(mapStateToProps, mapDispatchToProps)(FileUploader) +); diff --git a/client/modules/IDE/components/KeyboardShortcutModal.jsx b/client/modules/IDE/components/KeyboardShortcutModal.jsx index 8977e4290f..3c26c48929 100644 --- a/client/modules/IDE/components/KeyboardShortcutModal.jsx +++ b/client/modules/IDE/components/KeyboardShortcutModal.jsx @@ -4,12 +4,23 @@ import { metaKeyName, metaKey } from '../../../utils/metaKey'; function KeyboardShortcutModal() { const { t } = useTranslation(); - const replaceCommand = metaKey === 'Ctrl' ? `${metaKeyName} + H` : `${metaKeyName} + ⌥ + F`; + const replaceCommand = + metaKey === 'Ctrl' ? `${metaKeyName} + H` : `${metaKeyName} + ⌥ + F`; return ( <div className="keyboard-shortcuts"> - <h3 className="keyboard-shortcuts__title">{t('KeyboardShortcuts.CodeEditing.CodeEditing')}</h3> + <h3 className="keyboard-shortcuts__title"> + {t('KeyboardShortcuts.CodeEditing.CodeEditing')} + </h3> <p className="keyboard-shortcuts__description"> - {t('KeyboardShortcuts.ShortcutsFollow')} <a href="https://shortcuts.design/toolspage-sublimetext.html" target="_blank" rel="noopener noreferrer">{t('KeyboardShortcuts.SublimeText')}</a>. + {t('KeyboardShortcuts.ShortcutsFollow')}{' '} + <a + href="https://shortcuts.design/toolspage-sublimetext.html" + target="_blank" + rel="noopener noreferrer" + > + {t('KeyboardShortcuts.SublimeText')} + </a> + . </p> <ul className="keyboard-shortcuts__list"> <li className="keyboard-shortcut-item"> @@ -17,54 +28,42 @@ function KeyboardShortcutModal() { <span>{t('KeyboardShortcuts.CodeEditing.Tidy')}</span> </li> <li className="keyboard-shortcut-item"> - <span className="keyboard-shortcut__command"> - {metaKeyName} + F - </span> + <span className="keyboard-shortcut__command">{metaKeyName} + F</span> <span>{t('KeyboardShortcuts.CodeEditing.FindText')}</span> </li> <li className="keyboard-shortcut-item"> - <span className="keyboard-shortcut__command"> - {metaKeyName} + G - </span> + <span className="keyboard-shortcut__command">{metaKeyName} + G</span> <span>{t('KeyboardShortcuts.CodeEditing.FindNextTextMatch')}</span> </li> <li className="keyboard-shortcut-item"> <span className="keyboard-shortcut__command"> {metaKeyName} + {'\u21E7'} + G </span> - <span>{t('KeyboardShortcuts.CodeEditing.FindPreviousTextMatch')}</span> + <span> + {t('KeyboardShortcuts.CodeEditing.FindPreviousTextMatch')} + </span> </li> <li className="keyboard-shortcut-item"> - <span className="keyboard-shortcut__command"> - {replaceCommand} - </span> + <span className="keyboard-shortcut__command">{replaceCommand}</span> <span>{t('KeyboardShortcuts.CodeEditing.ReplaceTextMatch')}</span> </li> <li className="keyboard-shortcut-item"> - <span className="keyboard-shortcut__command"> - {metaKeyName} + [ - </span> + <span className="keyboard-shortcut__command">{metaKeyName} + [</span> <span>{t('KeyboardShortcuts.CodeEditing.IndentCodeLeft')}</span> </li> <li className="keyboard-shortcut-item"> - <span className="keyboard-shortcut__command"> - {metaKeyName} + ] - </span> + <span className="keyboard-shortcut__command">{metaKeyName} + ]</span> <span>{t('KeyboardShortcuts.CodeEditing.IndentCodeRight')}</span> </li> <li className="keyboard-shortcut-item"> - <span className="keyboard-shortcut__command"> - {metaKeyName} + / - </span> + <span className="keyboard-shortcut__command">{metaKeyName} + /</span> <span>{t('KeyboardShortcuts.CodeEditing.CommentLine')}</span> </li> </ul> <h3 className="keyboard-shortcuts__title">General</h3> <ul className="keyboard-shortcuts__list"> <li className="keyboard-shortcut-item"> - <span className="keyboard-shortcut__command"> - {metaKeyName} + S - </span> + <span className="keyboard-shortcut__command">{metaKeyName} + S</span> <span>{t('Common.Save')}</span> </li> <li className="keyboard-shortcut-item"> diff --git a/client/modules/IDE/components/NewFileForm.jsx b/client/modules/IDE/components/NewFileForm.jsx index abc48ecf88..2cc93566ae 100644 --- a/client/modules/IDE/components/NewFileForm.jsx +++ b/client/modules/IDE/components/NewFileForm.jsx @@ -33,21 +33,12 @@ function NewFileForm() { }); return ( - <Form - fields={['name']} - validate={validate} - onSubmit={onSubmit} - > - {({ - handleSubmit, errors, touched, invalid, submitting - }) => ( - <form - className="new-file-form" - onSubmit={handleSubmit} - > + <Form fields={['name']} validate={validate} onSubmit={onSubmit}> + {({ handleSubmit, errors, touched, invalid, submitting }) => ( + <form className="new-file-form" onSubmit={handleSubmit}> <div className="new-file-form__input-wrapper"> <Field name="name"> - {field => ( + {(field) => ( <React.Fragment> <label className="new-file-form__name-label" htmlFor="name"> Name: @@ -64,10 +55,8 @@ function NewFileForm() { </React.Fragment> )} </Field> - <Button - type="submit" - disabled={invalid || submitting} - >{t('NewFileForm.AddFileSubmit')} + <Button type="submit" disabled={invalid || submitting}> + {t('NewFileForm.AddFileSubmit')} </Button> </div> {touched.name && errors.name && ( diff --git a/client/modules/IDE/components/NewFileModal.jsx b/client/modules/IDE/components/NewFileModal.jsx index c140244d57..d85cf44ea2 100644 --- a/client/modules/IDE/components/NewFileModal.jsx +++ b/client/modules/IDE/components/NewFileModal.jsx @@ -7,7 +7,6 @@ import NewFileForm from './NewFileForm'; import { closeNewFileModal } from '../actions/ide'; import ExitIcon from '../../../images/exit.svg'; - // At some point this will probably be generalized to a generic modal // in which you can insert different content // but for now, let's just make this work @@ -40,10 +39,17 @@ class NewFileModal extends React.Component { render() { return ( - <section className="modal" ref={(element) => { this.modal = element; }}> + <section + className="modal" + ref={(element) => { + this.modal = element; + }} + > <div className="modal-content"> <div className="modal__header"> - <h2 className="modal__title">{this.props.t('NewFileModal.Title')}</h2> + <h2 className="modal__title"> + {this.props.t('NewFileModal.Title')} + </h2> <button className="modal__exit-button" onClick={this.props.closeNewFileModal} @@ -52,9 +58,7 @@ class NewFileModal extends React.Component { <ExitIcon focusable="false" aria-hidden="true" /> </button> </div> - <NewFileForm - focusOnModal={this.focusOnModal} - /> + <NewFileForm focusOnModal={this.focusOnModal} /> </div> </section> ); @@ -74,4 +78,6 @@ function mapDispatchToProps(dispatch) { return bindActionCreators({ closeNewFileModal }, dispatch); } -export default withTranslation()(connect(mapStateToProps, mapDispatchToProps)(NewFileModal)); +export default withTranslation()( + connect(mapStateToProps, mapDispatchToProps)(NewFileModal) +); diff --git a/client/modules/IDE/components/NewFolderForm.jsx b/client/modules/IDE/components/NewFolderForm.jsx index 44c272f237..bc305b8b18 100644 --- a/client/modules/IDE/components/NewFolderForm.jsx +++ b/client/modules/IDE/components/NewFolderForm.jsx @@ -30,21 +30,12 @@ function NewFolderForm() { } return ( - <Form - fields={['name']} - validate={validate} - onSubmit={onSubmit} - > - {({ - handleSubmit, invalid, submitting, touched, errors - }) => ( - <form - className="new-folder-form" - onSubmit={handleSubmit} - > + <Form fields={['name']} validate={validate} onSubmit={onSubmit}> + {({ handleSubmit, invalid, submitting, touched, errors }) => ( + <form className="new-folder-form" onSubmit={handleSubmit}> <div className="new-folder-form__input-wrapper"> <Field name="name"> - {field => ( + {(field) => ( <React.Fragment> <label className="new-folder-form__name-label" htmlFor="name"> Name: @@ -61,17 +52,14 @@ function NewFolderForm() { </React.Fragment> )} </Field> - <Button - type="submit" - disabled={invalid || submitting} - >{t('NewFolderForm.AddFolderSubmit')} + <Button type="submit" disabled={invalid || submitting}> + {t('NewFolderForm.AddFolderSubmit')} </Button> </div> {touched.name && errors.name && ( <span className="form-error">{errors.name}</span> )} </form> - )} </Form> ); diff --git a/client/modules/IDE/components/NewFolderModal.jsx b/client/modules/IDE/components/NewFolderModal.jsx index ab66b2f1ae..fc5161ddc0 100644 --- a/client/modules/IDE/components/NewFolderModal.jsx +++ b/client/modules/IDE/components/NewFolderModal.jsx @@ -28,10 +28,17 @@ class NewFolderModal extends React.Component { render() { return ( - <section className="modal" ref={(element) => { this.newFolderModal = element; }} > + <section + className="modal" + ref={(element) => { + this.newFolderModal = element; + }} + > <div className="modal-content-folder"> <div className="modal__header"> - <h2 className="modal__title">{this.props.t('NewFolderModal.Title')}</h2> + <h2 className="modal__title"> + {this.props.t('NewFolderModal.Title')} + </h2> <button className="modal__exit-button" onClick={this.props.closeModal} diff --git a/client/modules/IDE/components/Preferences/PreferenceCreators.jsx b/client/modules/IDE/components/Preferences/PreferenceCreators.jsx index 4a946ac2bf..53530f72eb 100644 --- a/client/modules/IDE/components/Preferences/PreferenceCreators.jsx +++ b/client/modules/IDE/components/Preferences/PreferenceCreators.jsx @@ -4,23 +4,32 @@ export const optionsOnOff = (name) => { const { t } = useTranslation(); return [ { - value: true, label: t('PreferenceCreators.On'), ariaLabel: `${name} on`, name: `${name}`, id: `${name}-on`.replace(' ', '-') + value: true, + label: t('PreferenceCreators.On'), + ariaLabel: `${name} on`, + name: `${name}`, + id: `${name}-on`.replace(' ', '-') }, { - value: false, label: t('PreferenceCreators.Off'), ariaLabel: `${name} off`, name: `${name}`, id: `${name}-off`.replace(' ', '-') + value: false, + label: t('PreferenceCreators.Off'), + ariaLabel: `${name} off`, + name: `${name}`, + id: `${name}-off`.replace(' ', '-') } ]; }; -export const optionsPickOne = (name, ...options) => options.map(option => ({ - value: option, - label: option, - ariaLabel: `${option} ${name} on`, - name: `${option} ${name}`, - id: `${option}-${name}-on`.replace(' ', '-') -})); +export const optionsPickOne = (name, ...options) => + options.map((option) => ({ + value: option, + label: option, + ariaLabel: `${option} ${name} on`, + name: `${option} ${name}`, + id: `${option}-${name}-on`.replace(' ', '-') + })); -const nameToValueName = x => (x && x.toLowerCase().replace(/#|_|-/g, ' ')); +const nameToValueName = (x) => x && x.toLowerCase().replace(/#|_|-/g, ' '); // preferenceOnOff: name, value and onSelect are mandatory. propname is optional export const preferenceOnOff = (name, value, onSelect, propname) => ({ diff --git a/client/modules/IDE/components/Preferences/index.jsx b/client/modules/IDE/components/Preferences/index.jsx index b5998f5206..a6729add15 100644 --- a/client/modules/IDE/components/Preferences/index.jsx +++ b/client/modules/IDE/components/Preferences/index.jsx @@ -99,13 +99,23 @@ class Preferences extends React.Component { <Tabs> <TabList> <div className="tabs__titles"> - <Tab><h4 className="tabs__title">{this.props.t('Preferences.GeneralSettings')}</h4></Tab> - <Tab><h4 className="tabs__title">{this.props.t('Preferences.Accessibility')}</h4></Tab> + <Tab> + <h4 className="tabs__title"> + {this.props.t('Preferences.GeneralSettings')} + </h4> + </Tab> + <Tab> + <h4 className="tabs__title"> + {this.props.t('Preferences.Accessibility')} + </h4> + </Tab> </div> </TabList> <TabPanel> <div className="preference"> - <h4 className="preference__title">{this.props.t('Preferences.Theme')}</h4> + <h4 className="preference__title"> + {this.props.t('Preferences.Theme')} + </h4> <div className="preference__options"> <input type="radio" @@ -117,7 +127,9 @@ class Preferences extends React.Component { value="light" checked={this.props.theme === 'light'} /> - <label htmlFor="light-theme-on" className="preference__option">{this.props.t('Preferences.LightTheme')}</label> + <label htmlFor="light-theme-on" className="preference__option"> + {this.props.t('Preferences.LightTheme')} + </label> <input type="radio" onChange={() => this.props.setTheme('dark')} @@ -128,7 +140,9 @@ class Preferences extends React.Component { value="dark" checked={this.props.theme === 'dark'} /> - <label htmlFor="dark-theme-on" className="preference__option">{this.props.t('Preferences.DarkTheme')}</label> + <label htmlFor="dark-theme-on" className="preference__option"> + {this.props.t('Preferences.DarkTheme')} + </label> <input type="radio" onChange={() => this.props.setTheme('contrast')} @@ -139,11 +153,18 @@ class Preferences extends React.Component { value="contrast" checked={this.props.theme === 'contrast'} /> - <label htmlFor="high-contrast-theme-on" className="preference__option">{this.props.t('Preferences.HighContrastTheme')}</label> + <label + htmlFor="high-contrast-theme-on" + className="preference__option" + > + {this.props.t('Preferences.HighContrastTheme')} + </label> </div> </div> <div className="preference"> - <h4 className="preference__title">{this.props.t('Preferences.TextSize')}</h4> + <h4 className="preference__title"> + {this.props.t('Preferences.TextSize')} + </h4> <button className="preference__minus-button" onClick={this.decreaseFontSize} @@ -151,7 +172,9 @@ class Preferences extends React.Component { disabled={this.state.fontSize <= 8} > <MinusIcon focusable="false" aria-hidden="true" /> - <h6 className="preference__label">{this.props.t('Preferences.DecreaseFont')}</h6> + <h6 className="preference__label"> + {this.props.t('Preferences.DecreaseFont')} + </h6> </button> <form onSubmit={this.onFontInputSubmit}> <input @@ -161,8 +184,12 @@ class Preferences extends React.Component { value={this.state.fontSize} onChange={this.onFontInputChange} type="text" - ref={(element) => { this.fontSizeInput = element; }} - onClick={() => { this.fontSizeInput.select(); }} + ref={(element) => { + this.fontSizeInput = element; + }} + onClick={() => { + this.fontSizeInput.select(); + }} /> </form> <button @@ -172,11 +199,15 @@ class Preferences extends React.Component { disabled={this.state.fontSize >= 36} > <PlusIcon focusable="false" aria-hidden="true" /> - <h6 className="preference__label">{this.props.t('Preferences.IncreaseFont')}</h6> + <h6 className="preference__label"> + {this.props.t('Preferences.IncreaseFont')} + </h6> </button> </div> <div className="preference"> - <h4 className="preference__title">{this.props.t('Preferences.Autosave')}</h4> + <h4 className="preference__title"> + {this.props.t('Preferences.Autosave')} + </h4> <div className="preference__options"> <input type="radio" @@ -188,7 +219,9 @@ class Preferences extends React.Component { value="On" checked={this.props.autosave} /> - <label htmlFor="autosave-on" className="preference__option">{this.props.t('Preferences.On')}</label> + <label htmlFor="autosave-on" className="preference__option"> + {this.props.t('Preferences.On')} + </label> <input type="radio" onChange={() => this.props.setAutosave(false)} @@ -199,38 +232,58 @@ class Preferences extends React.Component { value="Off" checked={!this.props.autosave} /> - <label htmlFor="autosave-off" className="preference__option">{this.props.t('Preferences.Off')}</label> + <label htmlFor="autosave-off" className="preference__option"> + {this.props.t('Preferences.Off')} + </label> </div> </div> <div className="preference"> - <h4 className="preference__title">{this.props.t('Preferences.AutocloseBracketsQuotes')}</h4> + <h4 className="preference__title"> + {this.props.t('Preferences.AutocloseBracketsQuotes')} + </h4> <div className="preference__options"> <input type="radio" onChange={() => this.props.setAutocloseBracketsQuotes(true)} - aria-label={this.props.t('Preferences.AutocloseBracketsQuotesOnARIA')} + aria-label={this.props.t( + 'Preferences.AutocloseBracketsQuotesOnARIA' + )} name="autoclosebracketsquotes" id="autoclosebracketsquotes-on" className="preference__radio-button" value="On" checked={this.props.autocloseBracketsQuotes} /> - <label htmlFor="autoclosebracketsquotes-on" className="preference__option">{this.props.t('Preferences.On')}</label> + <label + htmlFor="autoclosebracketsquotes-on" + className="preference__option" + > + {this.props.t('Preferences.On')} + </label> <input type="radio" onChange={() => this.props.setAutocloseBracketsQuotes(false)} - aria-label={this.props.t('Preferences.AutocloseBracketsQuotesOffARIA')} + aria-label={this.props.t( + 'Preferences.AutocloseBracketsQuotesOffARIA' + )} name="autoclosebracketsquotes" id="autoclosebracketsquotes-off" className="preference__radio-button" value="Off" checked={!this.props.autocloseBracketsQuotes} /> - <label htmlFor="autoclosebracketsquotes-off" className="preference__option">{this.props.t('Preferences.Off')}</label> + <label + htmlFor="autoclosebracketsquotes-off" + className="preference__option" + > + {this.props.t('Preferences.Off')} + </label> </div> </div> <div className="preference"> - <h4 className="preference__title">{this.props.t('Preferences.WordWrap')}</h4> + <h4 className="preference__title"> + {this.props.t('Preferences.WordWrap')} + </h4> <div className="preference__options"> <input type="radio" @@ -242,7 +295,9 @@ class Preferences extends React.Component { value="On" checked={this.props.linewrap} /> - <label htmlFor="linewrap-on" className="preference__option">{this.props.t('Preferences.On')}</label> + <label htmlFor="linewrap-on" className="preference__option"> + {this.props.t('Preferences.On')} + </label> <input type="radio" onChange={() => this.props.setLinewrap(false)} @@ -253,13 +308,17 @@ class Preferences extends React.Component { value="Off" checked={!this.props.linewrap} /> - <label htmlFor="linewrap-off" className="preference__option">{this.props.t('Preferences.Off')}</label> + <label htmlFor="linewrap-off" className="preference__option"> + {this.props.t('Preferences.Off')} + </label> </div> </div> </TabPanel> <TabPanel> <div className="preference"> - <h4 className="preference__title">{this.props.t('Preferences.LineNumbers')}</h4> + <h4 className="preference__title"> + {this.props.t('Preferences.LineNumbers')} + </h4> <div className="preference__options"> <input type="radio" @@ -271,7 +330,9 @@ class Preferences extends React.Component { value="On" checked={this.props.lineNumbers} /> - <label htmlFor="line-numbers-on" className="preference__option">{this.props.t('Preferences.On')}</label> + <label htmlFor="line-numbers-on" className="preference__option"> + {this.props.t('Preferences.On')} + </label> <input type="radio" onChange={() => this.props.setLineNumbers(false)} @@ -282,11 +343,18 @@ class Preferences extends React.Component { value="Off" checked={!this.props.lineNumbers} /> - <label htmlFor="line-numbers-off" className="preference__option">{this.props.t('Preferences.Off')}</label> + <label + htmlFor="line-numbers-off" + className="preference__option" + > + {this.props.t('Preferences.Off')} + </label> </div> </div> <div className="preference"> - <h4 className="preference__title">{this.props.t('Preferences.LintWarningSound')}</h4> + <h4 className="preference__title"> + {this.props.t('Preferences.LintWarningSound')} + </h4> <div className="preference__options"> <input type="radio" @@ -298,7 +366,9 @@ class Preferences extends React.Component { value="On" checked={this.props.lintWarning} /> - <label htmlFor="lint-warning-on" className="preference__option">{this.props.t('Preferences.On')}</label> + <label htmlFor="lint-warning-on" className="preference__option"> + {this.props.t('Preferences.On')} + </label> <input type="radio" onChange={() => this.props.setLintWarning(false)} @@ -309,7 +379,12 @@ class Preferences extends React.Component { value="Off" checked={!this.props.lintWarning} /> - <label htmlFor="lint-warning-off" className="preference__option">{this.props.t('Preferences.Off')}</label> + <label + htmlFor="lint-warning-off" + className="preference__option" + > + {this.props.t('Preferences.Off')} + </label> <button className="preference__preview-button" onClick={() => beep.play()} @@ -320,8 +395,12 @@ class Preferences extends React.Component { </div> </div> <div className="preference"> - <h4 className="preference__title">{this.props.t('Preferences.AccessibleTextBasedCanvas')}</h4> - <h6 className="preference__subtitle">{this.props.t('Preferences.UsedScreenReader')}</h6> + <h4 className="preference__title"> + {this.props.t('Preferences.AccessibleTextBasedCanvas')} + </h4> + <h6 className="preference__subtitle"> + {this.props.t('Preferences.UsedScreenReader')} + </h6> <div className="preference__options"> <input @@ -333,9 +412,14 @@ class Preferences extends React.Component { name="text output" id="text-output-on" value="On" - checked={(this.props.textOutput)} + checked={this.props.textOutput} /> - <label htmlFor="text-output-on" className="preference__option preference__canvas">{this.props.t('Preferences.PlainText')}</label> + <label + htmlFor="text-output-on" + className="preference__option preference__canvas" + > + {this.props.t('Preferences.PlainText')} + </label> <input type="checkbox" onChange={(event) => { @@ -345,9 +429,14 @@ class Preferences extends React.Component { name="table output" id="table-output-on" value="On" - checked={(this.props.gridOutput)} + checked={this.props.gridOutput} /> - <label htmlFor="table-output-on" className="preference__option preference__canvas">{this.props.t('Preferences.TableText')}</label> + <label + htmlFor="table-output-on" + className="preference__option preference__canvas" + > + {this.props.t('Preferences.TableText')} + </label> <input type="checkbox" onChange={(event) => { @@ -357,9 +446,14 @@ class Preferences extends React.Component { name="sound output" id="sound-output-on" value="On" - checked={(this.props.soundOutput)} + checked={this.props.soundOutput} /> - <label htmlFor="sound-output-on" className="preference__option preference__canvas">{this.props.t('Preferences.Sound')}</label> + <label + htmlFor="sound-output-on" + className="preference__option preference__canvas" + > + {this.props.t('Preferences.Sound')} + </label> </div> </div> </TabPanel> @@ -390,7 +484,7 @@ Preferences.propTypes = { setTheme: PropTypes.func.isRequired, autocloseBracketsQuotes: PropTypes.bool.isRequired, setAutocloseBracketsQuotes: PropTypes.func.isRequired, - t: PropTypes.func.isRequired, + t: PropTypes.func.isRequired }; export default withTranslation()(Preferences); diff --git a/client/modules/IDE/components/PreviewFrame.jsx b/client/modules/IDE/components/PreviewFrame.jsx index 7bd8bacc8a..534cf1f285 100644 --- a/client/modules/IDE/components/PreviewFrame.jsx +++ b/client/modules/IDE/components/PreviewFrame.jsx @@ -21,17 +21,23 @@ import { EXTERNAL_LINK_REGEX, NOT_EXTERNAL_LINK_REGEX } from '../../../../server/utils/fileUtils'; -import { hijackConsoleErrorsScript, startTag, getAllScriptOffsets } - from '../../../utils/consoleUtils'; +import { + hijackConsoleErrorsScript, + startTag, + getAllScriptOffsets +} from '../../../utils/consoleUtils'; import { getHTMLFile } from '../reducers/files'; import { stopSketch, expandConsole, endSketchRefresh } from '../actions/ide'; -import { setTextOutput, setGridOutput, setSoundOutput } from '../actions/preferences'; +import { + setTextOutput, + setGridOutput, + setSoundOutput +} from '../actions/preferences'; import { setBlobUrl } from '../actions/files'; import { clearConsole, dispatchConsoleEvent } from '../actions/console'; - const shouldRenderSketch = (props, prevProps = undefined) => { const { isPlaying, previewIsRefreshing, fullView } = props; @@ -40,12 +46,14 @@ const shouldRenderSketch = (props, prevProps = undefined) => { if (!prevProps) return false; - return (props.isPlaying !== prevProps.isPlaying // if sketch starts or stops playing, want to rerender - || props.isAccessibleOutputPlaying !== prevProps.isAccessibleOutputPlaying // if user switches textoutput preferences - || props.textOutput !== prevProps.textOutput - || props.gridOutput !== prevProps.gridOutput - || props.soundOutput !== prevProps.soundOutput - || (fullView && props.files[0].id !== prevProps.files[0].id)); + return ( + props.isPlaying !== prevProps.isPlaying || // if sketch starts or stops playing, want to rerender + props.isAccessibleOutputPlaying !== prevProps.isAccessibleOutputPlaying || // if user switches textoutput preferences + props.textOutput !== prevProps.textOutput || + props.gridOutput !== prevProps.gridOutput || + props.soundOutput !== prevProps.soundOutput || + (fullView && props.files[0].id !== prevProps.files[0].id) + ); }; class PreviewFrame extends React.Component { @@ -74,21 +82,27 @@ class PreviewFrame extends React.Component { componentWillUnmount() { window.removeEventListener('message', this.handleConsoleEvent); const iframeBody = this.iframeElement.contentDocument.body; - if (iframeBody) { ReactDOM.unmountComponentAtNode(iframeBody); } + if (iframeBody) { + ReactDOM.unmountComponentAtNode(iframeBody); + } } handleConsoleEvent(messageEvent) { if (Array.isArray(messageEvent.data)) { - const decodedMessages = messageEvent.data.map(message => + const decodedMessages = messageEvent.data.map((message) => Object.assign(Decode(message.log), { source: message.source - })); + }) + ); decodedMessages.every((message, index, arr) => { const { data: args } = message; let hasInfiniteLoop = false; Object.keys(args).forEach((key) => { - if (typeof args[key] === 'string' && args[key].includes('Exiting potential infinite loop')) { + if ( + typeof args[key] === 'string' && + args[key].includes('Exiting potential infinite loop') + ) { this.props.stopSketch(); this.props.expandConsole(); hasInfiniteLoop = true; @@ -103,7 +117,10 @@ class PreviewFrame extends React.Component { } const cur = Object.assign(message, { times: 1 }); const nextIndex = index + 1; - while (isEqual(cur.data, arr[nextIndex].data) && cur.method === arr[nextIndex].method) { + while ( + isEqual(cur.data, arr[nextIndex].data) && + cur.method === arr[nextIndex].method + ) { cur.times += 1; arr.splice(nextIndex, 1); if (nextIndex === arr.length) { @@ -145,7 +162,8 @@ class PreviewFrame extends React.Component { const files = this.props.files.slice(); if (this.props.cmController.getContent) { const activeFileInEditor = this.props.cmController.getContent(); - files.find(file => file.id === activeFileInEditor.id).content = activeFileInEditor.content; + files.find((file) => file.id === activeFileInEditor.id).content = + activeFileInEditor.content; } return files; } @@ -206,9 +224,14 @@ class PreviewFrame extends React.Component { const sketchDocString = `<!DOCTYPE HTML>\n${sketchDoc.documentElement.outerHTML}`; scriptOffs = getAllScriptOffsets(sketchDocString); const consoleErrorsScript = sketchDoc.createElement('script'); - consoleErrorsScript.innerHTML = hijackConsoleErrorsScript(JSON.stringify(scriptOffs)); + consoleErrorsScript.innerHTML = hijackConsoleErrorsScript( + JSON.stringify(scriptOffs) + ); this.addLoopProtect(sketchDoc); - sketchDoc.head.insertBefore(consoleErrorsScript, sketchDoc.head.firstElement); + sketchDoc.head.insertBefore( + consoleErrorsScript, + sketchDoc.head.firstElement + ); return `<!DOCTYPE HTML>\n${sketchDoc.documentElement.outerHTML}`; } @@ -218,7 +241,10 @@ class PreviewFrame extends React.Component { const elementsArray = Array.prototype.slice.call(elements); elementsArray.forEach((element) => { if (element.getAttribute(attr).match(MEDIA_FILE_REGEX)) { - const resolvedFile = resolvePathToFile(element.getAttribute(attr), files); + const resolvedFile = resolvePathToFile( + element.getAttribute(attr), + files + ); if (resolvedFile && resolvedFile.url) { element.setAttribute(attr, resolvedFile.url); } @@ -252,13 +278,19 @@ class PreviewFrame extends React.Component { if (resolvedFile) { if (resolvedFile.url) { - newContent = newContent.replace(jsFileString, quoteCharacter + resolvedFile.url + quoteCharacter); + newContent = newContent.replace( + jsFileString, + quoteCharacter + resolvedFile.url + quoteCharacter + ); } else if (resolvedFile.name.match(PLAINTEXT_FILE_REGEX)) { // could also pull file from API instead of using bloburl const blobURL = getBlobUrl(resolvedFile); this.props.setBlobUrl(resolvedFile, blobURL); - newContent = newContent.replace(jsFileString, quoteCharacter + blobURL + quoteCharacter); + newContent = newContent.replace( + jsFileString, + quoteCharacter + blobURL + quoteCharacter + ); } } } @@ -278,7 +310,10 @@ class PreviewFrame extends React.Component { const resolvedFile = resolvePathToFile(filePath, files); if (resolvedFile) { if (resolvedFile.url) { - newContent = newContent.replace(cssFileString, quoteCharacter + resolvedFile.url + quoteCharacter); + newContent = newContent.replace( + cssFileString, + quoteCharacter + resolvedFile.url + quoteCharacter + ); } } } @@ -290,8 +325,14 @@ class PreviewFrame extends React.Component { const scriptsInHTML = sketchDoc.getElementsByTagName('script'); const scriptsInHTMLArray = Array.prototype.slice.call(scriptsInHTML); scriptsInHTMLArray.forEach((script) => { - if (script.getAttribute('src') && script.getAttribute('src').match(NOT_EXTERNAL_LINK_REGEX) !== null) { - const resolvedFile = resolvePathToFile(script.getAttribute('src'), files); + if ( + script.getAttribute('src') && + script.getAttribute('src').match(NOT_EXTERNAL_LINK_REGEX) !== null + ) { + const resolvedFile = resolvePathToFile( + script.getAttribute('src'), + files + ); if (resolvedFile) { if (resolvedFile.url) { script.setAttribute('src', resolvedFile.url); @@ -301,7 +342,12 @@ class PreviewFrame extends React.Component { script.innerHTML = resolvedFile.content; // eslint-disable-line } } - } else if (!(script.getAttribute('src') && script.getAttribute('src').match(EXTERNAL_LINK_REGEX)) !== null) { + } else if ( + !( + script.getAttribute('src') && + script.getAttribute('src').match(EXTERNAL_LINK_REGEX) + ) !== null + ) { script.setAttribute('crossorigin', ''); script.innerHTML = this.resolveJSLinksInString(script.innerHTML, files); // eslint-disable-line } @@ -318,7 +364,10 @@ class PreviewFrame extends React.Component { const cssLinksInHTML = sketchDoc.querySelectorAll('link[rel="stylesheet"]'); const cssLinksInHTMLArray = Array.prototype.slice.call(cssLinksInHTML); cssLinksInHTMLArray.forEach((css) => { - if (css.getAttribute('href') && css.getAttribute('href').match(NOT_EXTERNAL_LINK_REGEX) !== null) { + if ( + css.getAttribute('href') && + css.getAttribute('href').match(NOT_EXTERNAL_LINK_REGEX) !== null + ) { const resolvedFile = resolvePathToFile(css.getAttribute('href'), files); if (resolvedFile) { if (resolvedFile.url) { @@ -364,7 +413,9 @@ class PreviewFrame extends React.Component { role="main" frameBorder="0" title="sketch preview" - ref={(element) => { this.iframeElement = element; }} + ref={(element) => { + this.iframeElement = element; + }} sandbox={sandboxAttributes} /> ); @@ -380,12 +431,14 @@ PreviewFrame.propTypes = { htmlFile: PropTypes.shape({ content: PropTypes.string.isRequired }).isRequired, - files: PropTypes.arrayOf(PropTypes.shape({ - content: PropTypes.string.isRequired, - name: PropTypes.string.isRequired, - url: PropTypes.string, - id: PropTypes.string.isRequired - })).isRequired, + files: PropTypes.arrayOf( + PropTypes.shape({ + content: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + url: PropTypes.string, + id: PropTypes.string.isRequired + }) + ).isRequired, dispatchConsoleEvent: PropTypes.func.isRequired, endSketchRefresh: PropTypes.func.isRequired, previewIsRefreshing: PropTypes.bool.isRequired, @@ -396,7 +449,7 @@ PreviewFrame.propTypes = { clearConsole: PropTypes.func.isRequired, cmController: PropTypes.shape({ getContent: PropTypes.func - }), + }) }; PreviewFrame.defaultProps = { @@ -422,10 +475,11 @@ function mapStateToProps(state, ownProps) { return { files: state.files, htmlFile: getHTMLFile(state.files), - content: - (state.files.find(file => file.isSelectedFile) || - state.files.find(file => file.name === 'sketch.js') || - state.files.find(file => file.name !== 'root')).content, + content: ( + state.files.find((file) => file.isSelectedFile) || + state.files.find((file) => file.name === 'sketch.js') || + state.files.find((file) => file.name !== 'root') + ).content, isPlaying: state.ide.isPlaying, isAccessibleOutputPlaying: state.ide.isAccessibleOutputPlaying, previewIsRefreshing: state.ide.previewIsRefreshing, @@ -437,7 +491,6 @@ function mapStateToProps(state, ownProps) { }; } - const mapDispatchToProps = { stopSketch, expandConsole, @@ -450,5 +503,4 @@ const mapDispatchToProps = { dispatchConsoleEvent }; - -export default (connect(mapStateToProps, mapDispatchToProps)(PreviewFrame)); +export default connect(mapStateToProps, mapDispatchToProps)(PreviewFrame); diff --git a/client/modules/IDE/components/QuickAddList/Icons.jsx b/client/modules/IDE/components/QuickAddList/Icons.jsx index ae59c7cf7e..34363bf63f 100644 --- a/client/modules/IDE/components/QuickAddList/Icons.jsx +++ b/client/modules/IDE/components/QuickAddList/Icons.jsx @@ -7,20 +7,37 @@ import CloseIcon from '../../../../images/close.svg'; const Icons = ({ isAdded }) => { const classes = [ 'quick-add__icon', - isAdded ? 'quick-add__icon--in-collection' : 'quick-add__icon--not-in-collection' + isAdded + ? 'quick-add__icon--in-collection' + : 'quick-add__icon--not-in-collection' ].join(' '); return ( <div className={classes}> - <CloseIcon className="quick-add__remove-icon" role="img" aria-label="Descending" focusable="false" /> - <CheckIcon className="quick-add__in-icon" role="img" aria-label="Descending" focusable="false" /> - <CloseIcon className="quick-add__add-icon" role="img" aria-label="Descending" focusable="false" /> + <CloseIcon + className="quick-add__remove-icon" + role="img" + aria-label="Descending" + focusable="false" + /> + <CheckIcon + className="quick-add__in-icon" + role="img" + aria-label="Descending" + focusable="false" + /> + <CloseIcon + className="quick-add__add-icon" + role="img" + aria-label="Descending" + focusable="false" + /> </div> ); }; Icons.propTypes = { - isAdded: PropTypes.bool.isRequired, + isAdded: PropTypes.bool.isRequired }; export default Icons; diff --git a/client/modules/IDE/components/QuickAddList/QuickAddList.jsx b/client/modules/IDE/components/QuickAddList/QuickAddList.jsx index 300d930eb3..2fcfc9223e 100644 --- a/client/modules/IDE/components/QuickAddList/QuickAddList.jsx +++ b/client/modules/IDE/components/QuickAddList/QuickAddList.jsx @@ -5,13 +5,17 @@ import { withTranslation } from 'react-i18next'; import Icons from './Icons'; -const Item = ({ - isAdded, onSelect, name, url, t -}) => { - const buttonLabel = isAdded ? t('QuickAddList.ButtonRemoveARIA') : t('QuickAddList.ButtonAddToCollectionARIA'); +const Item = ({ isAdded, onSelect, name, url, t }) => { + const buttonLabel = isAdded + ? t('QuickAddList.ButtonRemoveARIA') + : t('QuickAddList.ButtonAddToCollectionARIA'); return ( <li className="quick-add__item" onClick={onSelect}> { /* eslint-disable-line */ } - <button className="quick-add__item-toggle" onClick={onSelect} aria-label={buttonLabel}> + <button + className="quick-add__item-toggle" + onClick={onSelect} + aria-label={buttonLabel} + > <Icons isAdded={isAdded} /> </button> <span className="quick-add__item-name">{name}</span> @@ -19,7 +23,7 @@ const Item = ({ className="quick-add__item-view" to={url} target="_blank" - onClick={e => e.stopPropagation()} + onClick={(e) => e.stopPropagation()} > {t('QuickAddList.View')} </Link> @@ -30,7 +34,7 @@ const Item = ({ const ItemType = PropTypes.shape({ name: PropTypes.string.isRequired, url: PropTypes.string.isRequired, - isAdded: PropTypes.bool.isRequired, + isAdded: PropTypes.bool.isRequired }); Item.propTypes = { @@ -41,9 +45,7 @@ Item.propTypes = { t: PropTypes.func.isRequired }; -const QuickAddList = ({ - items, onAdd, onRemove, t -}) => { +const QuickAddList = ({ items, onAdd, onRemove, t }) => { const handleAction = (item) => { if (item.isAdded) { onRemove(item); @@ -53,18 +55,19 @@ const QuickAddList = ({ }; return ( - <ul className="quick-add">{items.map(item => (<Item - key={item.id} - t={t} - {...item} - onSelect={ - (event) => { - event.stopPropagation(); - event.currentTarget.blur(); - handleAction(item); - } - } - />))} + <ul className="quick-add"> + {items.map((item) => ( + <Item + key={item.id} + t={t} + {...item} + onSelect={(event) => { + event.stopPropagation(); + event.currentTarget.blur(); + handleAction(item); + }} + /> + ))} </ul> ); }; diff --git a/client/modules/IDE/components/Searchbar/Collection.jsx b/client/modules/IDE/components/Searchbar/Collection.jsx index a0a2b57968..e36c4bf174 100644 --- a/client/modules/IDE/components/Searchbar/Collection.jsx +++ b/client/modules/IDE/components/Searchbar/Collection.jsx @@ -5,20 +5,19 @@ import * as SortingActions from '../../actions/sorting'; import Searchbar from './Searchbar'; - const scope = 'collection'; function mapStateToProps(state) { return { searchLabel: i18next.t('Searchbar.SearchCollection'), - searchTerm: state.search[`${scope}SearchTerm`], + searchTerm: state.search[`${scope}SearchTerm`] }; } function mapDispatchToProps(dispatch) { const actions = { - setSearchTerm: term => SortingActions.setSearchTerm(scope, term), - resetSearchTerm: () => SortingActions.resetSearchTerm(scope), + setSearchTerm: (term) => SortingActions.setSearchTerm(scope, term), + resetSearchTerm: () => SortingActions.resetSearchTerm(scope) }; return bindActionCreators(Object.assign({}, actions), dispatch); } diff --git a/client/modules/IDE/components/Searchbar/Searchbar.jsx b/client/modules/IDE/components/Searchbar/Searchbar.jsx index 05a99e9815..a7c79d1f46 100644 --- a/client/modules/IDE/components/Searchbar/Searchbar.jsx +++ b/client/modules/IDE/components/Searchbar/Searchbar.jsx @@ -5,7 +5,6 @@ import { withTranslation } from 'react-i18next'; import i18next from 'i18next'; import SearchIcon from '../../../../images/magnifyingglass.svg'; - class Searchbar extends React.Component { constructor(props) { super(props); @@ -23,7 +22,7 @@ class Searchbar extends React.Component { this.setState({ searchValue: '' }, () => { this.props.resetSearchTerm(); }); - } + }; searchChange = () => { this.props.setSearchTerm(this.state.searchValue.trim()); @@ -33,14 +32,22 @@ class Searchbar extends React.Component { this.setState({ searchValue: e.target.value }, () => { this.throttledSearchChange(this.state.searchValue.trim()); }); - } + }; render() { const { searchValue } = this.state; return ( - <div className={`searchbar ${searchValue === '' ? 'searchbar--is-empty' : ''}`}> + <div + className={`searchbar ${ + searchValue === '' ? 'searchbar--is-empty' : '' + }`} + > <div className="searchbar__button"> - <SearchIcon className="searchbar__icon" focusable="false" aria-hidden="true" /> + <SearchIcon + className="searchbar__icon" + focusable="false" + aria-hidden="true" + /> </div> <input className="searchbar__input" @@ -52,7 +59,8 @@ class Searchbar extends React.Component { <button className="searchbar__clear-button" onClick={this.handleResetSearch} - >{this.props.t('Searchbar.ClearTerm')} + > + {this.props.t('Searchbar.ClearTerm')} </button> </div> ); diff --git a/client/modules/IDE/components/Searchbar/Sketch.jsx b/client/modules/IDE/components/Searchbar/Sketch.jsx index cc995103e8..5d4f840bb0 100644 --- a/client/modules/IDE/components/Searchbar/Sketch.jsx +++ b/client/modules/IDE/components/Searchbar/Sketch.jsx @@ -10,14 +10,14 @@ const scope = 'sketch'; function mapStateToProps(state) { return { searchLabel: i18next.t('Searchbar.SearchSketch'), - searchTerm: state.search[`${scope}SearchTerm`], + searchTerm: state.search[`${scope}SearchTerm`] }; } function mapDispatchToProps(dispatch) { const actions = { - setSearchTerm: term => SortingActions.setSearchTerm(scope, term), - resetSearchTerm: () => SortingActions.resetSearchTerm(scope), + setSearchTerm: (term) => SortingActions.setSearchTerm(scope, term), + resetSearchTerm: () => SortingActions.resetSearchTerm(scope) }; return bindActionCreators(Object.assign({}, actions), dispatch); } diff --git a/client/modules/IDE/components/ShareModal.jsx b/client/modules/IDE/components/ShareModal.jsx index bea4c42a8c..ea947e88ad 100644 --- a/client/modules/IDE/components/ShareModal.jsx +++ b/client/modules/IDE/components/ShareModal.jsx @@ -5,17 +5,11 @@ import CopyableInput from './CopyableInput'; class ShareModal extends React.PureComponent { render() { - const { - projectId, - ownerUsername, - projectName - } = this.props; + const { projectId, ownerUsername, projectName } = this.props; const hostname = window.location.origin; return ( <div className="share-modal"> - <h3 className="share-modal__project-name"> - {projectName} - </h3> + <h3 className="share-modal__project-name">{projectName}</h3> <CopyableInput label={this.props.t('ShareModal.Embed')} value={`<iframe src="${hostname}/${ownerUsername}/embed/${projectId}"></iframe>`} diff --git a/client/modules/IDE/components/Sidebar.jsx b/client/modules/IDE/components/Sidebar.jsx index 5d4802788f..885907578c 100644 --- a/client/modules/IDE/components/Sidebar.jsx +++ b/client/modules/IDE/components/Sidebar.jsx @@ -16,7 +16,7 @@ class Sidebar extends React.Component { this.onFocusComponent = this.onFocusComponent.bind(this); this.state = { - isFocused: false, + isFocused: false }; } @@ -51,7 +51,10 @@ class Sidebar extends React.Component { let canEdit; if (!this.props.owner) { canEdit = true; - } else if (this.props.user.authenticated && this.props.owner.id === this.props.user.id) { + } else if ( + this.props.user.authenticated && + this.props.owner.id === this.props.user.id + ) { canEdit = true; } else { canEdit = false; @@ -62,16 +65,19 @@ class Sidebar extends React.Component { render() { const canEditProject = this.userCanEditProject(); const sidebarClass = classNames({ - 'sidebar': true, + "sidebar": true, 'sidebar--contracted': !this.props.isExpanded, 'sidebar--project-options': this.props.projectOptionsVisible, 'sidebar--cant-edit': !canEditProject }); - const rootFile = this.props.files.filter(file => file.name === 'root')[0]; + const rootFile = this.props.files.filter((file) => file.name === 'root')[0]; return ( <section className={sidebarClass}> - <header className="sidebar__header" onContextMenu={this.toggleProjectOptions}> + <header + className="sidebar__header" + onContextMenu={this.toggleProjectOptions} + > <h3 className="sidebar__title"> <span>{this.props.t('Sidebar.Title')}</span> </h3> @@ -80,7 +86,9 @@ class Sidebar extends React.Component { aria-label={this.props.t('Sidebar.ToggleARIA')} className="sidebar__add" tabIndex="0" - ref={(element) => { this.sidebarOptions = element; }} + ref={(element) => { + this.sidebarOptions = element; + }} onClick={this.toggleProjectOptions} onBlur={this.onBlurComponent} onFocus={this.onFocusComponent} @@ -114,8 +122,7 @@ class Sidebar extends React.Component { {this.props.t('Sidebar.AddFile')} </button> </li> - { - this.props.user.authenticated && + {this.props.user.authenticated && ( <li> <button aria-label={this.props.t('Sidebar.UploadFileARIA')} @@ -129,24 +136,23 @@ class Sidebar extends React.Component { {this.props.t('Sidebar.UploadFile')} </button> </li> - } + )} </ul> </div> </header> - <ConnectedFileNode - id={rootFile.id} - canEdit={canEditProject} - /> + <ConnectedFileNode id={rootFile.id} canEdit={canEditProject} /> </section> ); } } Sidebar.propTypes = { - files: PropTypes.arrayOf(PropTypes.shape({ - name: PropTypes.string.isRequired, - id: PropTypes.string.isRequired - })).isRequired, + files: PropTypes.arrayOf( + PropTypes.shape({ + name: PropTypes.string.isRequired, + id: PropTypes.string.isRequired + }) + ).isRequired, setSelectedFile: PropTypes.func.isRequired, isExpanded: PropTypes.bool.isRequired, projectOptionsVisible: PropTypes.bool.isRequired, @@ -162,7 +168,7 @@ Sidebar.propTypes = { id: PropTypes.string, authenticated: PropTypes.bool.isRequired }).isRequired, - t: PropTypes.func.isRequired, + t: PropTypes.func.isRequired }; Sidebar.defaultProps = { diff --git a/client/modules/IDE/components/SketchList.jsx b/client/modules/IDE/components/SketchList.jsx index 6abdd79020..98abd67340 100644 --- a/client/modules/IDE/components/SketchList.jsx +++ b/client/modules/IDE/components/SketchList.jsx @@ -23,8 +23,8 @@ import ArrowUpIcon from '../../../images/sort-arrow-up.svg'; import ArrowDownIcon from '../../../images/sort-arrow-down.svg'; import DownFilledTriangleIcon from '../../../images/down-filled-triangle.svg'; - -const formatDateCell = (date, mobile = false) => dates.format(date, { showTime: !mobile }); +const formatDateCell = (date, mobile = false) => + dates.format(date, { showTime: !mobile }); class SketchListRowBase extends React.Component { constructor(props) { @@ -33,14 +33,14 @@ class SketchListRowBase extends React.Component { optionsOpen: false, renameOpen: false, renameValue: props.sketch.name, - isFocused: false, + isFocused: false }; this.renameInput = React.createRef(); } onFocusComponent = () => { this.setState({ isFocused: true }); - } + }; onBlurComponent = () => { this.setState({ isFocused: false }); @@ -49,19 +49,19 @@ class SketchListRowBase extends React.Component { this.closeAll(); } }, 200); - } + }; openOptions = () => { this.setState({ optionsOpen: true }); - } + }; closeOptions = () => { this.setState({ optionsOpen: false }); - } + }; toggleOptions = () => { if (this.state.optionsOpen) { @@ -69,96 +69,112 @@ class SketchListRowBase extends React.Component { } else { this.openOptions(); } - } + }; openRename = () => { - this.setState({ - renameOpen: true, - renameValue: this.props.sketch.name - }, () => this.renameInput.current.focus()); - } + this.setState( + { + renameOpen: true, + renameValue: this.props.sketch.name + }, + () => this.renameInput.current.focus() + ); + }; closeRename = () => { this.setState({ renameOpen: false }); - } + }; closeAll = () => { this.setState({ renameOpen: false, optionsOpen: false }); - } + }; handleRenameChange = (e) => { this.setState({ renameValue: e.target.value }); - } + }; handleRenameEnter = (e) => { if (e.key === 'Enter') { this.updateName(); this.closeAll(); } - } + }; handleRenameBlur = () => { this.updateName(); this.closeAll(); - } + }; updateName = () => { const isValid = this.state.renameValue.trim().length !== 0; if (isValid) { - this.props.changeProjectName(this.props.sketch.id, this.state.renameValue.trim()); + this.props.changeProjectName( + this.props.sketch.id, + this.state.renameValue.trim() + ); } - } + }; resetSketchName = () => { this.setState({ renameValue: this.props.sketch.name, renameOpen: false }); - } + }; handleDropdownOpen = () => { this.closeAll(); this.openOptions(); - } + }; handleRenameOpen = () => { this.closeAll(); this.openRename(); - } + }; handleSketchDownload = () => { this.props.exportProjectAsZip(this.props.sketch.id); - } + }; handleSketchDuplicate = () => { this.closeAll(); this.props.cloneProject(this.props.sketch); - } + }; handleSketchShare = () => { this.closeAll(); - this.props.showShareModal(this.props.sketch.id, this.props.sketch.name, this.props.username); - } + this.props.showShareModal( + this.props.sketch.id, + this.props.sketch.name, + this.props.username + ); + }; handleSketchDelete = () => { this.closeAll(); - if (window.confirm(this.props.t('Common.DeleteConfirmation', { name: this.props.sketch.name }))) { + if ( + window.confirm( + this.props.t('Common.DeleteConfirmation', { + name: this.props.sketch.name + }) + ) + ) { this.props.deleteProject(this.props.sketch.id); } - } + }; - renderViewButton = sketchURL => ( + renderViewButton = (sketchURL) => ( <td className="sketch-list__dropdown-column"> <Link to={sketchURL}>{this.props.t('SketchList.View')}</Link> </td> - ) + ); renderDropdown = () => { const { optionsOpen } = this.state; @@ -175,21 +191,20 @@ class SketchListRowBase extends React.Component { > <DownFilledTriangleIcon focusable="false" aria-hidden="true" /> </button> - {optionsOpen && - <ul - className="sketch-list__action-dialogue" - > - {userIsOwner && - <li> - <button - className="sketch-list__action-option" - onClick={this.handleRenameOpen} - onBlur={this.onBlurComponent} - onFocus={this.onFocusComponent} - > - {this.props.t('SketchList.DropdownRename')} - </button> - </li>} + {optionsOpen && ( + <ul className="sketch-list__action-dialogue"> + {userIsOwner && ( + <li> + <button + className="sketch-list__action-option" + onClick={this.handleRenameOpen} + onBlur={this.onBlurComponent} + onFocus={this.onFocusComponent} + > + {this.props.t('SketchList.DropdownRename')} + </button> + </li> + )} <li> <button className="sketch-list__action-option" @@ -200,18 +215,19 @@ class SketchListRowBase extends React.Component { {this.props.t('SketchList.DropdownDownload')} </button> </li> - {this.props.user.authenticated && - <li> - <button - className="sketch-list__action-option" - onClick={this.handleSketchDuplicate} - onBlur={this.onBlurComponent} - onFocus={this.onFocusComponent} - > - {this.props.t('SketchList.DropdownDuplicate')} - </button> - </li>} - {this.props.user.authenticated && + {this.props.user.authenticated && ( + <li> + <button + className="sketch-list__action-option" + onClick={this.handleSketchDuplicate} + onBlur={this.onBlurComponent} + onFocus={this.onFocusComponent} + > + {this.props.t('SketchList.DropdownDuplicate')} + </button> + </li> + )} + {this.props.user.authenticated && ( <li> <button className="sketch-list__action-option" @@ -224,8 +240,9 @@ class SketchListRowBase extends React.Component { > {this.props.t('SketchList.DropdownAddToCollection')} </button> - </li>} - { /* <li> + </li> + )} + {/* <li> <button className="sketch-list__action-option" onClick={this.handleSketchShare} @@ -234,29 +251,27 @@ class SketchListRowBase extends React.Component { > Share </button> - </li> */ } - {userIsOwner && - <li> - <button - className="sketch-list__action-option" - onClick={this.handleSketchDelete} - onBlur={this.onBlurComponent} - onFocus={this.onFocusComponent} - > - {this.props.t('SketchList.DropdownDelete')} - </button> - </li>} - </ul>} + </li> */} + {userIsOwner && ( + <li> + <button + className="sketch-list__action-option" + onClick={this.handleSketchDelete} + onBlur={this.onBlurComponent} + onFocus={this.onFocusComponent} + > + {this.props.t('SketchList.DropdownDelete')} + </button> + </li> + )} + </ul> + )} </td> ); - } + }; render() { - const { - sketch, - username, - mobile - } = this.props; + const { sketch, username, mobile } = this.props; const { renameOpen, renameValue } = this.state; let url = `/${username}/sketches/${sketch.id}`; if (username === 'p5') { @@ -265,20 +280,17 @@ class SketchListRowBase extends React.Component { const name = ( <React.Fragment> - <Link to={url}> - {renameOpen ? '' : sketch.name} - </Link> - {renameOpen - && - <input - value={renameValue} - onChange={this.handleRenameChange} - onKeyUp={this.handleRenameEnter} - onBlur={this.handleRenameBlur} - onClick={e => e.stopPropagation()} - ref={this.renameInput} - /> - } + <Link to={url}>{renameOpen ? '' : sketch.name}</Link> + {renameOpen && ( + <input + value={renameValue} + onChange={this.handleRenameChange} + onKeyUp={this.handleRenameEnter} + onBlur={this.handleRenameBlur} + onClick={(e) => e.stopPropagation()} + ref={this.renameInput} + /> + )} </React.Fragment> ); @@ -289,14 +301,19 @@ class SketchListRowBase extends React.Component { key={sketch.id} onClick={this.handleRowClick} > - <th scope="row"> - {name} - </th> - <td>{mobile && 'Created: '}{formatDateCell(sketch.createdAt, mobile)}</td> - <td>{mobile && 'Updated: '}{formatDateCell(sketch.updatedAt, mobile)}</td> + <th scope="row">{name}</th> + <td> + {mobile && 'Created: '} + {formatDateCell(sketch.createdAt, mobile)} + </td> + <td> + {mobile && 'Updated: '} + {formatDateCell(sketch.updatedAt, mobile)} + </td> {this.renderDropdown()} </tr> - </React.Fragment>); + </React.Fragment> + ); } } @@ -327,10 +344,16 @@ SketchListRowBase.defaultProps = { }; function mapDispatchToPropsSketchListRow(dispatch) { - return bindActionCreators(Object.assign({}, ProjectActions, IdeActions), dispatch); + return bindActionCreators( + Object.assign({}, ProjectActions, IdeActions), + dispatch + ); } -const SketchListRow = connect(null, mapDispatchToPropsSketchListRow)(SketchListRowBase); +const SketchListRow = connect( + null, + mapDispatchToPropsSketchListRow +)(SketchListRowBase); class SketchList extends React.Component { constructor(props) { @@ -339,15 +362,18 @@ class SketchList extends React.Component { this.props.resetSorting(); this.state = { - isInitialDataLoad: true, + isInitialDataLoad: true }; } componentDidUpdate(prevProps) { - if (this.props.sketches !== prevProps.sketches && Array.isArray(this.props.sketches)) { + if ( + this.props.sketches !== prevProps.sketches && + Array.isArray(this.props.sketches) + ) { // eslint-disable-next-line react/no-did-update-set-state this.setState({ - isInitialDataLoad: false, + isInitialDataLoad: false }); } } @@ -356,7 +382,9 @@ class SketchList extends React.Component { if (this.props.username === this.props.user.username) { return this.props.t('SketchList.Title'); } - return this.props.t('SketchList.AnothersTitle', { anotheruser: this.props.username }); + return this.props.t('SketchList.AnothersTitle', { + anotheruser: this.props.username + }); } hasSketches() { @@ -374,7 +402,11 @@ class SketchList extends React.Component { _renderEmptyTable() { if (!this.isLoading() && this.props.sketches.length === 0) { - return (<p className="sketches-table__empty">{this.props.t('SketchList.NoSketches')}</p>); + return ( + <p className="sketches-table__empty"> + {this.props.t('SketchList.NoSketches')} + </p> + ); } return null; } @@ -384,17 +416,25 @@ class SketchList extends React.Component { let buttonLabel; if (field !== fieldName) { if (field === 'name') { - buttonLabel = this.props.t('SketchList.ButtonLabelAscendingARIA', { displayName }); + buttonLabel = this.props.t('SketchList.ButtonLabelAscendingARIA', { + displayName + }); } else { - buttonLabel = this.props.t('SketchList.ButtonLabelDescendingARIA', { displayName }); + buttonLabel = this.props.t('SketchList.ButtonLabelDescendingARIA', { + displayName + }); } } else if (direction === SortingActions.DIRECTION.ASC) { - buttonLabel = this.props.t('SketchList.ButtonLabelDescendingARIA', { displayName }); + buttonLabel = this.props.t('SketchList.ButtonLabelDescendingARIA', { + displayName + }); } else { - buttonLabel = this.props.t('SketchList.ButtonLabelAscendingARIA', { displayName }); + buttonLabel = this.props.t('SketchList.ButtonLabelAscendingARIA', { + displayName + }); } return buttonLabel; - } + }; _renderFieldHeader = (fieldName, displayName) => { const { field, direction } = this.props.sorting; @@ -411,19 +451,32 @@ class SketchList extends React.Component { aria-label={buttonLabel} > <span className={headerClass}>{displayName}</span> - {field === fieldName && direction === SortingActions.DIRECTION.ASC && - <ArrowUpIcon role="img" aria-label={this.props.t('SketchList.DirectionAscendingARIA')} focusable="false" /> - } - {field === fieldName && direction === SortingActions.DIRECTION.DESC && - <ArrowDownIcon role="img" aria-label={this.props.t('SketchList.DirectionDescendingARIA')} focusable="false" /> - } + {field === fieldName && + direction === SortingActions.DIRECTION.ASC && ( + <ArrowUpIcon + role="img" + aria-label={this.props.t('SketchList.DirectionAscendingARIA')} + focusable="false" + /> + )} + {field === fieldName && + direction === SortingActions.DIRECTION.DESC && ( + <ArrowDownIcon + role="img" + aria-label={this.props.t('SketchList.DirectionDescendingARIA')} + focusable="false" + /> + )} </button> </th> ); - } + }; render() { - const username = this.props.username !== undefined ? this.props.username : this.props.user.username; + const username = + this.props.username !== undefined + ? this.props.username + : this.props.user.username; const { mobile } = this.props; return ( <article className="sketches-table-container"> @@ -432,19 +485,35 @@ class SketchList extends React.Component { </Helmet> {this._renderLoader()} {this._renderEmptyTable()} - {this.hasSketches() && - <table className="sketches-table" summary={this.props.t('SketchList.TableSummary')}> + {this.hasSketches() && ( + <table + className="sketches-table" + summary={this.props.t('SketchList.TableSummary')} + > <thead> <tr> - {this._renderFieldHeader('name', this.props.t('SketchList.HeaderName'))} - {this._renderFieldHeader('createdAt', this.props.t('SketchList.HeaderCreatedAt', { context: mobile ? 'mobile' : '' }))} - {this._renderFieldHeader('updatedAt', this.props.t('SketchList.HeaderUpdatedAt', { context: mobile ? 'mobile' : '' }))} + {this._renderFieldHeader( + 'name', + this.props.t('SketchList.HeaderName') + )} + {this._renderFieldHeader( + 'createdAt', + this.props.t('SketchList.HeaderCreatedAt', { + context: mobile ? 'mobile' : '' + }) + )} + {this._renderFieldHeader( + 'updatedAt', + this.props.t('SketchList.HeaderUpdatedAt', { + context: mobile ? 'mobile' : '' + }) + )} <th scope="col"></th> </tr> </thead> <tbody> - {this.props.sketches.map(sketch => - (<SketchListRow + {this.props.sketches.map((sketch) => ( + <SketchListRow mobile={mobile} key={sketch.id} sketch={sketch} @@ -454,23 +523,26 @@ class SketchList extends React.Component { this.setState({ sketchToAddToCollection: sketch }); }} t={this.props.t} - />))} + /> + ))} </tbody> - </table>} - { - this.state.sketchToAddToCollection && - <Overlay - isFixedHeight - title={this.props.t('SketchList.AddToCollectionOverlayTitle')} - closeOverlay={() => this.setState({ sketchToAddToCollection: null })} - > - <AddToCollectionList - project={this.state.sketchToAddToCollection} - username={this.props.username} - user={this.props.user} - /> - </Overlay> - } + </table> + )} + {this.state.sketchToAddToCollection && ( + <Overlay + isFixedHeight + title={this.props.t('SketchList.AddToCollectionOverlayTitle')} + closeOverlay={() => + this.setState({ sketchToAddToCollection: null }) + } + > + <AddToCollectionList + project={this.state.sketchToAddToCollection} + username={this.props.username} + user={this.props.user} + /> + </Overlay> + )} </article> ); } @@ -482,12 +554,14 @@ SketchList.propTypes = { authenticated: PropTypes.bool.isRequired }).isRequired, getProjects: PropTypes.func.isRequired, - sketches: PropTypes.arrayOf(PropTypes.shape({ - id: PropTypes.string.isRequired, - name: PropTypes.string.isRequired, - createdAt: PropTypes.string.isRequired, - updatedAt: PropTypes.string.isRequired - })).isRequired, + sketches: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + createdAt: PropTypes.string.isRequired, + updatedAt: PropTypes.string.isRequired + }) + ).isRequired, username: PropTypes.string, loading: PropTypes.bool.isRequired, toggleDirectionForField: PropTypes.func.isRequired, @@ -502,7 +576,7 @@ SketchList.propTypes = { SketchList.defaultProps = { username: undefined, - mobile: false, + mobile: false }; function mapStateToProps(state) { @@ -517,9 +591,17 @@ function mapStateToProps(state) { function mapDispatchToProps(dispatch) { return bindActionCreators( - Object.assign({}, ProjectsActions, CollectionsActions, ToastActions, SortingActions), + Object.assign( + {}, + ProjectsActions, + CollectionsActions, + ToastActions, + SortingActions + ), dispatch ); } -export default withTranslation()(connect(mapStateToProps, mapDispatchToProps)(SketchList)); +export default withTranslation()( + connect(mapStateToProps, mapDispatchToProps)(SketchList) +); diff --git a/client/modules/IDE/components/Toast.jsx b/client/modules/IDE/components/Toast.jsx index f3cc0fc76c..9f29dc1872 100644 --- a/client/modules/IDE/components/Toast.jsx +++ b/client/modules/IDE/components/Toast.jsx @@ -11,10 +11,12 @@ function Toast(props) { const { t } = useTranslation(); return ( <section className="toast"> - <p> - {t(props.text)} - </p> - <button className="toast__close" onClick={props.hideToast} aria-label="Close Alert" > + <p>{t(props.text)}</p> + <button + className="toast__close" + onClick={props.hideToast} + aria-label="Close Alert" + > <ExitIcon focusable="false" aria-hidden="true" /> </button> </section> diff --git a/client/modules/IDE/components/Toolbar.jsx b/client/modules/IDE/components/Toolbar.jsx index 8e357ff852..f87d80bf34 100644 --- a/client/modules/IDE/components/Toolbar.jsx +++ b/client/modules/IDE/components/Toolbar.jsx @@ -21,7 +21,7 @@ class Toolbar extends React.Component { this.handleProjectNameSave = this.handleProjectNameSave.bind(this); this.state = { - projectNameInputValue: props.project.name, + projectNameInputValue: props.project.name }; } @@ -40,7 +40,7 @@ class Toolbar extends React.Component { const newProjectName = this.state.projectNameInputValue.trim(); if (newProjectName.length === 0) { this.setState({ - projectNameInputValue: this.props.project.name, + projectNameInputValue: this.props.project.name }); } else { this.props.setProjectName(newProjectName); @@ -52,9 +52,13 @@ class Toolbar extends React.Component { } canEditProjectName() { - return (this.props.owner && this.props.owner.username - && this.props.owner.username === this.props.currentUser) - || !this.props.owner || !this.props.owner.username; + return ( + (this.props.owner && + this.props.owner.username && + this.props.owner.username === this.props.currentUser) || + !this.props.owner || + !this.props.owner.username + ); } render() { @@ -72,7 +76,8 @@ class Toolbar extends React.Component { }); const nameContainerClass = classNames({ 'toolbar__project-name-container': true, - 'toolbar__project-name-container--editing': this.props.project.isEditingName + 'toolbar__project-name-container--editing': this.props.project + .isEditingName }); const canEditProjectName = this.canEditProjectName(); @@ -133,14 +138,13 @@ class Toolbar extends React.Component { aria-label={this.props.t('Toolbar.EditSketchARIA')} > <span>{this.props.project.name}</span> - { - canEditProjectName && + {canEditProjectName && ( <EditProjectNameIcon className="toolbar__edit-name-button" focusable="false" aria-hidden="true" /> - } + )} </button> <input type="text" @@ -149,7 +153,9 @@ class Toolbar extends React.Component { aria-label={this.props.t('Toolbar.NewSketchNameARIA')} value={this.state.projectNameInputValue} onChange={this.handleProjectNameChange} - ref={(element) => { this.projectNameInput = element; }} + ref={(element) => { + this.projectNameInput = element; + }} onBlur={this.handleProjectNameSave} onKeyPress={this.handleKeyPress} /> @@ -157,7 +163,10 @@ class Toolbar extends React.Component { if (this.props.owner) { return ( <p className="toolbar__project-owner"> - {this.props.t('Toolbar.By')} <Link to={`/${this.props.owner.username}/sketches`}>{this.props.owner.username}</Link> + {this.props.t('Toolbar.By')}{' '} + <Link to={`/${this.props.owner.username}/sketches`}> + {this.props.owner.username} + </Link> </p> ); } @@ -187,7 +196,7 @@ Toolbar.propTypes = { project: PropTypes.shape({ name: PropTypes.string.isRequired, isEditingName: PropTypes.bool, - id: PropTypes.string, + id: PropTypes.string }).isRequired, showEditProjectName: PropTypes.func.isRequired, hideEditProjectName: PropTypes.func.isRequired, @@ -201,7 +210,6 @@ Toolbar.propTypes = { saveProject: PropTypes.func.isRequired, currentUser: PropTypes.string, t: PropTypes.func.isRequired - }; Toolbar.defaultProps = { @@ -217,14 +225,14 @@ function mapStateToProps(state) { isPlaying: state.ide.isPlaying, owner: state.project.owner, preferencesIsVisible: state.ide.preferencesIsVisible, - project: state.project, + project: state.project }; } const mapDispatchToProps = { ...IDEActions, ...preferenceActions, - ...projectActions, + ...projectActions }; export const ToolbarComponent = withTranslation()(Toolbar); diff --git a/client/modules/IDE/components/Toolbar.test.jsx b/client/modules/IDE/components/Toolbar.test.jsx index 79b0f9ee1a..0d13d633af 100644 --- a/client/modules/IDE/components/Toolbar.test.jsx +++ b/client/modules/IDE/components/Toolbar.test.jsx @@ -5,35 +5,38 @@ import { fireEvent, render, screen, waitFor } from '../../../test-utils'; import { ToolbarComponent } from './Toolbar'; const renderComponent = (extraProps = {}) => { - const props = lodash.merge({ - isPlaying: false, - preferencesIsVisible: false, - stopSketch: jest.fn(), - setProjectName: jest.fn(), - openPreferences: jest.fn(), - showEditProjectName: jest.fn(), - hideEditProjectName: jest.fn(), - infiniteLoop: false, - autorefresh: false, - setAutorefresh: jest.fn(), - setTextOutput: jest.fn(), - setGridOutput: jest.fn(), - startSketch: jest.fn(), - startAccessibleSketch: jest.fn(), - saveProject: jest.fn(), - currentUser: 'me', - originalProjectName: 'testname', - - owner: { - username: 'me' + const props = lodash.merge( + { + isPlaying: false, + preferencesIsVisible: false, + stopSketch: jest.fn(), + setProjectName: jest.fn(), + openPreferences: jest.fn(), + showEditProjectName: jest.fn(), + hideEditProjectName: jest.fn(), + infiniteLoop: false, + autorefresh: false, + setAutorefresh: jest.fn(), + setTextOutput: jest.fn(), + setGridOutput: jest.fn(), + startSketch: jest.fn(), + startAccessibleSketch: jest.fn(), + saveProject: jest.fn(), + currentUser: 'me', + originalProjectName: 'testname', + + owner: { + username: 'me' + }, + project: { + name: 'testname', + isEditingName: false, + id: 'id' + }, + t: jest.fn() }, - project: { - name: 'testname', - isEditingName: false, - id: 'id', - }, - t: jest.fn() - }, extraProps); + extraProps + ); render(<ToolbarComponent {...props} />); @@ -57,21 +60,27 @@ describe('<ToolbarComponent />', () => { fireEvent.click(sketchName); expect(sketchName).toBeDisabled(); - await waitFor(() => expect(props.showEditProjectName).not.toHaveBeenCalled()); + await waitFor(() => + expect(props.showEditProjectName).not.toHaveBeenCalled() + ); }); it('sketch owner can change name', async () => { const props = renderComponent({ project: { isEditingName: true } }); const sketchNameInput = screen.getByLabelText('New sketch name'); - fireEvent.change(sketchNameInput, { target: { value: 'my new sketch name' } }); + fireEvent.change(sketchNameInput, { + target: { value: 'my new sketch name' } + }); fireEvent.blur(sketchNameInput); - await waitFor(() => expect(props.setProjectName).toHaveBeenCalledWith('my new sketch name')); + await waitFor(() => + expect(props.setProjectName).toHaveBeenCalledWith('my new sketch name') + ); await waitFor(() => expect(props.saveProject).toHaveBeenCalled()); }); - it('sketch owner can\'t change to empty name', async () => { + it("sketch owner can't change to empty name", async () => { const props = renderComponent({ project: { isEditingName: true } }); const sketchNameInput = screen.getByLabelText('New sketch name'); diff --git a/client/modules/IDE/components/UploadFileModal.jsx b/client/modules/IDE/components/UploadFileModal.jsx index 793c4183a3..dd32d850e5 100644 --- a/client/modules/IDE/components/UploadFileModal.jsx +++ b/client/modules/IDE/components/UploadFileModal.jsx @@ -17,7 +17,7 @@ class UploadFileModal extends React.Component { reachedTotalSizeLimit: PropTypes.bool.isRequired, closeModal: PropTypes.func.isRequired, t: PropTypes.func.isRequired - } + }; componentDidMount() { this.focusOnModal(); @@ -25,15 +25,21 @@ class UploadFileModal extends React.Component { focusOnModal = () => { this.modal.focus(); - } - + }; render() { return ( - <section className="modal" ref={(element) => { this.modal = element; }}> + <section + className="modal" + ref={(element) => { + this.modal = element; + }} + > <div className="modal-content"> <div className="modal__header"> - <h2 className="modal__title">{this.props.t('UploadFileModal.Title')}</h2> + <h2 className="modal__title"> + {this.props.t('UploadFileModal.Title')} + </h2> <button className="modal__exit-button" onClick={this.props.closeModal} @@ -42,18 +48,22 @@ class UploadFileModal extends React.Component { <ExitIcon focusable="false" aria-hidden="true" /> </button> </div> - { this.props.reachedTotalSizeLimit && + {this.props.reachedTotalSizeLimit && ( <p> - {this.props.t('UploadFileModal.SizeLimitError', { sizeLimit: limitText })} - <Link to="/assets" onClick={this.props.closeModal}>assets</Link> + {this.props.t('UploadFileModal.SizeLimitError', { + sizeLimit: limitText + })} + <Link to="/assets" onClick={this.props.closeModal}> + assets + </Link> . </p> - } - { !this.props.reachedTotalSizeLimit && + )} + {!this.props.reachedTotalSizeLimit && ( <div> <FileUploader /> </div> - } + )} </div> </section> ); diff --git a/client/modules/IDE/hooks/useHandleMessageEvent.js b/client/modules/IDE/hooks/useHandleMessageEvent.js index a2233513e8..dc3d157f27 100644 --- a/client/modules/IDE/hooks/useHandleMessageEvent.js +++ b/client/modules/IDE/hooks/useHandleMessageEvent.js @@ -10,12 +10,15 @@ export default function useHandleMessageEvent() { const handleMessageEvent = (data) => { const { source, messages } = data; if (source === 'sketch' && Array.isArray(messages)) { - const decodedMessages = messages.map(message => Decode(message.log)); + const decodedMessages = messages.map((message) => Decode(message.log)); decodedMessages.every((message, index, arr) => { const { data: args } = message; let hasInfiniteLoop = false; Object.keys(args).forEach((key) => { - if (typeof args[key] === 'string' && args[key].includes('Exiting potential infinite loop')) { + if ( + typeof args[key] === 'string' && + args[key].includes('Exiting potential infinite loop') + ) { dispatch(stopSketch()); dispatch(expandConsole()); hasInfiniteLoop = true; @@ -31,7 +34,10 @@ export default function useHandleMessageEvent() { // this should be done in the reducer probs const cur = Object.assign(message, { times: 1 }); const nextIndex = index + 1; - while (isEqual(cur.data, arr[nextIndex].data) && cur.method === arr[nextIndex].method) { + while ( + isEqual(cur.data, arr[nextIndex].data) && + cur.method === arr[nextIndex].method + ) { cur.times += 1; arr.splice(nextIndex, 1); if (nextIndex === arr.length) { diff --git a/client/modules/IDE/pages/FullView.jsx b/client/modules/IDE/pages/FullView.jsx index d2f4f57a65..d635124dc0 100644 --- a/client/modules/IDE/pages/FullView.jsx +++ b/client/modules/IDE/pages/FullView.jsx @@ -9,7 +9,10 @@ import * as ProjectActions from '../actions/project'; class FullView extends React.Component { componentDidMount() { - this.props.getProject(this.props.params.project_id, this.props.params.username); + this.props.getProject( + this.props.params.project_id, + this.props.params.username + ); } render() { @@ -19,13 +22,18 @@ class FullView extends React.Component { <title>{this.props.project.name}</title> </Helmet> <PreviewNav - owner={{ username: this.props.project.owner ? `${this.props.project.owner.username}` : '' }} - project={{ name: this.props.project.name, id: this.props.params.project_id }} + owner={{ + username: this.props.project.owner + ? `${this.props.project.owner.username}` + : '' + }} + project={{ + name: this.props.project.name, + id: this.props.params.project_id + }} /> <main className="preview-frame-holder"> - <PreviewFrame - fullView - /> + <PreviewFrame fullView /> </main> </div> ); diff --git a/client/modules/IDE/pages/IDEView.jsx b/client/modules/IDE/pages/IDEView.jsx index 94a416be88..fd1036e772 100644 --- a/client/modules/IDE/pages/IDEView.jsx +++ b/client/modules/IDE/pages/IDEView.jsx @@ -36,19 +36,21 @@ import Feedback from '../components/Feedback'; import { CollectionSearchbar } from '../components/Searchbar'; import { getIsUserOwner } from '../selectors/users'; - function getTitle(props) { const { id } = props.project; return id ? `p5.js Web Editor | ${props.project.name}` : 'p5.js Web Editor'; } function warnIfUnsavedChanges(props, nextLocation) { - const toAuth = nextLocation && + const toAuth = + nextLocation && nextLocation.action === 'PUSH' && (nextLocation.pathname === '/login' || nextLocation.pathname === '/signup'); - const onAuth = nextLocation && - (props.location.pathname === '/login' || props.location.pathname === '/signup'); - if (props.ide.unsavedChanges && (!toAuth && !onAuth)) { + const onAuth = + nextLocation && + (props.location.pathname === '/login' || + props.location.pathname === '/signup'); + if (props.ide.unsavedChanges && !toAuth && !onAuth) { if (!window.confirm(props.t('Nav.WarningUnsavedChanges'))) { return false; } @@ -64,7 +66,7 @@ class IDEView extends React.Component { this.state = { consoleSize: props.ide.consoleIsExpanded ? 150 : 29, - sidebarSize: props.ide.sidebarIsExpanded ? 160 : 20, + sidebarSize: props.ide.sidebarIsExpanded ? 160 : 20 }; } @@ -102,13 +104,13 @@ class IDEView extends React.Component { if (this.props.ide.consoleIsExpanded !== nextProps.ide.consoleIsExpanded) { this.setState({ - consoleSize: nextProps.ide.consoleIsExpanded ? 150 : 29, + consoleSize: nextProps.ide.consoleIsExpanded ? 150 : 29 }); } if (this.props.ide.sidebarIsExpanded !== nextProps.ide.sidebarIsExpanded) { this.setState({ - sidebarSize: nextProps.ide.sidebarIsExpanded ? 160 : 20, + sidebarSize: nextProps.ide.sidebarIsExpanded ? 160 : 20 }); } } @@ -148,7 +150,8 @@ class IDEView extends React.Component { if (this.props.route.path !== prevProps.route.path) { this.props.router.setRouteLeaveHook(this.props.route, () => - warnIfUnsavedChanges(this.props)); + warnIfUnsavedChanges(this.props) + ); } } componentWillUnmount() { @@ -234,7 +237,8 @@ class IDEView extends React.Component { } } - handleUnsavedChanges = nextLocation => warnIfUnsavedChanges(this.props, nextLocation); + handleUnsavedChanges = (nextLocation) => + warnIfUnsavedChanges(this.props, nextLocation); handleBeforeUnload = (e) => { const confirmationMessage = this.props.t('Nav.WarningUnsavedChanges'); @@ -243,7 +247,7 @@ class IDEView extends React.Component { return confirmationMessage; } return null; - } + }; render() { return ( @@ -282,7 +286,9 @@ class IDEView extends React.Component { setSoundOutput={this.props.setSoundOutput} theme={this.props.preferences.theme} setTheme={this.props.setTheme} - autocloseBracketsQuotes={this.props.preferences.autocloseBracketsQuotes} + autocloseBracketsQuotes={ + this.props.preferences.autocloseBracketsQuotes + } setAutocloseBracketsQuotes={this.props.setAutocloseBracketsQuotes} /> </Overlay> @@ -291,7 +297,7 @@ class IDEView extends React.Component { <SplitPane split="vertical" size={this.state.sidebarSize} - onChange={size => this.setState({ sidebarSize: size })} + onChange={(size) => this.setState({ sidebarSize: size })} onDragFinished={this._handleSidebarPaneOnDragFinished} allowResize={this.props.ide.sidebarIsExpanded} minSize={125} @@ -325,7 +331,7 @@ class IDEView extends React.Component { borderLeftWidth: '2px', borderRightWidth: '2px', width: '2px', - margin: '0px 0px', + margin: '0px 0px' }} > <SplitPane @@ -333,16 +339,22 @@ class IDEView extends React.Component { primary="second" size={this.state.consoleSize} minSize={29} - onChange={size => this.setState({ consoleSize: size })} + onChange={(size) => this.setState({ consoleSize: size })} allowResize={this.props.ide.consoleIsExpanded} className="editor-preview-subpanel" > - <Editor provideController={(ctl) => { this.cmController = ctl; }} /> + <Editor + provideController={(ctl) => { + this.cmController = ctl; + }} + /> <Console /> </SplitPane> <section className="preview-frame-holder"> <header className="preview-frame__header"> - <h2 className="preview-frame__title">{this.props.t('Toolbar.Preview')}</h2> + <h2 className="preview-frame__title"> + {this.props.t('Toolbar.Preview')} + </h2> </header> <div className="preview-frame__content"> <div @@ -451,16 +463,16 @@ IDEView.propTypes = { params: PropTypes.shape({ project_id: PropTypes.string, username: PropTypes.string, - reset_password_token: PropTypes.string, + reset_password_token: PropTypes.string }).isRequired, location: PropTypes.shape({ - pathname: PropTypes.string, + pathname: PropTypes.string }).isRequired, getProject: PropTypes.func.isRequired, user: PropTypes.shape({ authenticated: PropTypes.bool.isRequired, id: PropTypes.string, - username: PropTypes.string, + username: PropTypes.string }).isRequired, saveProject: PropTypes.func.isRequired, ide: PropTypes.shape({ @@ -482,7 +494,7 @@ IDEView.propTypes = { justOpenedProject: PropTypes.bool.isRequired, sidebarIsExpanded: PropTypes.bool.isRequired, consoleIsExpanded: PropTypes.bool.isRequired, - unsavedChanges: PropTypes.bool.isRequired, + unsavedChanges: PropTypes.bool.isRequired }).isRequired, stopSketch: PropTypes.func.isRequired, project: PropTypes.shape({ @@ -490,12 +502,12 @@ IDEView.propTypes = { name: PropTypes.string.isRequired, owner: PropTypes.shape({ username: PropTypes.string, - id: PropTypes.string, + id: PropTypes.string }), - updatedAt: PropTypes.string, + updatedAt: PropTypes.string }).isRequired, editorAccessibility: PropTypes.shape({ - lintMessages: PropTypes.objectOf(PropTypes.shape()).isRequired, + lintMessages: PropTypes.objectOf(PropTypes.shape()).isRequired }).isRequired, preferences: PropTypes.shape({ autosave: PropTypes.bool.isRequired, @@ -522,21 +534,23 @@ IDEView.propTypes = { setGridOutput: PropTypes.func.isRequired, setSoundOutput: PropTypes.func.isRequired, setAllAccessibleOutput: PropTypes.func.isRequired, - files: PropTypes.arrayOf(PropTypes.shape({ - id: PropTypes.string.isRequired, - name: PropTypes.string.isRequired, - content: PropTypes.string.isRequired, - })).isRequired, + files: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + content: PropTypes.string.isRequired + }) + ).isRequired, selectedFile: PropTypes.shape({ id: PropTypes.string.isRequired, content: PropTypes.string.isRequired, - name: PropTypes.string.isRequired, + name: PropTypes.string.isRequired }).isRequired, setSelectedFile: PropTypes.func.isRequired, htmlFile: PropTypes.shape({ id: PropTypes.string.isRequired, name: PropTypes.string.isRequired, - content: PropTypes.string.isRequired, + content: PropTypes.string.isRequired }).isRequired, newFile: PropTypes.func.isRequired, expandSidebar: PropTypes.func.isRequired, @@ -555,11 +569,11 @@ IDEView.propTypes = { closeShareModal: PropTypes.func.isRequired, closeKeyboardShortcutModal: PropTypes.func.isRequired, toast: PropTypes.shape({ - isVisible: PropTypes.bool.isRequired, + isVisible: PropTypes.bool.isRequired }).isRequired, autosaveProject: PropTypes.func.isRequired, router: PropTypes.shape({ - setRouteLeaveHook: PropTypes.func, + setRouteLeaveHook: PropTypes.func }).isRequired, route: PropTypes.oneOfType([PropTypes.object, PropTypes.element]).isRequired, setTheme: PropTypes.func.isRequired, @@ -578,9 +592,9 @@ function mapStateToProps(state) { return { files: state.files, selectedFile: - state.files.find(file => file.isSelectedFile) || - state.files.find(file => file.name === 'sketch.js') || - state.files.find(file => file.name !== 'root'), + state.files.find((file) => file.isSelectedFile) || + state.files.find((file) => file.name === 'sketch.js') || + state.files.find((file) => file.name !== 'root'), htmlFile: getHTMLFile(state.files), ide: state.ide, preferences: state.preferences, @@ -610,5 +624,6 @@ function mapDispatchToProps(dispatch) { ); } - -export default withTranslation()(withRouter(connect(mapStateToProps, mapDispatchToProps)(IDEView))); +export default withTranslation()( + withRouter(connect(mapStateToProps, mapDispatchToProps)(IDEView)) +); diff --git a/client/modules/IDE/pages/MobileIDEView.jsx b/client/modules/IDE/pages/MobileIDEView.jsx index 0d58887e04..7ca51eae7c 100644 --- a/client/modules/IDE/pages/MobileIDEView.jsx +++ b/client/modules/IDE/pages/MobileIDEView.jsx @@ -18,7 +18,14 @@ import * as EditorAccessibilityActions from '../actions/editorAccessibility'; // Local Imports import Editor from '../components/Editor'; -import { PlayIcon, MoreIcon, FolderIcon, PreferencesIcon, TerminalIcon, SaveIcon } from '../../../common/icons'; +import { + PlayIcon, + MoreIcon, + FolderIcon, + PreferencesIcon, + TerminalIcon, + SaveIcon +} from '../../../common/icons'; import UnsavedChangesDotIcon from '../../../images/unsaved-changes-dot.svg'; import IconButton from '../../../components/mobile/IconButton'; @@ -36,8 +43,10 @@ import useAsModal from '../../../components/useAsModal'; import Dropdown from '../../../components/Dropdown'; import { getIsUserOwner } from '../selectors/users'; - -import { useEffectWithComparison, useEventListener } from '../../../utils/custom-hooks'; +import { + useEffectWithComparison, + useEventListener +} from '../../../utils/custom-hooks'; import * as device from '../../../utils/device'; @@ -45,39 +54,85 @@ const withChangeDot = (title, unsavedChanges = false) => ( <span> {title} <span className="editor__unsaved-changes"> - {unsavedChanges && - <UnsavedChangesDotIcon role="img" aria-label="Sketch has unsaved changes" focusable="false" />} + {unsavedChanges && ( + <UnsavedChangesDotIcon + role="img" + aria-label="Sketch has unsaved changes" + focusable="false" + /> + )} </span> </span> ); -const getRootFile = files => files && files.filter(file => file.name === 'root')[0]; -const getRootFileID = files => (root => root && root.id)(getRootFile(files)); +const getRootFile = (files) => + files && files.filter((file) => file.name === 'root')[0]; +const getRootFileID = (files) => + ((root) => root && root.id)(getRootFile(files)); const Expander = styled.div` - height: ${props => (props.expanded ? remSize(160) : remSize(27))}; + height: ${(props) => (props.expanded ? remSize(160) : remSize(27))}; `; const NavItem = styled.li` position: relative; `; -const getNavOptions = (username = undefined, logoutUser = () => {}, toggleForceDesktop = () => {}) => { +const getNavOptions = ( + username = undefined, + logoutUser = () => {}, + toggleForceDesktop = () => {} +) => { const { t } = useTranslation(); - return (username + return username ? [ - { icon: PreferencesIcon, title: t('MobileIDEView.Preferences'), href: '/preferences', }, - { icon: PreferencesIcon, title: t('MobileIDEView.MyStuff'), href: `/${username}/sketches` }, - { icon: PreferencesIcon, title: t('MobileIDEView.Examples'), href: '/p5/sketches' }, - { icon: PreferencesIcon, title: t('MobileIDEView.OriginalEditor'), action: toggleForceDesktop, }, - { icon: PreferencesIcon, title: t('MobileIDEView.Logout'), action: logoutUser, }, + { + icon: PreferencesIcon, + title: t('MobileIDEView.Preferences'), + href: '/preferences' + }, + { + icon: PreferencesIcon, + title: t('MobileIDEView.MyStuff'), + href: `/${username}/sketches` + }, + { + icon: PreferencesIcon, + title: t('MobileIDEView.Examples'), + href: '/p5/sketches' + }, + { + icon: PreferencesIcon, + title: t('MobileIDEView.OriginalEditor'), + action: toggleForceDesktop + }, + { + icon: PreferencesIcon, + title: t('MobileIDEView.Logout'), + action: logoutUser + } ] : [ - { icon: PreferencesIcon, title: t('MobileIDEView.Preferences'), href: '/preferences', }, - { icon: PreferencesIcon, title: t('MobileIDEView.Examples'), href: '/p5/sketches' }, - { icon: PreferencesIcon, title: t('MobileIDEView.OriginalEditor'), action: toggleForceDesktop, }, - { icon: PreferencesIcon, title: t('MobileIDEView.Login'), href: '/login', }, - ] - ); + { + icon: PreferencesIcon, + title: t('MobileIDEView.Preferences'), + href: '/preferences' + }, + { + icon: PreferencesIcon, + title: t('MobileIDEView.Examples'), + href: '/p5/sketches' + }, + { + icon: PreferencesIcon, + title: t('MobileIDEView.OriginalEditor'), + action: toggleForceDesktop + }, + { + icon: PreferencesIcon, + title: t('MobileIDEView.Login'), + href: '/login' + } + ]; }; const canSaveProject = (isUserOwner, project, user) => @@ -86,18 +141,29 @@ const canSaveProject = (isUserOwner, project, user) => // TODO: This could go into <Editor /> const handleGlobalKeydown = (props, cmController) => (e) => { const { - user, project, ide, + user, + project, + ide, setAllAccessibleOutput, - saveProject, cloneProject, showErrorModal, startSketch, stopSketch, - expandSidebar, collapseSidebar, expandConsole, collapseConsole, - closeNewFolderModal, closeUploadFileModal, closeNewFileModal, isUserOwner + saveProject, + cloneProject, + showErrorModal, + startSketch, + stopSketch, + expandSidebar, + collapseSidebar, + expandConsole, + collapseConsole, + closeNewFolderModal, + closeUploadFileModal, + closeNewFileModal, + isUserOwner } = props; - const isMac = device.isMac(); // const ctrlDown = (e.metaKey && this.isMac) || (e.ctrlKey && !this.isMac); - const ctrlDown = (isMac ? e.metaKey : e.ctrlKey); + const ctrlDown = isMac ? e.metaKey : e.ctrlKey; if (ctrlDown) { if (e.shiftKey) { @@ -109,12 +175,11 @@ const handleGlobalKeydown = (props, cmController) => (e) => { e.preventDefault(); e.stopPropagation(); startSketch(); - // 50 === 2 - } else if (e.keyCode === 50 - ) { + // 50 === 2 + } else if (e.keyCode === 50) { e.preventDefault(); setAllAccessibleOutput(false); - // 49 === 1 + // 49 === 1 } else if (e.keyCode === 49) { e.preventDefault(); setAllAccessibleOutput(true); @@ -123,11 +188,12 @@ const handleGlobalKeydown = (props, cmController) => (e) => { // 83 === s e.preventDefault(); e.stopPropagation(); - if (canSaveProject(isUserOwner, project, user)) saveProject(cmController.getContent(), false, true); + if (canSaveProject(isUserOwner, project, user)) + saveProject(cmController.getContent(), false, true); else if (user.authenticated) cloneProject(); else showErrorModal('forceAuthentication'); - // 13 === enter + // 13 === enter } else if (e.keyCode === 66) { e.preventDefault(); if (!ide.sidebarIsExpanded) expandSidebar(); @@ -144,10 +210,17 @@ const handleGlobalKeydown = (props, cmController) => (e) => { } }; - -const autosave = (autosaveInterval, setAutosaveInterval) => (props, prevProps) => { +const autosave = (autosaveInterval, setAutosaveInterval) => ( + props, + prevProps +) => { const { - autosaveProject, preferences, ide, selectedFile: file, project, isUserOwner + autosaveProject, + preferences, + ide, + selectedFile: file, + project, + isUserOwner } = props; const { selectedFile: oldFile } = prevProps; @@ -187,12 +260,28 @@ const MobileIDEView = (props) => { // } = props; const { - ide, preferences, project, selectedFile, user, params, unsavedChanges, expandConsole, collapseConsole, - stopSketch, startSketch, getProject, clearPersistedState, autosaveProject, saveProject, files, - toggleForceDesktop, logoutUser, toast, isUserOwner + ide, + preferences, + project, + selectedFile, + user, + params, + unsavedChanges, + expandConsole, + collapseConsole, + stopSketch, + startSketch, + getProject, + clearPersistedState, + autosaveProject, + saveProject, + files, + toggleForceDesktop, + logoutUser, + toast, + isUserOwner } = props; - const [cmController, setCmController] = useState(null); // eslint-disable-line const { username } = user; @@ -219,33 +308,54 @@ const MobileIDEView = (props) => { }, [params, project, username]); // Screen Modals - const [toggleNavDropdown, NavDropDown] = useAsModal(<Dropdown - items={getNavOptions(username, logoutUser, toggleForceDesktop)} - align="right" - />); - - const [toggleExplorer, Explorer] = useAsModal(toggle => - (<MobileExplorer - id={getRootFileID(files)} - canEdit={false} - onPressClose={toggle} - />), true); + const [toggleNavDropdown, NavDropDown] = useAsModal( + <Dropdown + items={getNavOptions(username, logoutUser, toggleForceDesktop)} + align="right" + /> + ); + + const [toggleExplorer, Explorer] = useAsModal( + (toggle) => ( + <MobileExplorer + id={getRootFileID(files)} + canEdit={false} + onPressClose={toggle} + /> + ), + true + ); // TODO: This behavior could move to <Editor /> const [autosaveInterval, setAutosaveInterval] = useState(null); useEffectWithComparison(autosave(autosaveInterval, setAutosaveInterval), { - autosaveProject, preferences, ide, selectedFile, project, user, isUserOwner + autosaveProject, + preferences, + ide, + selectedFile, + project, + user, + isUserOwner }); - useEventListener('keydown', handleGlobalKeydown(props, cmController), false, [props]); + useEventListener('keydown', handleGlobalKeydown(props, cmController), false, [ + props + ]); - const projectActions = - [{ - icon: TerminalIcon, aria: 'Toggle console open/closed', action: consoleIsExpanded ? collapseConsole : expandConsole, inverted: true + const projectActions = [ + { + icon: TerminalIcon, + aria: 'Toggle console open/closed', + action: consoleIsExpanded ? collapseConsole : expandConsole, + inverted: true + }, + { + icon: SaveIcon, + aria: 'Save project', + action: () => saveProject(cmController.getContent(), false, true) }, - { icon: SaveIcon, aria: 'Save project', action: () => saveProject(cmController.getContent(), false, true) }, { icon: FolderIcon, aria: 'Open files explorer', action: toggleExplorer } - ]; + ]; return ( <Screen fullscreen> @@ -255,7 +365,6 @@ const MobileIDEView = (props) => { subtitle={filename} > <NavItem> - <IconButton onClick={toggleNavDropdown} icon={MoreIcon} @@ -264,7 +373,14 @@ const MobileIDEView = (props) => { <NavDropDown /> </NavItem> <li> - <IconButton to="/preview" onClick={() => { startSketch(); }} icon={PlayIcon} aria-label="Run sketch" /> + <IconButton + to="/preview" + onClick={() => { + startSketch(); + }} + icon={PlayIcon} + aria-label="Run sketch" + /> </li> </Header> {toast.isVisible && <Toast />} @@ -304,40 +420,40 @@ const handleGlobalKeydownProps = { MobileIDEView.propTypes = { ide: PropTypes.shape({ - consoleIsExpanded: PropTypes.bool.isRequired, + consoleIsExpanded: PropTypes.bool.isRequired }).isRequired, - preferences: PropTypes.shape({ - }).isRequired, + preferences: PropTypes.shape({}).isRequired, project: PropTypes.shape({ id: PropTypes.string, name: PropTypes.string.isRequired, owner: PropTypes.shape({ username: PropTypes.string, - id: PropTypes.string, - }), + id: PropTypes.string + }) }).isRequired, - selectedFile: PropTypes.shape({ id: PropTypes.string.isRequired, content: PropTypes.string.isRequired, - name: PropTypes.string.isRequired, + name: PropTypes.string.isRequired }).isRequired, - files: PropTypes.arrayOf(PropTypes.shape({ - id: PropTypes.string.isRequired, - name: PropTypes.string.isRequired, - content: PropTypes.string.isRequired, - })).isRequired, + files: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + content: PropTypes.string.isRequired + }) + ).isRequired, toggleForceDesktop: PropTypes.func.isRequired, user: PropTypes.shape({ authenticated: PropTypes.bool.isRequired, id: PropTypes.string, - username: PropTypes.string, + username: PropTypes.string }).isRequired, toast: PropTypes.shape({ @@ -359,16 +475,15 @@ MobileIDEView.propTypes = { autosaveProject: PropTypes.func.isRequired, isUserOwner: PropTypes.bool.isRequired, - ...handleGlobalKeydownProps }; function mapStateToProps(state) { return { selectedFile: - state.files.find(file => file.isSelectedFile) || - state.files.find(file => file.name === 'sketch.js') || - state.files.find(file => file.name !== 'root'), + state.files.find((file) => file.isSelectedFile) || + state.files.find((file) => file.name === 'sketch.js') || + state.files.find((file) => file.name !== 'root'), ide: state.ide, files: state.files, unsavedChanges: state.ide.unsavedChanges, @@ -381,12 +496,18 @@ function mapStateToProps(state) { }; } -const mapDispatchToProps = dispatch => bindActionCreators({ - ...ProjectActions, - ...IDEActions, - ...ConsoleActions, - ...PreferencesActions, - ...EditorAccessibilityActions -}, dispatch); +const mapDispatchToProps = (dispatch) => + bindActionCreators( + { + ...ProjectActions, + ...IDEActions, + ...ConsoleActions, + ...PreferencesActions, + ...EditorAccessibilityActions + }, + dispatch + ); -export default withRouter(connect(mapStateToProps, mapDispatchToProps)(MobileIDEView)); +export default withRouter( + connect(mapStateToProps, mapDispatchToProps)(MobileIDEView) +); diff --git a/client/modules/IDE/reducers/assets.js b/client/modules/IDE/reducers/assets.js index a251d8b29e..f1dd0f40cb 100644 --- a/client/modules/IDE/reducers/assets.js +++ b/client/modules/IDE/reducers/assets.js @@ -11,7 +11,7 @@ const assets = (state = initialState, action) => { case ActionTypes.SET_ASSETS: return { list: action.assets, totalSize: action.totalSize }; case ActionTypes.DELETE_ASSET: - return { list: state.list.filter(asset => asset.key !== action.key) }; + return { list: state.list.filter((asset) => asset.key !== action.key) }; default: return state; } diff --git a/client/modules/IDE/reducers/editorAccessibility.js b/client/modules/IDE/reducers/editorAccessibility.js index 73d6cd753d..2f46ec2bae 100644 --- a/client/modules/IDE/reducers/editorAccessibility.js +++ b/client/modules/IDE/reducers/editorAccessibility.js @@ -12,13 +12,16 @@ const editorAccessibility = (state = initialState, action) => { messageId += 1; return Object.assign({}, state, { lintMessages: state.lintMessages.concat({ - severity: action.severity, line: action.line, message: action.message, id: messageId + severity: action.severity, + line: action.line, + message: action.message, + id: messageId }) }); case ActionTypes.CLEAR_LINT_MESSAGE: return Object.assign({}, state, { lintMessages: [] }); case ActionTypes.TOGGLE_FORCE_DESKTOP: - return Object.assign({}, state, { forceDesktop: !(state.forceDesktop) }); + return Object.assign({}, state, { forceDesktop: !state.forceDesktop }); default: return state; } diff --git a/client/modules/IDE/reducers/files.js b/client/modules/IDE/reducers/files.js index 80557fb35a..19a82335f0 100644 --- a/client/modules/IDE/reducers/files.js +++ b/client/modules/IDE/reducers/files.js @@ -9,8 +9,7 @@ function draw() { background(220); }`; -const defaultHTML = -`<!DOCTYPE html> +const defaultHTML = `<!DOCTYPE html> <html lang="en"> <head> <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.1.9/p5.js"></script> @@ -25,8 +24,7 @@ const defaultHTML = </html> `; -const defaultCSS = -`html, body { +const defaultCSS = `html, body { margin: 0; padding: 0; } @@ -73,21 +71,28 @@ const initialState = () => { _id: c, fileType: 'file', children: [] - }]; + } + ]; }; function getAllDescendantIds(state, nodeId) { - return state.find(file => file.id === nodeId).children - .reduce((acc, childId) => ( - [...acc, childId, ...getAllDescendantIds(state, childId)] - ), []); + return state + .find((file) => file.id === nodeId) + .children.reduce( + (acc, childId) => [ + ...acc, + childId, + ...getAllDescendantIds(state, childId) + ], + [] + ); } function deleteChild(state, parentId, id) { const newState = state.map((file) => { if (file.id === parentId) { const newFile = Object.assign({}, file); - newFile.children = newFile.children.filter(child => child !== id); + newFile.children = newFile.children.filter((child) => child !== id); return newFile; } return file; @@ -111,9 +116,9 @@ function deleteMany(state, ids) { } function sortedChildrenId(state, children) { - const childrenArray = state.filter(file => children.includes(file.id)); + const childrenArray = state.filter((file) => children.includes(file.id)); childrenArray.sort((a, b) => (a.name > b.name ? 1 : -1)); - return childrenArray.map(child => child.id); + return childrenArray.map((child) => child.id); } function updateParent(state, action) { @@ -174,7 +179,8 @@ const files = (state, action) => { url: action.url, children: action.children, fileType: action.fileType || 'file' - }]; + } + ]; return newState.map((file) => { if (file.id === action.parentId) { file.children = sortedChildrenId(newState, file.children); @@ -182,8 +188,7 @@ const files = (state, action) => { return file; }); } - case ActionTypes.UPDATE_FILE_NAME: - { + case ActionTypes.UPDATE_FILE_NAME: { const newState = renameFile(state, action); return newState.map((file) => { if (file.children.includes(action.id)) { @@ -192,9 +197,11 @@ const files = (state, action) => { return file; }); } - case ActionTypes.DELETE_FILE: - { - const newState = deleteMany(state, [action.id, ...getAllDescendantIds(state, action.id)]); + case ActionTypes.DELETE_FILE: { + const newState = deleteMany(state, [ + action.id, + ...getAllDescendantIds(state, action.id) + ]); return deleteChild(newState, action.parentId, action.id); // const newState = state.map((file) => { // if (file.id === action.parentId) { @@ -234,9 +241,12 @@ const files = (state, action) => { } }; -export const getHTMLFile = state => state.filter(file => file.name.match(/.*\.html$/i))[0]; -export const getJSFiles = state => state.filter(file => file.name.match(/.*\.js$/i)); -export const getCSSFiles = state => state.filter(file => file.name.match(/.*\.css$/i)); -export const getLinkedFiles = state => state.filter(file => file.url); +export const getHTMLFile = (state) => + state.filter((file) => file.name.match(/.*\.html$/i))[0]; +export const getJSFiles = (state) => + state.filter((file) => file.name.match(/.*\.js$/i)); +export const getCSSFiles = (state) => + state.filter((file) => file.name.match(/.*\.css$/i)); +export const getLinkedFiles = (state) => state.filter((file) => file.url); export default files; diff --git a/client/modules/IDE/reducers/ide.js b/client/modules/IDE/reducers/ide.js index 91e1080cb7..56eeaab6f5 100644 --- a/client/modules/IDE/reducers/ide.js +++ b/client/modules/IDE/reducers/ide.js @@ -78,7 +78,7 @@ const ide = (state = initialState, action) => { shareModalVisible: true, shareModalProjectId: action.payload.shareModalProjectId, shareModalProjectName: action.payload.shareModalProjectName, - shareModalProjectUsername: action.payload.shareModalProjectUsername, + shareModalProjectUsername: action.payload.shareModalProjectUsername }); case ActionTypes.CLOSE_SHARE_MODAL: return Object.assign({}, state, { shareModalVisible: false }); @@ -93,9 +93,15 @@ const ide = (state = initialState, action) => { case ActionTypes.SET_UNSAVED_CHANGES: return Object.assign({}, state, { unsavedChanges: action.value }); case ActionTypes.DETECT_INFINITE_LOOPS: - return Object.assign({}, state, { infiniteLoop: true, infiniteLoopMessage: action.message }); + return Object.assign({}, state, { + infiniteLoop: true, + infiniteLoopMessage: action.message + }); case ActionTypes.RESET_INFINITE_LOOPS: - return Object.assign({}, state, { infiniteLoop: false, infiniteLoopMessage: '' }); + return Object.assign({}, state, { + infiniteLoop: false, + infiniteLoopMessage: '' + }); case ActionTypes.START_SKETCH_REFRESH: return Object.assign({}, state, { previewIsRefreshing: true }); case ActionTypes.END_SKETCH_REFRESH: @@ -115,7 +121,10 @@ const ide = (state = initialState, action) => { case ActionTypes.SHOW_RUNTIME_ERROR_WARNING: return Object.assign({}, state, { runtimeErrorWarningVisible: true }); case ActionTypes.OPEN_UPLOAD_FILE_MODAL: - return Object.assign({}, state, { uploadFileModalVisible: true, parentId: action.parentId }); + return Object.assign({}, state, { + uploadFileModalVisible: true, + parentId: action.parentId + }); case ActionTypes.CLOSE_UPLOAD_FILE_MODAL: return Object.assign({}, state, { uploadFileModalVisible: false }); default: diff --git a/client/modules/IDE/reducers/preferences.js b/client/modules/IDE/reducers/preferences.js index cc8217c9cd..af760ea8f6 100644 --- a/client/modules/IDE/reducers/preferences.js +++ b/client/modules/IDE/reducers/preferences.js @@ -1,6 +1,5 @@ import * as ActionTypes from '../../../constants'; - const initialState = { fontSize: 18, autosave: true, @@ -43,7 +42,9 @@ const preferences = (state = initialState, action) => { case ActionTypes.SET_LANGUAGE: return Object.assign({}, state, { language: action.language }); case ActionTypes.SET_AUTOCLOSE_BRACKETS_QUOTES: - return Object.assign({}, state, { autocloseBracketsQuotes: action.value }); + return Object.assign({}, state, { + autocloseBracketsQuotes: action.value + }); default: return state; } diff --git a/client/modules/IDE/reducers/project.js b/client/modules/IDE/reducers/project.js index 2eb19d4de2..df0da82411 100644 --- a/client/modules/IDE/reducers/project.js +++ b/client/modules/IDE/reducers/project.js @@ -3,7 +3,8 @@ import { generateProjectName } from '../../../utils/generateRandomName'; const initialState = () => { const generatedString = generateProjectName(); - const generatedName = generatedString.charAt(0).toUpperCase() + generatedString.slice(1); + const generatedName = + generatedString.charAt(0).toUpperCase() + generatedString.slice(1); return { name: generatedName, updatedAt: '', diff --git a/client/modules/IDE/reducers/projects.js b/client/modules/IDE/reducers/projects.js index ff06c5c720..5950a042fa 100644 --- a/client/modules/IDE/reducers/projects.js +++ b/client/modules/IDE/reducers/projects.js @@ -5,8 +5,7 @@ const sketches = (state = [], action) => { case ActionTypes.SET_PROJECTS: return action.projects; case ActionTypes.DELETE_PROJECT: - return state.filter(sketch => - sketch.id !== action.id); + return state.filter((sketch) => sketch.id !== action.id); case ActionTypes.RENAME_PROJECT: { return state.map((sketch) => { if (sketch.id === action.payload.id) { diff --git a/client/modules/IDE/reducers/sorting.js b/client/modules/IDE/reducers/sorting.js index 91c7adddf3..747d16c80a 100644 --- a/client/modules/IDE/reducers/sorting.js +++ b/client/modules/IDE/reducers/sorting.js @@ -20,7 +20,11 @@ const sorting = (state = initialState, action) => { } return { ...state, direction: DIRECTION.ASC }; case ActionTypes.SET_SORTING: - return { ...state, field: action.payload.field, direction: action.payload.direction }; + return { + ...state, + field: action.payload.field, + direction: action.payload.direction + }; default: return state; } diff --git a/client/modules/IDE/selectors/collections.js b/client/modules/IDE/selectors/collections.js index 3571ef80ef..207dce39a9 100644 --- a/client/modules/IDE/selectors/collections.js +++ b/client/modules/IDE/selectors/collections.js @@ -4,10 +4,10 @@ import find from 'lodash/find'; import orderBy from 'lodash/orderBy'; import { DIRECTION } from '../actions/sorting'; -const getCollections = state => state.collections; -const getField = state => state.sorting.field; -const getDirection = state => state.sorting.direction; -const getSearchTerm = state => state.search.collectionSearchTerm; +const getCollections = (state) => state.collections; +const getField = (state) => state.sorting.field; +const getDirection = (state) => state.sorting.direction; +const getSearchTerm = (state) => state.search.collectionSearchTerm; const getFilteredCollections = createSelector( getCollections, @@ -18,15 +18,19 @@ const getFilteredCollections = createSelector( const smallCollection = { name: collection.name }; - return { ...collection, searchString: Object.values(smallCollection).join(' ').toLowerCase() }; + return { + ...collection, + searchString: Object.values(smallCollection).join(' ').toLowerCase() + }; }); - return searchStrings.filter(collection => collection.searchString.includes(search.toLowerCase())); + return searchStrings.filter((collection) => + collection.searchString.includes(search.toLowerCase()) + ); } return collections; } ); - const getSortedCollections = createSelector( getFilteredCollections, getField, diff --git a/client/modules/IDE/selectors/projects.js b/client/modules/IDE/selectors/projects.js index 4552ff25c3..08701d211c 100644 --- a/client/modules/IDE/selectors/projects.js +++ b/client/modules/IDE/selectors/projects.js @@ -3,10 +3,10 @@ import differenceInMilliseconds from 'date-fns/differenceInMilliseconds'; import orderBy from 'lodash/orderBy'; import { DIRECTION } from '../actions/sorting'; -const getSketches = state => state.sketches; -const getField = state => state.sorting.field; -const getDirection = state => state.sorting.direction; -const getSearchTerm = state => state.search.sketchSearchTerm; +const getSketches = (state) => state.sketches; +const getField = (state) => state.sorting.field; +const getDirection = (state) => state.sorting.direction; +const getSearchTerm = (state) => state.search.sketchSearchTerm; const getFilteredSketches = createSelector( getSketches, @@ -17,9 +17,14 @@ const getFilteredSketches = createSelector( const smallSketch = { name: sketch.name }; - return { ...sketch, searchString: Object.values(smallSketch).join(' ').toLowerCase() }; + return { + ...sketch, + searchString: Object.values(smallSketch).join(' ').toLowerCase() + }; }); - return searchStrings.filter(sketch => sketch.searchString.includes(search.toLowerCase())); + return searchStrings.filter((sketch) => + sketch.searchString.includes(search.toLowerCase()) + ); } return sketches; } diff --git a/client/modules/IDE/selectors/users.js b/client/modules/IDE/selectors/users.js index 66a933ca67..4086348041 100644 --- a/client/modules/IDE/selectors/users.js +++ b/client/modules/IDE/selectors/users.js @@ -1,11 +1,11 @@ import { createSelector } from 'reselect'; import getConfig from '../../../utils/getConfig'; -const getAuthenticated = state => state.user.authenticated; -const getTotalSize = state => state.user.totalSize; -const getAssetsTotalSize = state => state.assets.totalSize; -const getSketchOwner = state => state.project.owner; -const getUserId = state => state.user.id; +const getAuthenticated = (state) => state.user.authenticated; +const getTotalSize = (state) => state.user.totalSize; +const getAssetsTotalSize = (state) => state.assets.totalSize; +const getSketchOwner = (state) => state.project.owner; +const getUserId = (state) => state.user.id; const limit = getConfig('UPLOAD_LIMIT') || 250000000; export const getCanUploadMedia = createSelector( diff --git a/client/modules/Mobile/MobileDashboardView.jsx b/client/modules/Mobile/MobileDashboardView.jsx index 51f074fb9a..5daa88fe97 100644 --- a/client/modules/Mobile/MobileDashboardView.jsx +++ b/client/modules/Mobile/MobileDashboardView.jsx @@ -15,7 +15,10 @@ import SketchList from '../IDE/components/SketchList'; import CollectionList from '../IDE/components/CollectionList'; import AssetList from '../IDE/components/AssetList'; import Content from './MobileViewContent'; -import { SketchSearchbar, CollectionSearchbar } from '../IDE/components/Searchbar'; +import { + SketchSearchbar, + CollectionSearchbar +} from '../IDE/components/Searchbar'; import Button from '../../common/Button'; import useAsModal from '../../components/useAsModal'; import Dropdown from '../../components/Dropdown'; @@ -29,13 +32,14 @@ const EXAMPLE_USERNAME = 'p5'; const ContentWrapper = styled(Content)` table { table-layout: fixed; - margin-bottom: ${remSize(120)} + margin-bottom: ${remSize(120)}; } - - td ,thead button { + + td, + thead button { font-size: ${remSize(10)}; text-align: left; - }; + } tbody th { font-size: ${remSize(16)}; @@ -44,61 +48,75 @@ const ContentWrapper = styled(Content)` font-weight: bold; display: flex; grid-area: name; - }; + } - tbody td, thead th { + tbody td, + thead th { justify-self: center; align-self: flex-end; - color: ${prop('primaryTextColor')} + color: ${prop('primaryTextColor')}; } + thead th svg { + margin-left: ${remSize(8)}; + } - thead th svg { margin-left: ${remSize(8)} } - - - tbody td { justify-self: start; text-align: start; padding: 0 } - tbody td:nth-child(2) { justify-self: start; text-align: start; padding-left: ${remSize(12)}}; - tbody td:last-child { + tbody td { + justify-self: start; + text-align: start; + padding: 0; + } + tbody td:nth-child(2) { + justify-self: start; + text-align: start; + padding-left: ${remSize(12)}; + } + tbody td:last-child { justify-self: end; text-align: end; grid-row-start: 1; grid-column-start: 3; - }; + } - .sketch-list__dropdown-column { width: auto; }; + .sketch-list__dropdown-column { + width: auto; + } - tbody { height: ${remSize(48)}; } + tbody { + height: ${remSize(48)}; + } .sketches-table-container { background: ${prop('SketchList.background')}; - } + } .sketches-table__row { background: ${prop('SketchList.card.background')} !important; - height: auto + height: auto; } - - tr { align-self: start; display: grid; box-shadow: 0 0 18px 0 ${prop('shadowColor')}; - }; + } thead tr { - grid-template-columns: repeat(${props => props.fieldcount}, 1fr) 0fr; - ${props => props.noheader && 'display: none;'} + grid-template-columns: repeat(${(props) => props.fieldcount}, 1fr) 0fr; + ${(props) => props.noheader && 'display: none;'} } tbody tr { padding: ${remSize(8)}; border-radius: ${remSize(4)}; - grid-template-columns: repeat(${props => props.fieldcount - 1}) 1fr; - grid-template-areas: "name name name" "content content content"; - grid-row-gap: ${remSize(12)} + grid-template-columns: repeat(${(props) => props.fieldcount - 1}) 1fr; + grid-template-areas: 'name name name' 'content content content'; + grid-row-gap: ${remSize(12)}; } - .loader-container { position: fixed ; padding-bottom: 32% } + .loader-container { + position: fixed; + padding-bottom: 32%; + } .sketches-table thead th { background-color: transparent; @@ -110,21 +128,24 @@ const ContentWrapper = styled(Content)` } .asset-table thead tr { - height: ${remSize(32)} + height: ${remSize(32)}; } - `; const Subheader = styled.div` display: flex; flex-direction: row; - * { border-radius: 0px; } + * { + border-radius: 0px; + } .searchbar { display: flex; width: 100%; } - .searchbar__input { width: 100%; } + .searchbar__input { + width: 100%; + } `; const SubheaderButton = styled(Button)` @@ -137,19 +158,22 @@ const Panels = { assets: AssetList }; - const navOptions = (username) => { const { t } = useTranslation(); return [ { title: t('MobileDashboardView.CreateSketch'), href: '/' }, - { title: t('MobileDashboardView.CreateCollection'), href: `/${username}/collections/create` } + { + title: t('MobileDashboardView.CreateCollection'), + href: `/${username}/collections/create` + } ]; }; - const getPanel = (pathname) => { const pathparts = pathname ? pathname.split('/') : []; - const matches = Object.keys(Panels).map(part => part.toLowerCase()).filter(part => pathparts.includes(part)); + const matches = Object.keys(Panels) + .map((part) => part.toLowerCase()) + .filter((part) => pathparts.includes(part)); return matches && matches.length > 0 && matches[0]; }; @@ -157,13 +181,14 @@ const NavItem = styled.li` position: relative; `; +const isOwner = (user, params) => + user && params && user.username === params.username; -const isOwner = (user, params) => user && params && user.username === params.username; - -const renderPanel = (name, props) => (Component => (Component && <Component {...props} mobile />))(Panels[name]); +const renderPanel = (name, props) => + ((Component) => Component && <Component {...props} mobile />)(Panels[name]); const MobileDashboard = ({ params, location }) => { - const user = useSelector(state => state.user); + const user = useSelector((state) => state.user); const { username: paramsUsername } = params; const { pathname } = location; const { t } = useTranslation(); @@ -177,16 +202,21 @@ const MobileDashboard = ({ params, location }) => { const isExamples = paramsUsername === EXAMPLE_USERNAME; const panel = getPanel(pathname); - - const [toggleNavDropdown, NavDropdown] = useAsModal(<Dropdown - items={navOptions(user.username)} - align="right" - />); - + const [toggleNavDropdown, NavDropdown] = useAsModal( + <Dropdown items={navOptions(user.username)} align="right" /> + ); return ( <Screen fullscreen key={pathname}> - <Header slim inverted title={isExamples ? t('MobileDashboardView.Examples') : t('MobileDashboardView.MyStuff')}> + <Header + slim + inverted + title={ + isExamples + ? t('MobileDashboardView.Examples') + : t('MobileDashboardView.MyStuff') + } + > <NavItem> <IconButton onClick={toggleNavDropdown} @@ -194,13 +224,15 @@ const MobileDashboard = ({ params, location }) => { aria-label="Options" /> <NavDropdown /> - </NavItem> <IconButton to="/" icon={ExitIcon} aria-label="Return to ide view" /> </Header> - - <ContentWrapper slimheader fieldcount={panel === Tabs[1] ? 4 : 3} noheader={panel === Tabs[2]}> + <ContentWrapper + slimheader + fieldcount={panel === Tabs[1] ? 4 : 3} + noheader={panel === Tabs[2]} + > <Subheader> {panel === Tabs[0] && <SketchSearchbar />} {panel === Tabs[1] && <CollectionSearchbar />} @@ -208,24 +240,28 @@ const MobileDashboard = ({ params, location }) => { {renderPanel(panel, { username: paramsUsername, key: pathname })} </ContentWrapper> <Footer> - {!isExamples && + {!isExamples && ( <FooterTabSwitcher> - {Tabs.map(tab => ( + {Tabs.map((tab) => ( <FooterTab key={`tab-${tab}`} selected={tab === panel} to={pathname.replace(panel, tab)} > - <h3>{(isExamples && tab === 'Sketches') ? t('MobileDashboardView.Examples') : (TabLabels[tab] || tab)}</h3> - </FooterTab>)) - } + <h3> + {isExamples && tab === 'Sketches' + ? t('MobileDashboardView.Examples') + : TabLabels[tab] || tab} + </h3> + </FooterTab> + ))} </FooterTabSwitcher> - } + )} </Footer> - </Screen>); + </Screen> + ); }; - MobileDashboard.propTypes = { location: PropTypes.shape({ pathname: PropTypes.string.isRequired @@ -236,5 +272,4 @@ MobileDashboard.propTypes = { }; MobileDashboard.defaultProps = { params: {} }; - export default withRouter(MobileDashboard); diff --git a/client/modules/Mobile/MobilePreferences.jsx b/client/modules/Mobile/MobilePreferences.jsx index 07701e2684..cd6303a79e 100644 --- a/client/modules/Mobile/MobilePreferences.jsx +++ b/client/modules/Mobile/MobilePreferences.jsx @@ -16,7 +16,11 @@ import Header from '../../components/mobile/Header'; import PreferencePicker from '../../components/mobile/PreferencePicker'; import { ExitIcon } from '../../common/icons'; import { remSize, prop } from '../../theme'; -import { optionsOnOff, optionsPickOne, preferenceOnOff } from '../IDE/components/Preferences/PreferenceCreators'; +import { + optionsOnOff, + optionsPickOne, + preferenceOnOff +} from '../IDE/components/Preferences/PreferenceCreators'; const Content = styled.div` z-index: 0; @@ -32,17 +36,33 @@ const SectionSubeader = styled.h3` color: ${prop('primaryTextColor')}; `; - const MobilePreferences = () => { // Props const { - theme, autosave, linewrap, textOutput, gridOutput, soundOutput, lineNumbers, lintWarning - } = useSelector(state => state.preferences); + theme, + autosave, + linewrap, + textOutput, + gridOutput, + soundOutput, + lineNumbers, + lintWarning + } = useSelector((state) => state.preferences); // Actions const { - setTheme, setAutosave, setLinewrap, setTextOutput, setGridOutput, setSoundOutput, setLineNumbers, setLintWarning, - } = bindActionCreators({ ...PreferencesActions, ...IdeActions }, useDispatch()); + setTheme, + setAutosave, + setLinewrap, + setTextOutput, + setGridOutput, + setSoundOutput, + setLineNumbers, + setLintWarning + } = bindActionCreators( + { ...PreferencesActions, ...IdeActions }, + useDispatch() + ); const { t } = useTranslation(); @@ -56,21 +76,54 @@ const MobilePreferences = () => { t('MobilePreferences.DarkTheme'), t('MobilePreferences.HighContrastTheme') ), - onSelect: x => setTheme(x) // setTheme + onSelect: (x) => setTheme(x) // setTheme }, - preferenceOnOff(t('MobilePreferences.Autosave'), autosave, setAutosave, 'autosave'), - preferenceOnOff(t('MobilePreferences.WordWrap'), linewrap, setLinewrap, 'linewrap') + preferenceOnOff( + t('MobilePreferences.Autosave'), + autosave, + setAutosave, + 'autosave' + ), + preferenceOnOff( + t('MobilePreferences.WordWrap'), + linewrap, + setLinewrap, + 'linewrap' + ) ]; const outputSettings = [ - preferenceOnOff(t('MobilePreferences.PlainText'), textOutput, setTextOutput, 'text output'), - preferenceOnOff(t('MobilePreferences.TableText'), gridOutput, setGridOutput, 'table output'), - preferenceOnOff(t('MobilePreferences.Sound'), soundOutput, setSoundOutput, 'sound output') + preferenceOnOff( + t('MobilePreferences.PlainText'), + textOutput, + setTextOutput, + 'text output' + ), + preferenceOnOff( + t('MobilePreferences.TableText'), + gridOutput, + setGridOutput, + 'table output' + ), + preferenceOnOff( + t('MobilePreferences.Sound'), + soundOutput, + setSoundOutput, + 'sound output' + ) ]; const accessibilitySettings = [ - preferenceOnOff(t('MobilePreferences.LineNumbers'), lineNumbers, setLineNumbers), - preferenceOnOff(t('MobilePreferences.LintWarningSound'), lintWarning, setLintWarning) + preferenceOnOff( + t('MobilePreferences.LineNumbers'), + lineNumbers, + setLineNumbers + ), + preferenceOnOff( + t('MobilePreferences.LintWarningSound'), + lintWarning, + setLintWarning + ) ]; return ( @@ -81,20 +134,34 @@ const MobilePreferences = () => { </Header> <section className="preferences"> <Content> - <SectionHeader>{t('MobilePreferences.GeneralSettings')}</SectionHeader> - { generalSettings.map(option => <PreferencePicker key={`${option.title}wrapper`} {...option} />) } - - <SectionHeader>{t('MobilePreferences.Accessibility')}</SectionHeader> - { accessibilitySettings.map(option => <PreferencePicker key={`${option.title}wrapper`} {...option} />) } - - <SectionHeader>{t('MobilePreferences.AccessibleOutput')}</SectionHeader> - <SectionSubeader>{t('MobilePreferences.UsedScreenReader')}</SectionSubeader> - { outputSettings.map(option => <PreferencePicker key={`${option.title}wrapper`} {...option} />) } - + <SectionHeader> + {t('MobilePreferences.GeneralSettings')} + </SectionHeader> + {generalSettings.map((option) => ( + <PreferencePicker key={`${option.title}wrapper`} {...option} /> + ))} + + <SectionHeader> + {t('MobilePreferences.Accessibility')} + </SectionHeader> + {accessibilitySettings.map((option) => ( + <PreferencePicker key={`${option.title}wrapper`} {...option} /> + ))} + + <SectionHeader> + {t('MobilePreferences.AccessibleOutput')} + </SectionHeader> + <SectionSubeader> + {t('MobilePreferences.UsedScreenReader')} + </SectionSubeader> + {outputSettings.map((option) => ( + <PreferencePicker key={`${option.title}wrapper`} {...option} /> + ))} </Content> </section> </section> - </Screen>); + </Screen> + ); }; export default withRouter(MobilePreferences); diff --git a/client/modules/Mobile/MobileSketchView.jsx b/client/modules/Mobile/MobileSketchView.jsx index a4238fec07..4c2fecbe0d 100644 --- a/client/modules/Mobile/MobileSketchView.jsx +++ b/client/modules/Mobile/MobileSketchView.jsx @@ -21,44 +21,65 @@ import Footer from '../../components/mobile/Footer'; import Content from './MobileViewContent'; const MobileSketchView = () => { - const { files, ide, preferences } = useSelector(state => state); + const { files, ide, preferences } = useSelector((state) => state); - const htmlFile = useSelector(state => getHTMLFile(state.files)); - const projectName = useSelector(state => state.project.name); - const selectedFile = useSelector(state => state.files.find(file => file.isSelectedFile) || - state.files.find(file => file.name === 'sketch.js') || - state.files.find(file => file.name !== 'root')); + const htmlFile = useSelector((state) => getHTMLFile(state.files)); + const projectName = useSelector((state) => state.project.name); + const selectedFile = useSelector( + (state) => + state.files.find((file) => file.isSelectedFile) || + state.files.find((file) => file.name === 'sketch.js') || + state.files.find((file) => file.name !== 'root') + ); const { - setTextOutput, setGridOutput, setSoundOutput, dispatchConsoleEvent, - endSketchRefresh, stopSketch, setBlobUrl, expandConsole, clearConsole - } = bindActionCreators({ - ...ProjectActions, ...IDEActions, ...PreferencesActions, ...ConsoleActions, ...FilesActions - }, useDispatch()); + setTextOutput, + setGridOutput, + setSoundOutput, + dispatchConsoleEvent, + endSketchRefresh, + stopSketch, + setBlobUrl, + expandConsole, + clearConsole + } = bindActionCreators( + { + ...ProjectActions, + ...IDEActions, + ...PreferencesActions, + ...ConsoleActions, + ...FilesActions + }, + useDispatch() + ); return ( <Screen fullscreen> <Header - leftButton={<IconButton to="/" icon={ExitIcon} aria-label="Return to original editor" />} + leftButton={ + <IconButton + to="/" + icon={ExitIcon} + aria-label="Return to original editor" + /> + } title={projectName} /> <Content> <PreviewFrame htmlFile={htmlFile} files={files} - head={<link type="text/css" rel="stylesheet" href="/preview-styles.css" />} - + head={ + <link type="text/css" rel="stylesheet" href="/preview-styles.css" /> + } content={selectedFile.content} - isPlaying isAccessibleOutputPlaying={ide.isAccessibleOutputPlaying} previewIsRefreshing={ide.previewIsRefreshing} - textOutput={preferences.textOutput} gridOutput={preferences.gridOutput} soundOutput={preferences.soundOutput} autorefresh={preferences.autorefresh} - setTextOutput={setTextOutput} setGridOutput={setGridOutput} setSoundOutput={setSoundOutput} @@ -73,7 +94,8 @@ const MobileSketchView = () => { <Footer> <Console /> </Footer> - </Screen>); + </Screen> + ); }; export default MobileSketchView; diff --git a/client/modules/Mobile/MobileViewContent.jsx b/client/modules/Mobile/MobileViewContent.jsx index 39d2a59f0d..c8c91c2c6f 100644 --- a/client/modules/Mobile/MobileViewContent.jsx +++ b/client/modules/Mobile/MobileViewContent.jsx @@ -2,9 +2,8 @@ import React from 'react'; import styled from 'styled-components'; import { remSize } from '../../theme'; - export default styled.div` /* Dashboard Styles */ z-index: 0; - margin-top: ${props => remSize(props.slimheader ? 49 : 68)}; + margin-top: ${(props) => remSize(props.slimheader ? 49 : 68)}; `; diff --git a/client/modules/User/actions.js b/client/modules/User/actions.js index 363a4e4d90..210c2288d6 100644 --- a/client/modules/User/actions.js +++ b/client/modules/User/actions.js @@ -57,13 +57,21 @@ export function validateAndLoginUser(formProps) { .then((response) => { dispatch(loginUserSuccess(response.data)); dispatch(setPreferences(response.data.preferences)); - dispatch(setLanguage(response.data.preferences.language, { persistPreference: false })); + dispatch( + setLanguage(response.data.preferences.language, { + persistPreference: false + }) + ); dispatch(justOpenedProject()); browserHistory.push(previousPath); resolve(); }) - .catch(error => - resolve({ password: error.response.data.message, _error: 'Login failed!' })); + .catch((error) => + resolve({ + password: error.response.data.message, + _error: 'Login failed!' + }) + ); }); }; } @@ -91,7 +99,8 @@ export function validateAndSignUpUser(formValues) { export function getUser() { return (dispatch) => { - apiClient.get('/session') + apiClient + .get('/session') .then((response) => { dispatch({ type: ActionTypes.AUTH_USER, @@ -101,8 +110,11 @@ export function getUser() { type: ActionTypes.SET_PREFERENCES, preferences: response.data.preferences }); - setLanguage(response.data.preferences.language, { persistPreference: false }); - }).catch((error) => { + setLanguage(response.data.preferences.language, { + persistPreference: false + }); + }) + .catch((error) => { const { response } = error; const message = response.message || response.data.error; dispatch(authError(message)); @@ -112,7 +124,8 @@ export function getUser() { export function validateSession() { return (dispatch, getState) => { - apiClient.get('/session') + apiClient + .get('/session') .then((response) => { const state = getState(); if (state.user.username !== response.data.username) { @@ -130,7 +143,8 @@ export function validateSession() { export function logoutUser() { return (dispatch) => { - apiClient.get('/logout') + apiClient + .get('/logout') .then(() => { dispatch({ type: ActionTypes.UNAUTH_USER @@ -144,21 +158,23 @@ export function logoutUser() { } export function initiateResetPassword(formValues) { - return dispatch => new Promise((resolve) => { - dispatch({ - type: ActionTypes.RESET_PASSWORD_INITIATE - }); - return apiClient.post('/reset-password', formValues) - .then(() => resolve()) - .catch((error) => { - const { response } = error; - dispatch({ - type: ActionTypes.ERROR, - message: response.data - }); - resolve({ error }); + return (dispatch) => + new Promise((resolve) => { + dispatch({ + type: ActionTypes.RESET_PASSWORD_INITIATE }); - }); + return apiClient + .post('/reset-password', formValues) + .then(() => resolve()) + .catch((error) => { + const { response } = error; + dispatch({ + type: ActionTypes.ERROR, + message: response.data + }); + resolve({ error }); + }); + }); } export function initiateVerification() { @@ -166,7 +182,8 @@ export function initiateVerification() { dispatch({ type: ActionTypes.EMAIL_VERIFICATION_INITIATE }); - apiClient.post('/verify/send', {}) + apiClient + .post('/verify/send', {}) .then(() => { // do nothing }) @@ -184,13 +201,16 @@ export function verifyEmailConfirmation(token) { return (dispatch) => { dispatch({ type: ActionTypes.EMAIL_VERIFICATION_VERIFY, - state: 'checking', + state: 'checking' }); - return apiClient.get(`/verify?t=${token}`, {}) - .then(response => dispatch({ - type: ActionTypes.EMAIL_VERIFICATION_VERIFIED, - message: response.data, - })) + return apiClient + .get(`/verify?t=${token}`, {}) + .then((response) => + dispatch({ + type: ActionTypes.EMAIL_VERIFICATION_VERIFIED, + message: response.data + }) + ) .catch((error) => { const { response } = error; dispatch({ @@ -201,7 +221,6 @@ export function verifyEmailConfirmation(token) { }; } - export function resetPasswordReset() { return { type: ActionTypes.RESET_PASSWORD_RESET @@ -210,30 +229,36 @@ export function resetPasswordReset() { export function validateResetPasswordToken(token) { return (dispatch) => { - apiClient.get(`/reset-password/${token}`) + apiClient + .get(`/reset-password/${token}`) .then(() => { // do nothing if the token is valid }) - .catch(() => dispatch({ - type: ActionTypes.INVALID_RESET_PASSWORD_TOKEN - })); + .catch(() => + dispatch({ + type: ActionTypes.INVALID_RESET_PASSWORD_TOKEN + }) + ); }; } export function updatePassword(formValues, token) { - return dispatch => new Promise(resolve => - apiClient.post(`/reset-password/${token}`, formValues) - .then((response) => { - dispatch(loginUserSuccess(response.data)); - browserHistory.push('/'); - resolve(); - }) - .catch((error) => { - dispatch({ - type: ActionTypes.INVALID_RESET_PASSWORD_TOKEN - }); - resolve({ error }); - })); + return (dispatch) => + new Promise((resolve) => + apiClient + .post(`/reset-password/${token}`, formValues) + .then((response) => { + dispatch(loginUserSuccess(response.data)); + browserHistory.push('/'); + resolve(); + }) + .catch((error) => { + dispatch({ + type: ActionTypes.INVALID_RESET_PASSWORD_TOKEN + }); + resolve({ error }); + }) + ); } export function updateSettingsSuccess(user) { @@ -248,17 +273,20 @@ export function submitSettings(formValues) { } export function updateSettings(formValues) { - return dispatch => - new Promise(resolve => - submitSettings(formValues).then((response) => { - dispatch(updateSettingsSuccess(response.data)); - dispatch(showToast(5500)); - dispatch(setToastText('Toast.SettingsSaved')); - resolve(); - }).catch((error) => { - const { response } = error; - resolve({ error }); - })); + return (dispatch) => + new Promise((resolve) => + submitSettings(formValues) + .then((response) => { + dispatch(updateSettingsSuccess(response.data)); + dispatch(showToast(5500)); + dispatch(setToastText('Toast.SettingsSaved')); + resolve(); + }) + .catch((error) => { + const { response } = error; + resolve({ error }); + }) + ); } export function createApiKeySuccess(user) { @@ -269,8 +297,9 @@ export function createApiKeySuccess(user) { } export function createApiKey(label) { - return dispatch => - apiClient.post('/account/api-keys', { label }) + return (dispatch) => + apiClient + .post('/account/api-keys', { label }) .then((response) => { dispatch(createApiKeySuccess(response.data)); }) @@ -281,8 +310,9 @@ export function createApiKey(label) { } export function removeApiKey(keyId) { - return dispatch => - apiClient.delete(`/account/api-keys/${keyId}`) + return (dispatch) => + apiClient + .delete(`/account/api-keys/${keyId}`) .then((response) => { dispatch({ type: ActionTypes.API_KEY_REMOVED, @@ -298,13 +328,15 @@ export function removeApiKey(keyId) { export function unlinkService(service) { return (dispatch) => { if (!['github', 'google'].includes(service)) return; - apiClient.delete(`/auth/${service}`) + apiClient + .delete(`/auth/${service}`) .then((response) => { dispatch({ type: ActionTypes.AUTH_USER, user: response.data }); - }).catch((error) => { + }) + .catch((error) => { const { response } = error; const message = response.message || response.data.error; dispatch(authError(message)); diff --git a/client/modules/User/components/APIKeyForm.jsx b/client/modules/User/components/APIKeyForm.jsx index f586b41b25..2be6bf47bf 100644 --- a/client/modules/User/components/APIKeyForm.jsx +++ b/client/modules/User/components/APIKeyForm.jsx @@ -29,7 +29,7 @@ class APIKeyForm extends React.Component { const { keyLabel } = this.state; this.setState({ - keyLabel: '', + keyLabel: '' }); this.props.createApiKey(keyLabel); @@ -38,7 +38,9 @@ class APIKeyForm extends React.Component { } removeKey(key) { - const message = this.props.t('APIKeyForm.ConfirmDelete', { key_label: key.label }); + const message = this.props.t('APIKeyForm.ConfirmDelete', { + key_label: key.label + }); if (window.confirm(message)) { this.props.removeApiKey(key.id); @@ -50,14 +52,18 @@ class APIKeyForm extends React.Component { if (hasApiKeys) { return ( - <APIKeyList apiKeys={this.props.apiKeys} onRemove={this.removeKey} t={this.props.t} /> + <APIKeyList + apiKeys={this.props.apiKeys} + onRemove={this.removeKey} + t={this.props.t} + /> ); } return <p>{this.props.t('APIKeyForm.NoTokens')}</p>; } render() { - const keyWithToken = this.props.apiKeys.find(k => !!k.token); + const keyWithToken = this.props.apiKeys.find((k) => !!k.token); return ( <div className="api-key-form"> @@ -66,13 +72,22 @@ class APIKeyForm extends React.Component { </p> <div className="api-key-form__section"> - <h3 className="api-key-form__title">{this.props.t('APIKeyForm.CreateToken')}</h3> + <h3 className="api-key-form__title"> + {this.props.t('APIKeyForm.CreateToken')} + </h3> <form className="form form--inline" onSubmit={this.addKey}> - <label htmlFor="keyLabel" className="form__label form__label--hidden ">{this.props.t('APIKeyForm.TokenLabel')}</label> + <label + htmlFor="keyLabel" + className="form__label form__label--hidden " + > + {this.props.t('APIKeyForm.TokenLabel')} + </label> <input className="form__input" id="keyLabel" - onChange={(event) => { this.setState({ keyLabel: event.target.value }); }} + onChange={(event) => { + this.setState({ keyLabel: event.target.value }); + }} placeholder={this.props.t('APIKeyForm.TokenPlaceholder')} type="text" value={this.state.keyLabel} @@ -87,21 +102,26 @@ class APIKeyForm extends React.Component { </Button> </form> - { - keyWithToken && ( - <div className="api-key-form__new-token"> - <h4 className="api-key-form__new-token__title">{this.props.t('APIKeyForm.NewTokenTitle')}</h4> - <p className="api-key-form__new-token__info"> - {this.props.t('APIKeyForm.NewTokenInfo')} - </p> - <CopyableInput label={keyWithToken.label} value={keyWithToken.token} /> - </div> - ) - } + {keyWithToken && ( + <div className="api-key-form__new-token"> + <h4 className="api-key-form__new-token__title"> + {this.props.t('APIKeyForm.NewTokenTitle')} + </h4> + <p className="api-key-form__new-token__info"> + {this.props.t('APIKeyForm.NewTokenInfo')} + </p> + <CopyableInput + label={keyWithToken.label} + value={keyWithToken.token} + /> + </div> + )} </div> <div className="api-key-form__section"> - <h3 className="api-key-form__title">{this.props.t('APIKeyForm.ExistingTokensTitle')}</h3> + <h3 className="api-key-form__title"> + {this.props.t('APIKeyForm.ExistingTokensTitle')} + </h3> {this.renderApiKeys()} </div> </div> diff --git a/client/modules/User/components/APIKeyList.jsx b/client/modules/User/components/APIKeyList.jsx index 58997ec994..4b8b6329af 100644 --- a/client/modules/User/components/APIKeyList.jsx +++ b/client/modules/User/components/APIKeyList.jsx @@ -20,9 +20,9 @@ function APIKeyList({ apiKeys, onRemove, t }) { </thead> <tbody> {orderBy(apiKeys, ['createdAt'], ['desc']).map((key) => { - const lastUsed = key.lastUsedAt ? - dates.distanceInWordsToNow(new Date(key.lastUsedAt)) : - t('APIKeyList.Never'); + const lastUsed = key.lastUsedAt + ? dates.distanceInWordsToNow(new Date(key.lastUsedAt)) + : t('APIKeyList.Never'); return ( <tr key={key.id}> diff --git a/client/modules/User/components/AccountForm.jsx b/client/modules/User/components/AccountForm.jsx index 4ff2ef4b3f..02578a9ee3 100644 --- a/client/modules/User/components/AccountForm.jsx +++ b/client/modules/User/components/AccountForm.jsx @@ -8,11 +8,13 @@ import { updateSettings, initiateVerification } from '../actions'; import apiClient from '../../../utils/apiClient'; function asyncValidate(fieldToValidate, value) { - if (!value || value.trim().length === 0) return `Please enter a ${fieldToValidate}.`; + if (!value || value.trim().length === 0) + return `Please enter a ${fieldToValidate}.`; const queryParams = {}; queryParams[fieldToValidate] = value; queryParams.check_type = fieldToValidate; - return apiClient.get('/signup/duplicate_check', { params: queryParams }) + return apiClient + .get('/signup/duplicate_check', { params: queryParams }) .then((response) => { if (response.data.exists) { return response.data.message; @@ -23,7 +25,7 @@ function asyncValidate(fieldToValidate, value) { function AccountForm() { const { t } = useTranslation(); - const user = useSelector(state => state.user); + const user = useSelector((state) => state.user); const dispatch = useDispatch(); const handleInitiateVerification = (evt) => { @@ -51,19 +53,24 @@ function AccountForm() { validate={validateSettings} onSubmit={onSubmit} > - {({ - handleSubmit, submitting, invalid, restart - }) => ( + {({ handleSubmit, submitting, invalid, restart }) => ( <form className="form" onSubmit={(event) => { handleSubmit(event).then(restart); }} > - <Field name="email" validate={validateEmail} validateFields={[]} initialValue={user.email}> - {field => ( + <Field + name="email" + validate={validateEmail} + validateFields={[]} + initialValue={user.email} + > + {(field) => ( <p className="form__field"> - <label htmlFor="email" className="form__label">{t('AccountForm.Email')}</label> + <label htmlFor="email" className="form__label"> + {t('AccountForm.Email')} + </label> <input className="form__input" aria-label={t('AccountForm.EmailARIA')} @@ -77,29 +84,34 @@ function AccountForm() { </p> )} </Field> - { - user.verified !== 'verified' && - ( - <p className="form__context"> - <span className="form__status">{t('AccountForm.Unconfirmed')}</span> - { - user.emailVerificationInitiate === true ? - ( - <span className="form__status"> {t('AccountForm.EmailSent')}</span> - ) : - ( - <Button onClick={handleInitiateVerification}> - {t('AccountForm.Resend')} - </Button> - ) - } - </p> - ) - } - <Field name="username" validate={validateUsername} validateFields={[]} initialValue={user.username}> - {field => ( + {user.verified !== 'verified' && ( + <p className="form__context"> + <span className="form__status"> + {t('AccountForm.Unconfirmed')} + </span> + {user.emailVerificationInitiate === true ? ( + <span className="form__status"> + {' '} + {t('AccountForm.EmailSent')} + </span> + ) : ( + <Button onClick={handleInitiateVerification}> + {t('AccountForm.Resend')} + </Button> + )} + </p> + )} + <Field + name="username" + validate={validateUsername} + validateFields={[]} + initialValue={user.username} + > + {(field) => ( <p className="form__field"> - <label htmlFor="username" className="form__label">{t('AccountForm.UserName')}</label> + <label htmlFor="username" className="form__label"> + {t('AccountForm.UserName')} + </label> <input className="form__input" aria-label={t('AccountForm.UserNameARIA')} @@ -114,9 +126,11 @@ function AccountForm() { )} </Field> <Field name="currentPassword"> - {field => ( + {(field) => ( <p className="form__field"> - <label htmlFor="current password" className="form__label">{t('AccountForm.CurrentPassword')}</label> + <label htmlFor="current password" className="form__label"> + {t('AccountForm.CurrentPassword')} + </label> <input className="form__input" aria-label={t('AccountForm.CurrentPasswordARIA')} @@ -131,9 +145,11 @@ function AccountForm() { )} </Field> <Field name="newPassword"> - {field => ( + {(field) => ( <p className="form__field"> - <label htmlFor="new password" className="form__label">{t('AccountForm.NewPassword')}</label> + <label htmlFor="new password" className="form__label"> + {t('AccountForm.NewPassword')} + </label> <input className="form__input" aria-label={t('AccountForm.NewPasswordARIA')} @@ -147,10 +163,8 @@ function AccountForm() { </p> )} </Field> - <Button - type="submit" - disabled={submitting || invalid} - >{t('AccountForm.SubmitSaveAllSettings')} + <Button type="submit" disabled={submitting || invalid}> + {t('AccountForm.SubmitSaveAllSettings')} </Button> </form> )} diff --git a/client/modules/User/components/Collection.jsx b/client/modules/User/components/Collection.jsx index 2d7dd47c04..9aee42609e 100644 --- a/client/modules/User/components/Collection.jsx +++ b/client/modules/User/components/Collection.jsx @@ -59,11 +59,11 @@ const ShareURL = ({ value, t }) => { > {t('Collection.Share')} </Button> - { showURL && + {showURL && ( <div className="collection__share-dropdown"> <CopyableInput value={value} label={t('Collection.URLLink')} /> </div> - } + )} </div> ); }; @@ -74,37 +74,47 @@ ShareURL.propTypes = { }; const CollectionItemRowBase = ({ - collection, item, isOwner, removeFromCollection, t + collection, + item, + isOwner, + removeFromCollection, + t }) => { const projectIsDeleted = item.isDeleted; const handleSketchRemove = () => { const name = projectIsDeleted ? 'deleted sketch' : item.project.name; - if (window.confirm(t('Collection.DeleteFromCollection', { name_sketch: name }))) { + if ( + window.confirm( + t('Collection.DeleteFromCollection', { name_sketch: name }) + ) + ) { removeFromCollection(collection.id, item.projectId); } }; - const name = projectIsDeleted ? <span>{t('Collection.SketchDeleted')}</span> : ( + const name = projectIsDeleted ? ( + <span>{t('Collection.SketchDeleted')}</span> + ) : ( <Link to={`/${item.project.user.username}/sketches/${item.projectId}`}> {item.project.name} </Link> ); - const sketchOwnerUsername = projectIsDeleted ? null : item.project.user.username; + const sketchOwnerUsername = projectIsDeleted + ? null + : item.project.user.username; return ( <tr className={`sketches-table__row ${projectIsDeleted ? 'is-deleted' : ''}`} > - <th scope="row"> - {name} - </th> + <th scope="row">{name}</th> <td>{dates.format(item.createdAt)}</td> <td>{sketchOwnerUsername}</td> <td className="collection-row__action-column "> - {isOwner && + {isOwner && ( <button className="collection-row__remove-button" onClick={handleSketchRemove} @@ -112,12 +122,12 @@ const CollectionItemRowBase = ({ > <RemoveIcon focusable="false" aria-hidden="true" /> </button> - } + )} </td> - </tr>); + </tr> + ); }; - CollectionItemRowBase.propTypes = { collection: PropTypes.shape({ id: PropTypes.string.isRequired, @@ -132,8 +142,8 @@ CollectionItemRowBase.propTypes = { name: PropTypes.string.isRequired, user: PropTypes.shape({ username: PropTypes.string.isRequired - }), - }).isRequired, + }) + }).isRequired }).isRequired, isOwner: PropTypes.bool.isRequired, user: PropTypes.shape({ @@ -145,10 +155,16 @@ CollectionItemRowBase.propTypes = { }; function mapDispatchToPropsSketchListRow(dispatch) { - return bindActionCreators(Object.assign({}, CollectionsActions, ProjectActions, IdeActions), dispatch); + return bindActionCreators( + Object.assign({}, CollectionsActions, ProjectActions, IdeActions), + dispatch + ); } -const CollectionItemRow = connect(null, mapDispatchToPropsSketchListRow)(CollectionItemRowBase); +const CollectionItemRow = connect( + null, + mapDispatchToPropsSketchListRow +)(CollectionItemRowBase); class Collection extends React.Component { constructor(props) { @@ -160,7 +176,7 @@ class Collection extends React.Component { this.hideAddSketches = this.hideAddSketches.bind(this); this.state = { - isAddingSketches: false, + isAddingSketches: false }; } @@ -168,11 +184,15 @@ class Collection extends React.Component { if (this.props.username === this.props.user.username) { return this.props.t('Collection.Title'); } - return this.props.t('Collection.AnothersTitle', { anotheruser: this.props.username }); + return this.props.t('Collection.AnothersTitle', { + anotheruser: this.props.username + }); } getUsername() { - return this.props.username !== undefined ? this.props.username : this.props.user.username; + return this.props.username !== undefined + ? this.props.username + : this.props.user.username; } getCollectionName() { @@ -182,9 +202,11 @@ class Collection extends React.Component { isOwner() { let isOwner = false; - if (this.props.user != null && + if ( + this.props.user != null && this.props.user.username && - this.props.collection.owner.username === this.props.user.username) { + this.props.collection.owner.username === this.props.user.username + ) { isOwner = true; } @@ -205,9 +227,7 @@ class Collection extends React.Component { } _renderCollectionMetadata() { - const { - id, name, description, items, owner - } = this.props.collection; + const { id, name, description, items, owner } = this.props.collection; const hostname = window.location.origin; const { username } = this.props; @@ -242,47 +262,61 @@ class Collection extends React.Component { // }; return ( - <header className={`collection-metadata ${this.isOwner() ? 'collection-metadata--is-owner' : ''}`}> + <header + className={`collection-metadata ${ + this.isOwner() ? 'collection-metadata--is-owner' : '' + }`} + > <div className="collection-metadata__columns"> <div className="collection-metadata__column--left"> <h2 className="collection-metadata__name"> - { - this.isOwner() ? - <EditableInput value={name} onChange={handleEditCollectionName} validate={value => value !== ''} /> : - name - } + {this.isOwner() ? ( + <EditableInput + value={name} + onChange={handleEditCollectionName} + validate={(value) => value !== ''} + /> + ) : ( + name + )} </h2> <p className="collection-metadata__description"> - { - this.isOwner() ? - <EditableInput - InputComponent="textarea" - value={description} - onChange={handleEditCollectionDescription} - emptyPlaceholder={this.props.t('Collection.DescriptionPlaceholder')} - /> : - description - } + {this.isOwner() ? ( + <EditableInput + InputComponent="textarea" + value={description} + onChange={handleEditCollectionDescription} + emptyPlaceholder={this.props.t( + 'Collection.DescriptionPlaceholder' + )} + /> + ) : ( + description + )} </p> - <p className="collection-metadata__user">{this.props.t('Collection.By')} - <Link to={`${hostname}/${username}/sketches`}>{owner.username}</Link> + <p className="collection-metadata__user"> + {this.props.t('Collection.By')} + <Link to={`${hostname}/${username}/sketches`}> + {owner.username} + </Link> </p> - <p className="collection-metadata__user">{this.props.t('Collection.NumSketches', { count: items.length }) }</p> + <p className="collection-metadata__user"> + {this.props.t('Collection.NumSketches', { count: items.length })} + </p> </div> <div className="collection-metadata__column--right"> <p className="collection-metadata__share"> <ShareURL value={`${baseURL}${id}`} t={this.props.t} /> </p> - { - this.isOwner() && + {this.isOwner() && ( <Button onClick={this.showAddSketches}> {this.props.t('Collection.AddSketch')} </Button> - } + )} </div> </div> </header> @@ -291,23 +325,27 @@ class Collection extends React.Component { showAddSketches() { this.setState({ - isAddingSketches: true, + isAddingSketches: true }); } hideAddSketches() { this.setState({ - isAddingSketches: false, + isAddingSketches: false }); } _renderEmptyTable() { const isLoading = this.props.loading; - const hasCollectionItems = this.props.collection != null && - this.props.collection.items.length > 0; + const hasCollectionItems = + this.props.collection != null && this.props.collection.items.length > 0; if (!isLoading && !hasCollectionItems) { - return (<p className="collection-empty-message">{this.props.t('Collection.NoSketches')}</p>); + return ( + <p className="collection-empty-message"> + {this.props.t('Collection.NoSketches')} + </p> + ); } return null; } @@ -317,22 +355,30 @@ class Collection extends React.Component { let buttonLabel; if (field !== fieldName) { if (field === 'name') { - buttonLabel = this.props.t('Collection.ButtonLabelAscendingARIA', { displayName }); + buttonLabel = this.props.t('Collection.ButtonLabelAscendingARIA', { + displayName + }); } else { - buttonLabel = this.props.t('Collection.ButtonLabelDescendingARIA', { displayName }); + buttonLabel = this.props.t('Collection.ButtonLabelDescendingARIA', { + displayName + }); } } else if (direction === SortingActions.DIRECTION.ASC) { - buttonLabel = this.props.t('Collection.ButtonLabelDescendingARIA', { displayName }); + buttonLabel = this.props.t('Collection.ButtonLabelDescendingARIA', { + displayName + }); } else { - buttonLabel = this.props.t('Collection.ButtonLabelAscendingARIA', { displayName }); + buttonLabel = this.props.t('Collection.ButtonLabelAscendingARIA', { + displayName + }); } return buttonLabel; - } + }; _renderFieldHeader(fieldName, displayName) { const { field, direction } = this.props.sorting; const headerClass = classNames({ - 'arrowDown': true, + "arrowDown": true, 'sketches-table__header--selected': field === fieldName }); const buttonLabel = this._getButtonLabel(fieldName, displayName); @@ -344,12 +390,22 @@ class Collection extends React.Component { aria-label={buttonLabel} > <span className={headerClass}>{displayName}</span> - {field === fieldName && direction === SortingActions.DIRECTION.ASC && - <ArrowUpIcon role="img" aria-label={this.props.t('Collection.DirectionAscendingARIA')} focusable="false" /> - } - {field === fieldName && direction === SortingActions.DIRECTION.DESC && - <ArrowDownIcon role="img" aria-label={this.props.t('Collection.DirectionDescendingARIA')} focusable="false" /> - } + {field === fieldName && + direction === SortingActions.DIRECTION.ASC && ( + <ArrowUpIcon + role="img" + aria-label={this.props.t('Collection.DirectionAscendingARIA')} + focusable="false" + /> + )} + {field === fieldName && + direction === SortingActions.DIRECTION.DESC && ( + <ArrowDownIcon + role="img" + aria-label={this.props.t('Collection.DirectionDescendingARIA')} + focusable="false" + /> + )} </button> </th> ); @@ -360,7 +416,10 @@ class Collection extends React.Component { const isOwner = this.isOwner(); return ( - <main className="collection-container" data-has-items={this.hasCollectionItems() ? 'true' : 'false'}> + <main + className="collection-container" + data-has-items={this.hasCollectionItems() ? 'true' : 'false'} + > <article className="collection"> <Helmet> <title>{this.getTitle()}</title> @@ -370,19 +429,31 @@ class Collection extends React.Component { <article className="collection-content"> <div className="collection-table-wrapper"> {this._renderEmptyTable()} - {this.hasCollectionItems() && - <table className="sketches-table" summary={this.props.t('Collection.TableSummary')}> + {this.hasCollectionItems() && ( + <table + className="sketches-table" + summary={this.props.t('Collection.TableSummary')} + > <thead> <tr> - {this._renderFieldHeader('name', this.props.t('Collection.HeaderName'))} - {this._renderFieldHeader('createdAt', this.props.t('Collection.HeaderCreatedAt'))} - {this._renderFieldHeader('user', this.props.t('Collection.HeaderUser'))} + {this._renderFieldHeader( + 'name', + this.props.t('Collection.HeaderName') + )} + {this._renderFieldHeader( + 'createdAt', + this.props.t('Collection.HeaderCreatedAt') + )} + {this._renderFieldHeader( + 'user', + this.props.t('Collection.HeaderUser') + )} <th scope="col"></th> </tr> </thead> <tbody> - {this.props.collection.items.map(item => - (<CollectionItemRow + {this.props.collection.items.map((item) => ( + <CollectionItemRow key={item.id} item={item} user={this.props.user} @@ -390,25 +461,24 @@ class Collection extends React.Component { collection={this.props.collection} isOwner={isOwner} t={this.props.t} - />))} + /> + ))} </tbody> </table> - } - { - this.state.isAddingSketches && ( - <Overlay - title={this.props.t('Collection.AddSketch')} - actions={<SketchSearchbar />} - closeOverlay={this.hideAddSketches} - isFixedHeight - > - <AddToCollectionSketchList - username={this.props.username} - collection={this.props.collection} - /> - </Overlay> - ) - } + )} + {this.state.isAddingSketches && ( + <Overlay + title={this.props.t('Collection.AddSketch')} + actions={<SketchSearchbar />} + closeOverlay={this.hideAddSketches} + isFixedHeight + > + <AddToCollectionSketchList + username={this.props.username} + collection={this.props.collection} + /> + </Overlay> + )} </div> </article> </article> @@ -429,9 +499,9 @@ Collection.propTypes = { slug: PropTypes.string, description: PropTypes.string, owner: PropTypes.shape({ - username: PropTypes.string, + username: PropTypes.string }).isRequired, - items: PropTypes.arrayOf(PropTypes.shape({})), + items: PropTypes.arrayOf(PropTypes.shape({})) }), username: PropTypes.string, loading: PropTypes.bool.isRequired, @@ -468,9 +538,17 @@ function mapStateToProps(state, ownProps) { function mapDispatchToProps(dispatch) { return bindActionCreators( - Object.assign({}, CollectionsActions, ProjectsActions, ToastActions, SortingActions), + Object.assign( + {}, + CollectionsActions, + ProjectsActions, + ToastActions, + SortingActions + ), dispatch ); } -export default withTranslation()(connect(mapStateToProps, mapDispatchToProps)(Collection)); +export default withTranslation()( + connect(mapStateToProps, mapDispatchToProps)(Collection) +); diff --git a/client/modules/User/components/CollectionCreate.jsx b/client/modules/User/components/CollectionCreate.jsx index 9a7b6315c8..351a36747a 100644 --- a/client/modules/User/components/CollectionCreate.jsx +++ b/client/modules/User/components/CollectionCreate.jsx @@ -28,20 +28,20 @@ class CollectionCreate extends React.Component { return this.props.t('CollectionCreate.Title'); } - handleTextChange = field => (evt) => { + handleTextChange = (field) => (evt) => { this.setState({ collection: { ...this.state.collection, - [field]: evt.target.value, + [field]: evt.target.value } }); - } + }; handleCreateCollection = (event) => { event.preventDefault(); this.props.createCollection(this.state.collection); - } + }; render() { const { generatedCollectionName, creationError } = this.state; @@ -56,9 +56,15 @@ class CollectionCreate extends React.Component { </Helmet> <div className="sketches-table-container"> <form className="form" onSubmit={this.handleCreateCollection}> - {creationError && <span className="form-error">{this.props.t('CollectionCreate.FormError')}</span>} + {creationError && ( + <span className="form-error"> + {this.props.t('CollectionCreate.FormError')} + </span> + )} <p className="form__field"> - <label htmlFor="name" className="form__label">{this.props.t('CollectionCreate.FormLabel')}</label> + <label htmlFor="name" className="form__label"> + {this.props.t('CollectionCreate.FormLabel')} + </label> <input className="form__input" aria-label={this.props.t('CollectionCreate.FormLabelARIA')} @@ -68,10 +74,16 @@ class CollectionCreate extends React.Component { placeholder={generatedCollectionName} onChange={this.handleTextChange('name')} /> - {invalid && <span className="form-error">{this.props.t('CollectionCreate.NameRequired')}</span>} + {invalid && ( + <span className="form-error"> + {this.props.t('CollectionCreate.NameRequired')} + </span> + )} </p> <p className="form__field"> - <label htmlFor="description" className="form__label">{this.props.t('CollectionCreate.Description')}</label> + <label htmlFor="description" className="form__label"> + {this.props.t('CollectionCreate.Description')} + </label> <textarea className="form__input form__input-flexible-height" aria-label={this.props.t('CollectionCreate.DescriptionARIA')} @@ -79,11 +91,15 @@ class CollectionCreate extends React.Component { id="description" value={description} onChange={this.handleTextChange('description')} - placeholder={this.props.t('CollectionCreate.DescriptionPlaceholder')} + placeholder={this.props.t( + 'CollectionCreate.DescriptionPlaceholder' + )} rows="4" /> </p> - <Button type="submit" disabled={invalid}>{this.props.t('CollectionCreate.SubmitCollectionCreate')}</Button> + <Button type="submit" disabled={invalid}> + {this.props.t('CollectionCreate.SubmitCollectionCreate')} + </Button> </form> </div> </div> @@ -102,7 +118,7 @@ CollectionCreate.propTypes = { function mapStateToProps(state, ownProps) { return { - user: state.user, + user: state.user }; } @@ -110,4 +126,6 @@ function mapDispatchToProps(dispatch) { return bindActionCreators(Object.assign({}, CollectionsActions), dispatch); } -export default withTranslation()(connect(mapStateToProps, mapDispatchToProps)(CollectionCreate)); +export default withTranslation()( + connect(mapStateToProps, mapDispatchToProps)(CollectionCreate) +); diff --git a/client/modules/User/components/DashboardTabSwitcher.jsx b/client/modules/User/components/DashboardTabSwitcher.jsx index 51b0c5e730..34c4380dba 100644 --- a/client/modules/User/components/DashboardTabSwitcher.jsx +++ b/client/modules/User/components/DashboardTabSwitcher.jsx @@ -6,19 +6,21 @@ import { Link } from 'react-router'; const TabKey = { assets: 'assets', collections: 'collections', - sketches: 'sketches', + sketches: 'sketches' }; const Tab = ({ children, isSelected, to }) => { const selectedClassName = 'dashboard-header__tab--selected'; const location = { pathname: to, state: { skipSavingPath: true } }; - const content = isSelected ? <span>{children}</span> : <Link to={location}>{children}</Link>; + const content = isSelected ? ( + <span>{children}</span> + ) : ( + <Link to={location}>{children}</Link> + ); return ( <li className={`dashboard-header__tab ${isSelected && selectedClassName}`}> - <h4 className="dashboard-header__tab__title"> - {content} - </h4> + <h4 className="dashboard-header__tab__title">{content}</h4> </li> ); }; @@ -26,17 +28,32 @@ const Tab = ({ children, isSelected, to }) => { Tab.propTypes = { children: PropTypes.string.isRequired, isSelected: PropTypes.bool.isRequired, - to: PropTypes.string.isRequired, + to: PropTypes.string.isRequired }; -const DashboardTabSwitcher = ({ - currentTab, isOwner, username, t -}) => ( +const DashboardTabSwitcher = ({ currentTab, isOwner, username, t }) => ( <ul className="dashboard-header__switcher"> <div className="dashboard-header__tabs"> - <Tab to={`/${username}/sketches`} isSelected={currentTab === TabKey.sketches}>{t('DashboardTabSwitcher.Sketches')}</Tab> - <Tab to={`/${username}/collections`} isSelected={currentTab === TabKey.collections}>{t('DashboardTabSwitcher.Collections')}</Tab> - {isOwner && <Tab to={`/${username}/assets`} isSelected={currentTab === TabKey.assets}>{t('DashboardTabSwitcher.Assets')}</Tab>} + <Tab + to={`/${username}/sketches`} + isSelected={currentTab === TabKey.sketches} + > + {t('DashboardTabSwitcher.Sketches')} + </Tab> + <Tab + to={`/${username}/collections`} + isSelected={currentTab === TabKey.collections} + > + {t('DashboardTabSwitcher.Collections')} + </Tab> + {isOwner && ( + <Tab + to={`/${username}/assets`} + isSelected={currentTab === TabKey.assets} + > + {t('DashboardTabSwitcher.Assets')} + </Tab> + )} </div> </ul> ); @@ -48,6 +65,5 @@ DashboardTabSwitcher.propTypes = { t: PropTypes.func.isRequired }; - const DashboardTabSwitcherPublic = withTranslation()(DashboardTabSwitcher); export { DashboardTabSwitcherPublic as default, TabKey }; diff --git a/client/modules/User/components/LoginForm.jsx b/client/modules/User/components/LoginForm.jsx index 9c2ec6b949..01a593ff6c 100644 --- a/client/modules/User/components/LoginForm.jsx +++ b/client/modules/User/components/LoginForm.jsx @@ -19,17 +19,14 @@ function LoginForm(props) { validate={validateLogin} onSubmit={onSubmit} > - {({ - handleSubmit, pristine, submitting, invalid - }) => ( - <form - className="form" - onSubmit={handleSubmit} - > + {({ handleSubmit, pristine, submitting, invalid }) => ( + <form className="form" onSubmit={handleSubmit}> <Field name="email"> - {field => ( + {(field) => ( <p className="form__field"> - <label htmlFor="email" className="form__label">{props.t('LoginForm.UsernameOrEmail')}</label> + <label htmlFor="email" className="form__label"> + {props.t('LoginForm.UsernameOrEmail')} + </label> <input className="form__input" aria-label={props.t('LoginForm.UsernameOrEmailARIA')} @@ -44,9 +41,11 @@ function LoginForm(props) { )} </Field> <Field name="password"> - {field => ( + {(field) => ( <p className="form__field"> - <label htmlFor="password" className="form__label">{props.t('LoginForm.Password')}</label> + <label htmlFor="password" className="form__label"> + {props.t('LoginForm.Password')} + </label> <input className="form__input" aria-label={props.t('LoginForm.PasswordARIA')} @@ -60,10 +59,8 @@ function LoginForm(props) { </p> )} </Field> - <Button - type="submit" - disabled={submitting || invalid || pristine} - >{props.t('LoginForm.Submit')} + <Button type="submit" disabled={submitting || invalid || pristine}> + {props.t('LoginForm.Submit')} </Button> </form> )} diff --git a/client/modules/User/components/NewPasswordForm.jsx b/client/modules/User/components/NewPasswordForm.jsx index dfd1661676..26af961fc5 100644 --- a/client/modules/User/components/NewPasswordForm.jsx +++ b/client/modules/User/components/NewPasswordForm.jsx @@ -22,17 +22,14 @@ function NewPasswordForm(props) { validate={validateNewPassword} onSubmit={onSubmit} > - {({ - handleSubmit, submitting, invalid, pristine - }) => ( - <form - className="form" - onSubmit={handleSubmit} - > + {({ handleSubmit, submitting, invalid, pristine }) => ( + <form className="form" onSubmit={handleSubmit}> <Field name="password"> - {field => ( + {(field) => ( <p className="form__field"> - <label htmlFor="password" className="form__label">{t('NewPasswordForm.Title')}</label> + <label htmlFor="password" className="form__label"> + {t('NewPasswordForm.Title')} + </label> <input className="form__input" aria-label={t('NewPasswordForm.TitleARIA')} @@ -47,9 +44,11 @@ function NewPasswordForm(props) { )} </Field> <Field name="confirmPassword"> - {field => ( + {(field) => ( <p className="form__field"> - <label htmlFor="confirm password" className="form__label">{t('NewPasswordForm.ConfirmPassword')}</label> + <label htmlFor="confirm password" className="form__label"> + {t('NewPasswordForm.ConfirmPassword')} + </label> <input className="form__input" type="password" @@ -63,7 +62,9 @@ function NewPasswordForm(props) { </p> )} </Field> - <Button type="submit" disabled={submitting || invalid || pristine}>{t('NewPasswordForm.SubmitSetNewPassword')}</Button> + <Button type="submit" disabled={submitting || invalid || pristine}> + {t('NewPasswordForm.SubmitSetNewPassword')} + </Button> </form> )} </Form> @@ -71,7 +72,7 @@ function NewPasswordForm(props) { } NewPasswordForm.propTypes = { - resetPasswordToken: PropTypes.string.isRequired, + resetPasswordToken: PropTypes.string.isRequired }; export default NewPasswordForm; diff --git a/client/modules/User/components/ResetPasswordForm.jsx b/client/modules/User/components/ResetPasswordForm.jsx index c8f008e7bd..f44e7649bd 100644 --- a/client/modules/User/components/ResetPasswordForm.jsx +++ b/client/modules/User/components/ResetPasswordForm.jsx @@ -8,7 +8,9 @@ import Button from '../../../common/Button'; function ResetPasswordForm(props) { const { t } = useTranslation(); - const resetPasswordInitiate = useSelector(state => state.user.resetPasswordInitiate); + const resetPasswordInitiate = useSelector( + (state) => state.user.resetPasswordInitiate + ); const dispatch = useDispatch(); function onSubmit(formProps) { @@ -21,17 +23,14 @@ function ResetPasswordForm(props) { validate={validateResetPassword} onSubmit={onSubmit} > - {({ - handleSubmit, submitting, pristine, invalid - }) => ( - <form - className="form" - onSubmit={handleSubmit} - > + {({ handleSubmit, submitting, pristine, invalid }) => ( + <form className="form" onSubmit={handleSubmit}> <Field name="email"> - {field => ( + {(field) => ( <p className="form__field"> - <label htmlFor="email" className="form__label">{t('ResetPasswordForm.Email')}</label> + <label htmlFor="email" className="form__label"> + {t('ResetPasswordForm.Email')} + </label> <input className="form__input" aria-label={t('ResetPasswordForm.EmailARIA')} @@ -47,8 +46,11 @@ function ResetPasswordForm(props) { </Field> <Button type="submit" - disabled={submitting || invalid || pristine || resetPasswordInitiate} - >{t('ResetPasswordForm.Submit')} + disabled={ + submitting || invalid || pristine || resetPasswordInitiate + } + > + {t('ResetPasswordForm.Submit')} </Button> </form> )} diff --git a/client/modules/User/components/ResponsiveForm.jsx b/client/modules/User/components/ResponsiveForm.jsx index 45717269bb..fc91dbc606 100644 --- a/client/modules/User/components/ResponsiveForm.jsx +++ b/client/modules/User/components/ResponsiveForm.jsx @@ -3,29 +3,38 @@ import styled from 'styled-components'; import PropTypes from 'prop-types'; import { remSize } from '../../../theme'; - const ResponsiveForm = styled.div` .form-container__content { width: unset !important; padding-top: ${remSize(16)}; padding-bottom: ${remSize(64)}; } - + .form__input { min-width: unset; padding: 0px ${remSize(12)}; height: ${remSize(28)}; } - .form-container__title { margin-bottom: ${remSize(14)}} - p.form__field { margin-top: 0px !important; } - label.form__label { margin-top: ${remSize(8)} !important; } + .form-container__title { + margin-bottom: ${remSize(14)}; + } + p.form__field { + margin-top: 0px !important; + } + label.form__label { + margin-top: ${remSize(8)} !important; + } - .form-error { width: 100% } + .form-error { + width: 100%; + } - .nav__items-right:last-child { display: none } + .nav__items-right:last-child { + display: none; + } .form-container { - height: 100% + height: 100%; } .nav__dropdown { @@ -36,9 +45,11 @@ const ResponsiveForm = styled.div` .form-container__stack { svg { width: ${remSize(12)}; - height: ${remSize(12)} + height: ${remSize(12)}; + } + a { + padding: 0px; } - a { padding: 0px } } `; diff --git a/client/modules/User/components/SignupForm.jsx b/client/modules/User/components/SignupForm.jsx index 301d2503b0..00c82f11ba 100644 --- a/client/modules/User/components/SignupForm.jsx +++ b/client/modules/User/components/SignupForm.jsx @@ -9,11 +9,13 @@ import Button from '../../../common/Button'; import apiClient from '../../../utils/apiClient'; function asyncValidate(fieldToValidate, value) { - if (!value || value.trim().length === 0) return `Please enter a ${fieldToValidate}.`; + if (!value || value.trim().length === 0) + return `Please enter a ${fieldToValidate}.`; const queryParams = {}; queryParams[fieldToValidate] = value; queryParams.check_type = fieldToValidate; - return apiClient.get('/signup/duplicate_check', { params: queryParams }) + return apiClient + .get('/signup/duplicate_check', { params: queryParams }) .then((response) => { if (response.data.exists) { return response.data.message; @@ -42,17 +44,18 @@ function SignupForm(props) { validate={validateSignup} onSubmit={onSubmit} > - {({ - handleSubmit, pristine, submitting, invalid - }) => ( - <form - className="form" - onSubmit={handleSubmit} - > - <Field name="username" validate={validateUsername} validateFields={[]}> - {field => ( + {({ handleSubmit, pristine, submitting, invalid }) => ( + <form className="form" onSubmit={handleSubmit}> + <Field + name="username" + validate={validateUsername} + validateFields={[]} + > + {(field) => ( <p className="form__field"> - <label htmlFor="username" className="form__label">{props.t('SignupForm.Title')}</label> + <label htmlFor="username" className="form__label"> + {props.t('SignupForm.Title')} + </label> <input className="form__input" aria-label={props.t('SignupForm.TitleARIA')} @@ -67,9 +70,11 @@ function SignupForm(props) { )} </Field> <Field name="email" validate={validateEmail} validateFields={[]}> - {field => ( + {(field) => ( <p className="form__field"> - <label htmlFor="email" className="form__label">{props.t('SignupForm.Email')}</label> + <label htmlFor="email" className="form__label"> + {props.t('SignupForm.Email')} + </label> <input className="form__input" aria-label={props.t('SignupForm.EmailARIA')} @@ -84,9 +89,11 @@ function SignupForm(props) { )} </Field> <Field name="password"> - {field => ( + {(field) => ( <p className="form__field"> - <label htmlFor="password" className="form__label">{props.t('SignupForm.Password')}</label> + <label htmlFor="password" className="form__label"> + {props.t('SignupForm.Password')} + </label> <input className="form__input" aria-label={props.t('SignupForm.PasswordARIA')} @@ -101,9 +108,11 @@ function SignupForm(props) { )} </Field> <Field name="confirmPassword"> - {field => ( + {(field) => ( <p className="form__field"> - <label htmlFor="confirm password" className="form__label">{props.t('SignupForm.ConfirmPassword')}</label> + <label htmlFor="confirm password" className="form__label"> + {props.t('SignupForm.ConfirmPassword')} + </label> <input className="form__input" type="password" @@ -117,13 +126,10 @@ function SignupForm(props) { </p> )} </Field> - <Button - type="submit" - disabled={submitting || invalid || pristine} - >{props.t('SignupForm.SubmitSignup')} + <Button type="submit" disabled={submitting || invalid || pristine}> + {props.t('SignupForm.SubmitSignup')} </Button> </form> - )} </Form> ); diff --git a/client/modules/User/components/SocialAuthButton.jsx b/client/modules/User/components/SocialAuthButton.jsx index 94527b5ac7..2969d5f4b8 100644 --- a/client/modules/User/components/SocialAuthButton.jsx +++ b/client/modules/User/components/SocialAuthButton.jsx @@ -33,14 +33,16 @@ const StyledButton = styled(Button)` width: ${remSize(300)}; `; -function SocialAuthButton({ - service, linkStyle, isConnected, t -}) { +function SocialAuthButton({ service, linkStyle, isConnected, t }) { const ServiceIcon = icons[service]; const serviceLabel = servicesLabels[service]; const loginLabel = t('SocialAuthButton.Login', { serviceauth: serviceLabel }); - const connectLabel = t('SocialAuthButton.Connect', { serviceauth: serviceLabel }); - const unlinkLabel = t('SocialAuthButton.Unlink', { serviceauth: serviceLabel }); + const connectLabel = t('SocialAuthButton.Connect', { + serviceauth: serviceLabel + }); + const unlinkLabel = t('SocialAuthButton.Unlink', { + serviceauth: serviceLabel + }); const ariaLabel = t('SocialAuthButton.LogoARIA', { serviceauth: service }); const dispatch = useDispatch(); if (linkStyle) { @@ -48,7 +50,9 @@ function SocialAuthButton({ return ( <StyledButton iconBefore={<ServiceIcon aria-label={ariaLabel} />} - onClick={() => { dispatch(unlinkService(service)); }} + onClick={() => { + dispatch(unlinkService(service)); + }} > {unlinkLabel} </StyledButton> diff --git a/client/modules/User/components/SocialAuthButton.stories.jsx b/client/modules/User/components/SocialAuthButton.stories.jsx index 7433bd27e5..770dd70162 100644 --- a/client/modules/User/components/SocialAuthButton.stories.jsx +++ b/client/modules/User/components/SocialAuthButton.stories.jsx @@ -8,9 +8,13 @@ export default { }; export const Github = () => ( - <SocialAuthButton service={SocialAuthButton.services.github}>Log in with Github</SocialAuthButton> + <SocialAuthButton service={SocialAuthButton.services.github}> + Log in with Github + </SocialAuthButton> ); export const Google = () => ( - <SocialAuthButton service={SocialAuthButton.services.google}>Sign up with Google</SocialAuthButton> + <SocialAuthButton service={SocialAuthButton.services.google}> + Sign up with Google + </SocialAuthButton> ); diff --git a/client/modules/User/pages/AccountView.jsx b/client/modules/User/pages/AccountView.jsx index 54bedf3168..bb3ca46456 100644 --- a/client/modules/User/pages/AccountView.jsx +++ b/client/modules/User/pages/AccountView.jsx @@ -22,7 +22,9 @@ function SocialLoginPanel(props) { <React.Fragment> <AccountForm /> {/* eslint-disable-next-line react/prop-types */} - <h2 className="form-container__divider">{props.t('AccountView.SocialLogin')}</h2> + <h2 className="form-container__divider"> + {props.t('AccountView.SocialLogin')} + </h2> <p className="account__social-text"> {/* eslint-disable-next-line react/prop-types */} {props.t('AccountView.SocialLoginDescription')} @@ -70,7 +72,7 @@ class AccountView extends React.Component { <Nav layout="dashboard" /> - {showError && + {showError && ( <Overlay title={this.props.t('ErrorModal.LinkTitle')} ariaLabel={this.props.t('ErrorModal.LinkTitle')} @@ -78,23 +80,32 @@ class AccountView extends React.Component { browserHistory.push(this.props.location.pathname); }} > - <ErrorModal - type="oauthError" - service={errorType} - /> + <ErrorModal type="oauthError" service={errorType} /> </Overlay> - } + )} <main className="account-settings"> <header className="account-settings__header"> - <h1 className="account-settings__title">{this.props.t('AccountView.Settings')}</h1> + <h1 className="account-settings__title"> + {this.props.t('AccountView.Settings')} + </h1> </header> - {accessTokensUIEnabled && + {accessTokensUIEnabled && ( <Tabs className="account__tabs"> <TabList> <div className="tabs__titles"> - <Tab><h4 className="tabs__title">{this.props.t('AccountView.AccountTab')}</h4></Tab> - {accessTokensUIEnabled && <Tab><h4 className="tabs__title">{this.props.t('AccountView.AccessTokensTab')}</h4></Tab>} + <Tab> + <h4 className="tabs__title"> + {this.props.t('AccountView.AccountTab')} + </h4> + </Tab> + {accessTokensUIEnabled && ( + <Tab> + <h4 className="tabs__title"> + {this.props.t('AccountView.AccessTokensTab')} + </h4> + </Tab> + )} </div> </TabList> <TabPanel> @@ -104,8 +115,8 @@ class AccountView extends React.Component { <APIKeyForm {...this.props} /> </TabPanel> </Tabs> - } - { !accessTokensUIEnabled && <SocialLoginPanel {...this.props} /> } + )} + {!accessTokensUIEnabled && <SocialLoginPanel {...this.props} />} </main> </div> ); @@ -124,9 +135,13 @@ function mapStateToProps(state) { } function mapDispatchToProps(dispatch) { - return bindActionCreators({ - createApiKey, removeApiKey - }, dispatch); + return bindActionCreators( + { + createApiKey, + removeApiKey + }, + dispatch + ); } AccountView.propTypes = { @@ -138,8 +153,10 @@ AccountView.propTypes = { pathname: PropTypes.string.isRequired }).isRequired, toast: PropTypes.shape({ - isVisible: PropTypes.bool.isRequired, + isVisible: PropTypes.bool.isRequired }).isRequired }; -export default withTranslation()(withRouter(connect(mapStateToProps, mapDispatchToProps)(AccountView))); +export default withTranslation()( + withRouter(connect(mapStateToProps, mapDispatchToProps)(AccountView)) +); diff --git a/client/modules/User/pages/CollectionView.jsx b/client/modules/User/pages/CollectionView.jsx index 98189dc6e4..8ffad0d860 100644 --- a/client/modules/User/pages/CollectionView.jsx +++ b/client/modules/User/pages/CollectionView.jsx @@ -10,7 +10,7 @@ import Collection from '../components/Collection'; class CollectionView extends React.Component { static defaultProps = { - user: null, + user: null }; componentDidMount() { @@ -79,17 +79,19 @@ function mapDispatchToProps(dispatch) { CollectionView.propTypes = { location: PropTypes.shape({ - pathname: PropTypes.string.isRequired, + pathname: PropTypes.string.isRequired }).isRequired, params: PropTypes.shape({ collection_id: PropTypes.string.isRequired, - username: PropTypes.string.isRequired, + username: PropTypes.string.isRequired }).isRequired, theme: PropTypes.string.isRequired, user: PropTypes.shape({ - username: PropTypes.string, + username: PropTypes.string }), t: PropTypes.func.isRequired }; -export default withTranslation()(connect(mapStateToProps, mapDispatchToProps)(CollectionView)); +export default withTranslation()( + connect(mapStateToProps, mapDispatchToProps)(CollectionView) +); diff --git a/client/modules/User/pages/DashboardView.jsx b/client/modules/User/pages/DashboardView.jsx index f4e461203f..3c7cddc178 100644 --- a/client/modules/User/pages/DashboardView.jsx +++ b/client/modules/User/pages/DashboardView.jsx @@ -11,14 +11,19 @@ import AssetList from '../../IDE/components/AssetList'; import AssetSize from '../../IDE/components/AssetSize'; import CollectionList from '../../IDE/components/CollectionList'; import SketchList from '../../IDE/components/SketchList'; -import { CollectionSearchbar, SketchSearchbar } from '../../IDE/components/Searchbar'; +import { + CollectionSearchbar, + SketchSearchbar +} from '../../IDE/components/Searchbar'; import CollectionCreate from '../components/CollectionCreate'; -import DashboardTabSwitcherPublic, { TabKey } from '../components/DashboardTabSwitcher'; +import DashboardTabSwitcherPublic, { + TabKey +} from '../components/DashboardTabSwitcher'; class DashboardView extends React.Component { static defaultProps = { - user: null, + user: null }; constructor(props) { @@ -70,25 +75,30 @@ class DashboardView extends React.Component { returnToDashboard = () => { browserHistory.push(`/${this.ownerName()}/collections`); - } + }; renderActionButton(tabKey, username, t) { switch (tabKey) { case TabKey.assets: return this.isOwner() && <AssetSize />; case TabKey.collections: - return this.isOwner() && ( - <React.Fragment> - <Button to={`/${username}/collections/create`}> - {t('DashboardView.CreateCollection')} - </Button> - <CollectionSearchbar /> - </React.Fragment>); + return ( + this.isOwner() && ( + <React.Fragment> + <Button to={`/${username}/collections/create`}> + {t('DashboardView.CreateCollection')} + </Button> + <CollectionSearchbar /> + </React.Fragment> + ) + ); case TabKey.sketches: default: return ( <React.Fragment> - {this.isOwner() && <Button to="/">{t('DashboardView.NewSketch')}</Button>} + {this.isOwner() && ( + <Button to="/">{t('DashboardView.NewSketch')}</Button> + )} <SketchSearchbar /> </React.Fragment> ); @@ -119,14 +129,18 @@ class DashboardView extends React.Component { <main className="dashboard-header"> <div className="dashboard-header__header"> - <h2 className="dashboard-header__header__title">{this.ownerName()}</h2> + <h2 className="dashboard-header__header__title"> + {this.ownerName()} + </h2> <div className="dashboard-header__nav"> - <DashboardTabSwitcherPublic currentTab={currentTab} isOwner={isOwner} username={username} /> - {actions && - <div className="dashboard-header__actions"> - {actions} - </div> - } + <DashboardTabSwitcherPublic + currentTab={currentTab} + isOwner={isOwner} + username={username} + /> + {actions && ( + <div className="dashboard-header__actions">{actions}</div> + )} </div> </div> @@ -134,14 +148,14 @@ class DashboardView extends React.Component { {this.renderContent(currentTab, username)} </div> </main> - {this.isCollectionCreate() && + {this.isCollectionCreate() && ( <Overlay title={this.props.t('DashboardView.CreateCollectionOverlay')} closeOverlay={this.returnToDashboard} > <CollectionCreate /> </Overlay> - } + )} </div> ); } @@ -151,21 +165,21 @@ function mapStateToProps(state) { return { previousPath: state.ide.previousPath, user: state.user, - theme: state.preferences.theme, + theme: state.preferences.theme }; } DashboardView.propTypes = { location: PropTypes.shape({ - pathname: PropTypes.string.isRequired, + pathname: PropTypes.string.isRequired }).isRequired, params: PropTypes.shape({ - username: PropTypes.string.isRequired, + username: PropTypes.string.isRequired }).isRequired, previousPath: PropTypes.string.isRequired, theme: PropTypes.string.isRequired, user: PropTypes.shape({ - username: PropTypes.string, + username: PropTypes.string }), t: PropTypes.func.isRequired }; diff --git a/client/modules/User/pages/EmailVerificationView.jsx b/client/modules/User/pages/EmailVerificationView.jsx index f34f967c9d..6b96264758 100644 --- a/client/modules/User/pages/EmailVerificationView.jsx +++ b/client/modules/User/pages/EmailVerificationView.jsx @@ -9,11 +9,10 @@ import { Helmet } from 'react-helmet'; import { verifyEmailConfirmation } from '../actions'; import Nav from '../../../components/Nav'; - class EmailVerificationView extends React.Component { static defaultProps = { - emailVerificationTokenState: null, - } + emailVerificationTokenState: null + }; componentWillMount() { const verificationToken = this.verificationToken(); @@ -26,27 +25,17 @@ class EmailVerificationView extends React.Component { render() { let status = null; - const { - emailVerificationTokenState, - } = this.props; + const { emailVerificationTokenState } = this.props; if (this.verificationToken() == null) { - status = ( - <p>{this.props.t('EmailVerificationView.InvalidTokenNull')}</p> - ); + status = <p>{this.props.t('EmailVerificationView.InvalidTokenNull')}</p>; } else if (emailVerificationTokenState === 'checking') { - status = ( - <p>{this.props.t('EmailVerificationView.Checking')}</p> - ); + status = <p>{this.props.t('EmailVerificationView.Checking')}</p>; } else if (emailVerificationTokenState === 'verified') { - status = ( - <p>{this.props.t('EmailVerificationView.Verified')}</p> - ); + status = <p>{this.props.t('EmailVerificationView.Verified')}</p>; setTimeout(() => browserHistory.push('/'), 1000); } else if (emailVerificationTokenState === 'invalid') { - status = ( - <p>{this.props.t('EmailVerificationView.InvalidState')}</p> - ); + status = <p>{this.props.t('EmailVerificationView.InvalidState')}</p>; } return ( @@ -57,7 +46,9 @@ class EmailVerificationView extends React.Component { <title>{this.props.t('EmailVerificationView.Title')}</title> </Helmet> <div className="form-container__content"> - <h2 className="form-container__title">{this.props.t('EmailVerificationView.Verify')}</h2> + <h2 className="form-container__title"> + {this.props.t('EmailVerificationView.Verify')} + </h2> {status} </div> </div> @@ -68,23 +59,29 @@ class EmailVerificationView extends React.Component { function mapStateToProps(state) { return { - emailVerificationTokenState: state.user.emailVerificationTokenState, + emailVerificationTokenState: state.user.emailVerificationTokenState }; } function mapDispatchToProps(dispatch) { - return bindActionCreators({ - verifyEmailConfirmation, - }, dispatch); + return bindActionCreators( + { + verifyEmailConfirmation + }, + dispatch + ); } - EmailVerificationView.propTypes = { emailVerificationTokenState: PropTypes.oneOf([ - 'checking', 'verified', 'invalid' + 'checking', + 'verified', + 'invalid' ]), verifyEmailConfirmation: PropTypes.func.isRequired, t: PropTypes.func.isRequired }; -export default withTranslation()(connect(mapStateToProps, mapDispatchToProps)(EmailVerificationView)); +export default withTranslation()( + connect(mapStateToProps, mapDispatchToProps)(EmailVerificationView) +); diff --git a/client/modules/User/pages/LoginView.jsx b/client/modules/User/pages/LoginView.jsx index 606ef4d5e1..58449067bc 100644 --- a/client/modules/User/pages/LoginView.jsx +++ b/client/modules/User/pages/LoginView.jsx @@ -25,11 +25,16 @@ function LoginView() { </div> <p className="form__navigation-options"> {t('LoginView.DontHaveAccount')} - <Link className="form__signup-button" to="/signup">{t('LoginView.SignUp')}</Link> + <Link className="form__signup-button" to="/signup"> + {t('LoginView.SignUp')} + </Link> </p> <p className="form__navigation-options"> {t('LoginView.ForgotPassword')} - <Link className="form__reset-password-button" to="/reset-password"> {t('LoginView.ResetPassword')}</Link> + <Link className="form__reset-password-button" to="/reset-password"> + {' '} + {t('LoginView.ResetPassword')} + </Link> </p> </div> </main> diff --git a/client/modules/User/pages/NewPasswordView.jsx b/client/modules/User/pages/NewPasswordView.jsx index 3ef0ae8bca..ec8b2ca0e1 100644 --- a/client/modules/User/pages/NewPasswordView.jsx +++ b/client/modules/User/pages/NewPasswordView.jsx @@ -11,7 +11,9 @@ import Nav from '../../../components/Nav'; function NewPasswordView(props) { const { t } = useTranslation(); const resetPasswordToken = props.params.reset_password_token; - const resetPasswordInvalid = useSelector(state => state.user.resetPasswordInvalid); + const resetPasswordInvalid = useSelector( + (state) => state.user.resetPasswordInvalid + ); const dispatch = useDispatch(); useEffect(() => { @@ -22,7 +24,7 @@ function NewPasswordView(props) { 'new-password': true, 'new-password--invalid': resetPasswordInvalid, 'form-container': true, - 'user': true + "user": true }); return ( <div className="new-password-container"> @@ -32,7 +34,9 @@ function NewPasswordView(props) { <title>{t('NewPasswordView.Title')}</title> </Helmet> <div className="form-container__content"> - <h2 className="form-container__title">{t('NewPasswordView.Description')}</h2> + <h2 className="form-container__title"> + {t('NewPasswordView.Description')} + </h2> <NewPasswordForm resetPasswordToken={resetPasswordToken} /> <p className="new-password__invalid"> {t('NewPasswordView.TokenInvalidOrExpired')} @@ -45,7 +49,7 @@ function NewPasswordView(props) { NewPasswordView.propTypes = { params: PropTypes.shape({ - reset_password_token: PropTypes.string, + reset_password_token: PropTypes.string }).isRequired }; diff --git a/client/modules/User/pages/ResetPasswordView.jsx b/client/modules/User/pages/ResetPasswordView.jsx index ec3ac65fea..7c5942373b 100644 --- a/client/modules/User/pages/ResetPasswordView.jsx +++ b/client/modules/User/pages/ResetPasswordView.jsx @@ -9,12 +9,14 @@ import Nav from '../../../components/Nav'; function ResetPasswordView() { const { t } = useTranslation(); - const resetPasswordInitiate = useSelector(state => state.user.resetPasswordInitiate); + const resetPasswordInitiate = useSelector( + (state) => state.user.resetPasswordInitiate + ); const resetPasswordClass = classNames({ 'reset-password': true, 'reset-password--submitted': resetPasswordInitiate, 'form-container': true, - 'user': true + "user": true }); return ( <div className="reset-password-container"> @@ -24,15 +26,21 @@ function ResetPasswordView() { <title>{t('ResetPasswordView.Title')}</title> </Helmet> <div className="form-container__content"> - <h2 className="form-container__title">{t('ResetPasswordView.Reset')}</h2> + <h2 className="form-container__title"> + {t('ResetPasswordView.Reset')} + </h2> <ResetPasswordForm /> <p className="reset-password__submitted"> {t('ResetPasswordView.Submitted')} </p> <p className="form__navigation-options"> - <Link className="form__login-button" to="/login">{t('ResetPasswordView.Login')}</Link> + <Link className="form__login-button" to="/login"> + {t('ResetPasswordView.Login')} + </Link> {t('ResetPasswordView.LoginOr')} - <Link className="form__signup-button" to="/signup">{t('ResetPasswordView.SignUp')}</Link> + <Link className="form__signup-button" to="/signup"> + {t('ResetPasswordView.SignUp')} + </Link> </p> </div> </div> diff --git a/client/modules/User/pages/SignupView.jsx b/client/modules/User/pages/SignupView.jsx index e878cd5072..efd2438eed 100644 --- a/client/modules/User/pages/SignupView.jsx +++ b/client/modules/User/pages/SignupView.jsx @@ -6,7 +6,6 @@ import SignupForm from '../components/SignupForm'; import SocialAuthButton from '../components/SocialAuthButton'; import Nav from '../../../components/Nav'; - function SignupView() { const { t } = useTranslation(); return ( @@ -17,7 +16,9 @@ function SignupView() { <title>{t('SignupView.Title')}</title> </Helmet> <div className="form-container__content"> - <h2 className="form-container__title">{t('SignupView.Description')}</h2> + <h2 className="form-container__title"> + {t('SignupView.Description')} + </h2> <SignupForm /> <h2 className="form-container__divider">{t('SignupView.Or')}</h2> <div className="form-container__stack"> @@ -26,7 +27,9 @@ function SignupView() { </div> <p className="form__navigation-options"> {t('SignupView.AlreadyHave')} - <Link className="form__login-button" to="/login">{t('SignupView.Login')}</Link> + <Link className="form__login-button" to="/login"> + {t('SignupView.Login')} + </Link> </p> </div> </main> diff --git a/client/modules/User/reducers.js b/client/modules/User/reducers.js index 00454acdfe..87953d7810 100644 --- a/client/modules/User/reducers.js +++ b/client/modules/User/reducers.js @@ -24,11 +24,17 @@ const user = (state = { authenticated: false }, action) => { case ActionTypes.EMAIL_VERIFICATION_INITIATE: return Object.assign({}, state, { emailVerificationInitiate: true }); case ActionTypes.EMAIL_VERIFICATION_VERIFY: - return Object.assign({}, state, { emailVerificationTokenState: 'checking' }); + return Object.assign({}, state, { + emailVerificationTokenState: 'checking' + }); case ActionTypes.EMAIL_VERIFICATION_VERIFIED: - return Object.assign({}, state, { emailVerificationTokenState: 'verified' }); + return Object.assign({}, state, { + emailVerificationTokenState: 'verified' + }); case ActionTypes.EMAIL_VERIFICATION_INVALID: - return Object.assign({}, state, { emailVerificationTokenState: 'invalid' }); + return Object.assign({}, state, { + emailVerificationTokenState: 'invalid' + }); case ActionTypes.SETTINGS_UPDATED: return { ...state, ...action.user }; case ActionTypes.API_KEY_REMOVED: diff --git a/client/routes.jsx b/client/routes.jsx index 15f631e43a..321cd786c8 100644 --- a/client/routes.jsx +++ b/client/routes.jsx @@ -19,7 +19,11 @@ import createRedirectWithUsername from './components/createRedirectWithUsername' import MobileDashboardView from './modules/Mobile/MobileDashboardView'; import { getUser } from './modules/User/actions'; import { stopSketch } from './modules/IDE/actions/ide'; -import { userIsAuthenticated, userIsNotAuthenticated, userIsAuthorized } from './utils/auth'; +import { + userIsAuthenticated, + userIsNotAuthenticated, + userIsAuthorized +} from './utils/auth'; import { mobileFirst, responsiveForm } from './utils/responsive'; const checkAuth = (store) => { @@ -34,13 +38,35 @@ const onRouteChange = (store) => { store.dispatch(stopSketch()); }; -const routes = store => ( - <Route path="/" component={App} onChange={() => { onRouteChange(store); }}> - <IndexRoute onEnter={checkAuth(store)} component={mobileFirst(MobileIDEView, IDEView)} /> +const routes = (store) => ( + <Route + path="/" + component={App} + onChange={() => { + onRouteChange(store); + }} + > + <IndexRoute + onEnter={checkAuth(store)} + component={mobileFirst(MobileIDEView, IDEView)} + /> - <Route path="/login" component={userIsNotAuthenticated(mobileFirst(responsiveForm(LoginView), LoginView))} /> - <Route path="/signup" component={userIsNotAuthenticated(mobileFirst(responsiveForm(SignupView), SignupView))} /> - <Route path="/reset-password" component={userIsNotAuthenticated(ResetPasswordView)} /> + <Route + path="/login" + component={userIsNotAuthenticated( + mobileFirst(responsiveForm(LoginView), LoginView) + )} + /> + <Route + path="/signup" + component={userIsNotAuthenticated( + mobileFirst(responsiveForm(SignupView), SignupView) + )} + /> + <Route + path="/reset-password" + component={userIsNotAuthenticated(ResetPasswordView)} + /> <Route path="/verify" component={EmailVerificationView} /> <Route path="/reset-password/:reset_password_token" @@ -50,24 +76,49 @@ const routes = store => ( <Route path="/:username/full/:project_id" component={FullView} /> <Route path="/full/:project_id" component={FullView} /> - <Route path="/:username/assets" component={userIsAuthenticated(userIsAuthorized(mobileFirst(MobileDashboardView, DashboardView)))} /> - <Route path="/:username/sketches" component={mobileFirst(MobileDashboardView, DashboardView)} /> - <Route path="/:username/sketches/:project_id" component={mobileFirst(MobileIDEView, IDEView)} /> - <Route path="/:username/sketches/:project_id/add-to-collection" component={mobileFirst(MobileIDEView, IDEView)} /> - <Route path="/:username/collections" component={mobileFirst(MobileDashboardView, DashboardView)} /> + <Route + path="/:username/assets" + component={userIsAuthenticated( + userIsAuthorized(mobileFirst(MobileDashboardView, DashboardView)) + )} + /> + <Route + path="/:username/sketches" + component={mobileFirst(MobileDashboardView, DashboardView)} + /> + <Route + path="/:username/sketches/:project_id" + component={mobileFirst(MobileIDEView, IDEView)} + /> + <Route + path="/:username/sketches/:project_id/add-to-collection" + component={mobileFirst(MobileIDEView, IDEView)} + /> + <Route + path="/:username/collections" + component={mobileFirst(MobileDashboardView, DashboardView)} + /> <Route path="/:username/collections/create" component={DashboardView} /> - <Route path="/:username/collections/:collection_id" component={CollectionView} /> + <Route + path="/:username/collections/:collection_id" + component={CollectionView} + /> - <Route path="/sketches" component={createRedirectWithUsername('/:username/sketches')} /> - <Route path="/assets" component={createRedirectWithUsername('/:username/assets')} /> + <Route + path="/sketches" + component={createRedirectWithUsername('/:username/sketches')} + /> + <Route + path="/assets" + component={createRedirectWithUsername('/:username/assets')} + /> <Route path="/account" component={userIsAuthenticated(AccountView)} /> <Route path="/about" component={IDEView} /> {/* Mobile-only Routes */} <Route path="/preview" component={MobileSketchView} /> <Route path="/preferences" component={MobilePreferences} /> - </Route> ); diff --git a/client/store.js b/client/store.js index a8d2114e48..4cec9419b7 100644 --- a/client/store.js +++ b/client/store.js @@ -6,13 +6,15 @@ import { clearState, loadState } from './persistState'; import getConfig from './utils/getConfig'; export default function configureStore(initialState) { - const enhancers = [ - applyMiddleware(thunk), - ]; + const enhancers = [applyMiddleware(thunk)]; if (getConfig('CLIENT') && getConfig('NODE_ENV') === 'development') { // Enable DevTools only when rendering on client and during development. - enhancers.push(window.devToolsExtension ? window.devToolsExtension() : DevTools.instrument()); + enhancers.push( + window.devToolsExtension + ? window.devToolsExtension() + : DevTools.instrument() + ); } const savedState = loadState(); @@ -34,4 +36,3 @@ export default function configureStore(initialState) { return store; } - diff --git a/client/test-utils.js b/client/test-utils.js index b8e9e382ad..8847383068 100644 --- a/client/test-utils.js +++ b/client/test-utils.js @@ -14,7 +14,6 @@ import { render } from '@testing-library/react'; import React from 'react'; import PropTypes from 'prop-types'; - import { I18nextProvider } from 'react-i18next'; import i18n from './i18n-test'; @@ -24,13 +23,11 @@ export * from '@testing-library/react'; const Providers = ({ children }) => ( // eslint-disable-next-line react/jsx-filename-extension - <I18nextProvider i18n={i18n}> - {children} - </I18nextProvider> + <I18nextProvider i18n={i18n}>{children}</I18nextProvider> ); Providers.propTypes = { - children: PropTypes.element.isRequired, + children: PropTypes.element.isRequired }; const customRender = (ui, options) => diff --git a/client/theme.js b/client/theme.js index 9d096d78a2..f22f13adb4 100644 --- a/client/theme.js +++ b/client/theme.js @@ -3,7 +3,7 @@ import lodash from 'lodash'; export const Theme = { contrast: 'contrast', dark: 'dark', - light: 'light', + light: 'light' }; export const colors = { @@ -20,7 +20,7 @@ export const colors = { p5ContrastPink: ' #FFA9D9', borderColor: ' #B5B5B5', - outlineColor: '#0F9DD7', + outlineColor: '#0F9DD7' }; export const grays = { @@ -37,7 +37,7 @@ export const grays = { dark: '#333', // primary darker: '#1C1C1C', - darkest: '#000', + darkest: '#000' }; export const common = { @@ -45,9 +45,9 @@ export const common = { shadowColor: 'rgba(0, 0, 0, 0.16)' }; -export const remSize = size => `${size / common.baseFontSize}rem`; +export const remSize = (size) => `${size / common.baseFontSize}rem`; -export const prop = key => (props) => { +export const prop = (key) => (props) => { const keypath = `theme.${key}`; const value = lodash.get(props, keypath); @@ -58,7 +58,6 @@ export const prop = key => (props) => { return value; }; - export default { [Theme.light]: { colors, @@ -70,23 +69,23 @@ export default { default: { foreground: colors.black, background: grays.light, - border: grays.middleLight, + border: grays.middleLight }, hover: { foreground: grays.lightest, background: colors.p5jsPink, - border: colors.p5jsPink, + border: colors.p5jsPink }, active: { foreground: grays.lightest, background: colors.p5jsActivePink, - border: colors.p5jsActivePink, + border: colors.p5jsActivePink }, disabled: { foreground: colors.black, background: grays.light, - border: grays.middleLight, - }, + border: grays.middleLight + } }, Icon: { default: grays.middleGray, @@ -96,8 +95,8 @@ export default { default: { foreground: colors.black, background: grays.light, - border: grays.middleLight, - }, + border: grays.middleLight + } }, Modal: { background: grays.light, @@ -123,23 +122,23 @@ export default { default: { foreground: grays.light, background: grays.dark, - border: grays.middleDark, + border: grays.middleDark }, hover: { foreground: grays.lightest, background: colors.p5jsPink, - border: colors.p5jsPink, + border: colors.p5jsPink }, active: { foreground: grays.lightest, background: colors.p5jsActivePink, - border: colors.p5jsActivePink, + border: colors.p5jsActivePink }, disabled: { foreground: grays.light, background: grays.dark, - border: grays.middleDark, - }, + border: grays.middleDark + } }, Icon: { default: grays.middleLight, @@ -149,8 +148,8 @@ export default { default: { foreground: grays.light, background: grays.dark, - border: grays.middleDark, - }, + border: grays.middleDark + } }, Modal: { background: grays.dark, @@ -176,23 +175,23 @@ export default { default: { foreground: grays.light, background: grays.dark, - border: grays.middleDark, + border: grays.middleDark }, hover: { foreground: grays.dark, background: colors.yellow, - border: colors.yellow, + border: colors.yellow }, active: { foreground: grays.dark, background: colors.p5jsActivePink, - border: colors.p5jsActivePink, + border: colors.p5jsActivePink }, disabled: { foreground: grays.light, background: grays.dark, - border: grays.middleDark, - }, + border: grays.middleDark + } }, Icon: { default: grays.mediumLight, @@ -202,8 +201,8 @@ export default { default: { foreground: grays.light, background: grays.dark, - border: grays.middleDark, - }, + border: grays.middleDark + } }, Modal: { background: grays.dark, @@ -218,5 +217,5 @@ export default { background: grays.dark } } - }, + } }; diff --git a/client/utils/auth.js b/client/utils/auth.js index 26ba9e07e5..6114ef2a0b 100644 --- a/client/utils/auth.js +++ b/client/utils/auth.js @@ -7,15 +7,16 @@ export const userIsAuthenticated = connectedRouterRedirect({ // The url to redirect user to if they fail redirectPath: '/login', // Determine if the user is authenticated or not - authenticatedSelector: state => state.user.authenticated === true, + authenticatedSelector: (state) => state.user.authenticated === true, // A nice display name for this check wrapperDisplayName: 'UserIsAuthenticated' }); export const userIsNotAuthenticated = connectedRouterRedirect({ - redirectPath: (state, ownProps) => locationHelper.getRedirectQueryParam(ownProps) || '/', + redirectPath: (state, ownProps) => + locationHelper.getRedirectQueryParam(ownProps) || '/', allowRedirectBack: false, - authenticatedSelector: state => state.user.authenticated === false, + authenticatedSelector: (state) => state.user.authenticated === false, wrapperDisplayName: 'UserIsNotAuthenticated' }); @@ -25,5 +26,5 @@ export const userIsAuthorized = connectedRouterRedirect({ authenticatedSelector: (state, ownProps) => { const { username } = ownProps.params; return state.user.username === username; - }, + } }); diff --git a/client/utils/consoleUtils.js b/client/utils/consoleUtils.js index 19e942e838..0f10eb9841 100644 --- a/client/utils/consoleUtils.js +++ b/client/utils/consoleUtils.js @@ -78,7 +78,9 @@ export const getAllScriptOffsets = (htmlFile) => { } else { endFilenameInd = htmlFile.indexOf('.js', ind + startTag.length + 1); filename = htmlFile.substring(ind + startTag.length, endFilenameInd); - lineOffset = htmlFile.substring(0, ind).split('\n').length + hijackConsoleErrorsScriptLength; + lineOffset = + htmlFile.substring(0, ind).split('\n').length + + hijackConsoleErrorsScriptLength; offs.push([lineOffset, filename]); lastInd = ind + 1; } diff --git a/client/utils/custom-hooks.js b/client/utils/custom-hooks.js index 9271287e59..212b33d3b2 100644 --- a/client/utils/custom-hooks.js +++ b/client/utils/custom-hooks.js @@ -20,15 +20,20 @@ export const useModalBehavior = (hideOverlay) => { const ref = useRef({}); // Return values - const setRef = (r) => { ref.current = r; }; + const setRef = (r) => { + ref.current = r; + }; const [visible, setVisible] = useState(false); const trigger = () => setVisible(!visible); const hide = () => setVisible(false); - const handleClickOutside = ({ target }) => { - if (ref && ref.current && !(ref.current.contains && ref.current.contains(target))) { + if ( + ref && + ref.current && + !(ref.current.contains && ref.current.contains(target)) + ) { hide(); } }; @@ -53,7 +58,13 @@ export const useEffectWithComparison = (fn, props) => { }, Object.values(props)); }; -export const useEventListener = (event, callback, useCapture = false, list = []) => useEffect(() => { - document.addEventListener(event, callback, useCapture); - return () => document.removeEventListener(event, callback, useCapture); -}, list); +export const useEventListener = ( + event, + callback, + useCapture = false, + list = [] +) => + useEffect(() => { + document.addEventListener(event, callback, useCapture); + return () => document.removeEventListener(event, callback, useCapture); + }, list); diff --git a/client/utils/generateRandomName.js b/client/utils/generateRandomName.js index 101128d7fc..70da438fc7 100644 --- a/client/utils/generateRandomName.js +++ b/client/utils/generateRandomName.js @@ -1,12 +1,21 @@ import friendlyWords from 'friendly-words'; export function generateProjectName() { - const adj = friendlyWords.predicates[Math.floor(Math.random() * friendlyWords.predicates.length)]; - const obj = friendlyWords.objects[Math.floor(Math.random() * friendlyWords.objects.length)]; + const adj = + friendlyWords.predicates[ + Math.floor(Math.random() * friendlyWords.predicates.length) + ]; + const obj = + friendlyWords.objects[ + Math.floor(Math.random() * friendlyWords.objects.length) + ]; return `${adj} ${obj}`; } export function generateCollectionName() { - const adj = friendlyWords.predicates[Math.floor(Math.random() * friendlyWords.predicates.length)]; + const adj = + friendlyWords.predicates[ + Math.floor(Math.random() * friendlyWords.predicates.length) + ]; return `My ${adj} collection`; } diff --git a/client/utils/getConfig.js b/client/utils/getConfig.js index 43c050aea1..594af535f7 100644 --- a/client/utils/getConfig.js +++ b/client/utils/getConfig.js @@ -11,7 +11,8 @@ function getConfig(key, options = { warn: !isTestEnvironment() }) { throw new Error('"key" must be provided to getConfig()'); } - const env = (typeof global !== 'undefined' ? global : window)?.process?.env || {}; + const env = + (typeof global !== 'undefined' ? global : window)?.process?.env || {}; const value = env[key]; if (value == null && options?.warn !== false) { diff --git a/client/utils/isSecurePage.js b/client/utils/isSecurePage.js index 2efb908a62..e9e54ebe71 100644 --- a/client/utils/isSecurePage.js +++ b/client/utils/isSecurePage.js @@ -1,6 +1,3 @@ - -const isSecurePage = () => ( - window.location.protocol === 'https:' -); +const isSecurePage = () => window.location.protocol === 'https:'; export default isSecurePage; diff --git a/client/utils/metaKey.js b/client/utils/metaKey.js index e8c45eed89..6d9aab8a6c 100644 --- a/client/utils/metaKey.js +++ b/client/utils/metaKey.js @@ -1,8 +1,6 @@ const metaKey = (() => { if (navigator != null && navigator.platform != null) { - return /^MAC/i.test(navigator.platform) ? - 'Cmd' : - 'Ctrl'; + return /^MAC/i.test(navigator.platform) ? 'Cmd' : 'Ctrl'; } return 'Ctrl'; @@ -10,7 +8,4 @@ const metaKey = (() => { const metaKeyName = metaKey === 'Cmd' ? '⌘' : '⌃'; -export { - metaKey, - metaKeyName, -}; +export { metaKey, metaKeyName }; diff --git a/client/utils/reduxFormUtils.js b/client/utils/reduxFormUtils.js index 9339433a96..5d49ce4a28 100644 --- a/client/utils/reduxFormUtils.js +++ b/client/utils/reduxFormUtils.js @@ -32,7 +32,8 @@ function validateNameEmail(formProps, errors) { errors.email = i18n.t('ReduxFormUtils.errorEmptyEmail'); } else if ( // eslint-disable-next-line max-len - !formProps.email.match(EMAIL_REGEX)) { + !formProps.email.match(EMAIL_REGEX) + ) { errors.email = i18n.t('ReduxFormUtils.errorInvalidEmail'); } } @@ -73,7 +74,10 @@ function validatePasswords(formProps, errors) { errors.confirmPassword = i18n.t('ReduxFormUtils.errorConfirmPassword'); } - if (formProps.password !== formProps.confirmPassword && formProps.confirmPassword) { + if ( + formProps.password !== formProps.confirmPassword && + formProps.confirmPassword + ) { errors.confirmPassword = i18n.t('ReduxFormUtils.errorPasswordMismatch'); } } @@ -98,7 +102,8 @@ export function validateResetPassword(formProps) { errors.email = i18n.t('ReduxFormUtils.errorEmptyEmail'); } else if ( // eslint-disable-next-line max-len - !formProps.email.match(EMAIL_REGEX)) { + !formProps.email.match(EMAIL_REGEX) + ) { errors.email = i18n.t('ReduxFormUtils.errorInvalidEmail'); } return errors; diff --git a/client/utils/responsive.jsx b/client/utils/responsive.jsx index 05b0d84ec3..66cc9a750d 100644 --- a/client/utils/responsive.jsx +++ b/client/utils/responsive.jsx @@ -3,20 +3,24 @@ import { useSelector } from 'react-redux'; import MediaQuery from 'react-responsive'; import ResponsiveForm from '../modules/User/components/ResponsiveForm'; -export const mobileEnabled = () => (window.process.env.MOBILE_ENABLED === true); +export const mobileEnabled = () => window.process.env.MOBILE_ENABLED === true; export const mobileFirst = (MobileComponent, Fallback) => (props) => { - const { forceDesktop } = useSelector(state => state.editorAccessibility); + const { forceDesktop } = useSelector((state) => state.editorAccessibility); return ( <MediaQuery minWidth={770}> - {matches => ((matches || forceDesktop || (!mobileEnabled())) - ? <Fallback {...props} /> - : <MobileComponent {...props} />)} + {(matches) => + matches || forceDesktop || !mobileEnabled() ? ( + <Fallback {...props} /> + ) : ( + <MobileComponent {...props} /> + ) + } </MediaQuery> ); }; -export const responsiveForm = DesktopComponent => props => ( +export const responsiveForm = (DesktopComponent) => (props) => ( <ResponsiveForm> <DesktopComponent {...props} /> </ResponsiveForm> diff --git a/server/config/passport.js b/server/config/passport.js index b9b3c946ef..2d23628466 100644 --- a/server/config/passport.js +++ b/server/config/passport.js @@ -11,7 +11,10 @@ import { BasicStrategy } from 'passport-http'; import User from '../models/user'; function generateUniqueUsername(username) { - const adj = friendlyWords.predicates[Math.floor(Math.random() * friendlyWords.predicates.length)]; + const adj = + friendlyWords.predicates[ + Math.floor(Math.random() * friendlyWords.predicates.length) + ]; return slugify(`${username} ${adj}`); } @@ -28,39 +31,49 @@ passport.deserializeUser((id, done) => { /** * Sign in using Email/Username and Password. */ -passport.use(new LocalStrategy({ usernameField: 'email' }, (email, password, done) => { - User.findByEmailOrUsername(email) - .then((user) => { // eslint-disable-line consistent-return - if (!user) { - return done(null, false, { msg: `Email ${email} not found.` }); - } - user.comparePassword(password, (innerErr, isMatch) => { - if (isMatch) { - return done(null, user); +passport.use( + new LocalStrategy({ usernameField: 'email' }, (email, password, done) => { + User.findByEmailOrUsername(email) + .then((user) => { + // eslint-disable-line consistent-return + if (!user) { + return done(null, false, { msg: `Email ${email} not found.` }); } - return done(null, false, { msg: 'Invalid email or password.' }); - }); - }) - .catch(err => done(null, false, { msg: err })); -})); + user.comparePassword(password, (innerErr, isMatch) => { + if (isMatch) { + return done(null, user); + } + return done(null, false, { msg: 'Invalid email or password.' }); + }); + }) + .catch((err) => done(null, false, { msg: err })); + }) +); /** * Authentificate using Basic Auth (Username + Api Key) */ -passport.use(new BasicStrategy((userid, key, done) => { - User.findByUsername(userid, (err, user) => { // eslint-disable-line consistent-return - if (err) { return done(err); } - if (!user) { return done(null, false); } - user.findMatchingKey(key, (innerErr, isMatch, keyDocument) => { - if (isMatch) { - keyDocument.lastUsedAt = Date.now(); - user.save(); - return done(null, user); +passport.use( + new BasicStrategy((userid, key, done) => { + User.findByUsername(userid, (err, user) => { + // eslint-disable-line consistent-return + if (err) { + return done(err); + } + if (!user) { + return done(null, false); } - return done(null, false, { msg: 'Invalid username or API key' }); + user.findMatchingKey(key, (innerErr, isMatch, keyDocument) => { + if (isMatch) { + keyDocument.lastUsedAt = Date.now(); + user.save(); + return done(null, user); + } + return done(null, false, { msg: 'Invalid username or API key' }); + }); }); - }); -})); + }) +); /* Input: @@ -72,147 +85,184 @@ passport.use(new BasicStrategy((userid, key, done) => { Output: ['email@example.com'] */ -const getVerifiedEmails = githubEmails => ( +const getVerifiedEmails = (githubEmails) => (githubEmails || []) - .filter(item => item.verified === true) - .map(item => item.value) -); + .filter((item) => item.verified === true) + .map((item) => item.value); -const getPrimaryEmail = githubEmails => ( - ( - lodash.find(githubEmails, { primary: true }) || {} - ).value -); +const getPrimaryEmail = (githubEmails) => + (lodash.find(githubEmails, { primary: true }) || {}).value; /** * Sign in with GitHub. */ -passport.use(new GitHubStrategy({ - clientID: process.env.GITHUB_ID, - clientSecret: process.env.GITHUB_SECRET, - callbackURL: '/auth/github/callback', - passReqToCallback: true, - scope: ['user:email'], -}, (req, accessToken, refreshToken, profile, done) => { - User.findOne({ github: profile.id }, (findByGithubErr, existingUser) => { - if (existingUser) { - if (req.user && req.user.email !== existingUser.email) { - done(new Error('GitHub account is already linked to another account.')); - return; - } - done(null, existingUser); - return; - } +passport.use( + new GitHubStrategy( + { + clientID: process.env.GITHUB_ID, + clientSecret: process.env.GITHUB_SECRET, + callbackURL: '/auth/github/callback', + passReqToCallback: true, + scope: ['user:email'] + }, + (req, accessToken, refreshToken, profile, done) => { + User.findOne({ github: profile.id }, (findByGithubErr, existingUser) => { + if (existingUser) { + if (req.user && req.user.email !== existingUser.email) { + done( + new Error('GitHub account is already linked to another account.') + ); + return; + } + done(null, existingUser); + return; + } - const emails = getVerifiedEmails(profile.emails); - const primaryEmail = getPrimaryEmail(profile.emails); + const emails = getVerifiedEmails(profile.emails); + const primaryEmail = getPrimaryEmail(profile.emails); - if (req.user) { - if (!req.user.github) { - req.user.github = profile.id; - req.user.tokens.push({ kind: 'github', accessToken }); - req.user.verified = User.EmailConfirmation.Verified; - } - req.user.save(saveErr => done(null, req.user)); - } else { - User.findByEmail(emails, (findByEmailErr, existingEmailUser) => { - if (existingEmailUser) { - existingEmailUser.email = existingEmailUser.email || primaryEmail; - existingEmailUser.github = profile.id; - existingEmailUser.username = existingEmailUser.username || profile.username; - existingEmailUser.tokens.push({ kind: 'github', accessToken }); - existingEmailUser.name = existingEmailUser.name || profile.displayName; - existingEmailUser.verified = User.EmailConfirmation.Verified; - existingEmailUser.save(saveErr => done(null, existingEmailUser)); + if (req.user) { + if (!req.user.github) { + req.user.github = profile.id; + req.user.tokens.push({ kind: 'github', accessToken }); + req.user.verified = User.EmailConfirmation.Verified; + } + req.user.save((saveErr) => done(null, req.user)); } else { - let { username } = profile; - User.findByUsername(username, { caseInsensitive: true }, (findByUsernameErr, existingUsernameUser) => { - if (existingUsernameUser) { - username = generateUniqueUsername(username); + User.findByEmail(emails, (findByEmailErr, existingEmailUser) => { + if (existingEmailUser) { + existingEmailUser.email = existingEmailUser.email || primaryEmail; + existingEmailUser.github = profile.id; + existingEmailUser.username = + existingEmailUser.username || profile.username; + existingEmailUser.tokens.push({ kind: 'github', accessToken }); + existingEmailUser.name = + existingEmailUser.name || profile.displayName; + existingEmailUser.verified = User.EmailConfirmation.Verified; + existingEmailUser.save((saveErr) => + done(null, existingEmailUser) + ); + } else { + let { username } = profile; + User.findByUsername( + username, + { caseInsensitive: true }, + (findByUsernameErr, existingUsernameUser) => { + if (existingUsernameUser) { + username = generateUniqueUsername(username); + } + const user = new User(); + user.email = primaryEmail; + user.github = profile.id; + user.username = profile.username; + user.tokens.push({ kind: 'github', accessToken }); + user.name = profile.displayName; + user.verified = User.EmailConfirmation.Verified; + user.save((saveErr) => done(null, user)); + } + ); } - const user = new User(); - user.email = primaryEmail; - user.github = profile.id; - user.username = profile.username; - user.tokens.push({ kind: 'github', accessToken }); - user.name = profile.displayName; - user.verified = User.EmailConfirmation.Verified; - user.save(saveErr => done(null, user)); }); } }); } - }); -})); + ) +); /** * Sign in with Google. */ -passport.use(new GoogleStrategy({ - clientID: process.env.GOOGLE_ID, - clientSecret: process.env.GOOGLE_SECRET, - callbackURL: '/auth/google/callback', - passReqToCallback: true, - scope: ['openid email'], -}, (req, accessToken, refreshToken, profile, done) => { - User.findOne({ google: profile._json.emails[0].value }, (findByGoogleErr, existingUser) => { - if (existingUser) { - if (req.user && req.user.email !== existingUser.email) { - done(new Error('Google account is already linked to another account.')); - return; - } - done(null, existingUser); - return; - } +passport.use( + new GoogleStrategy( + { + clientID: process.env.GOOGLE_ID, + clientSecret: process.env.GOOGLE_SECRET, + callbackURL: '/auth/google/callback', + passReqToCallback: true, + scope: ['openid email'] + }, + (req, accessToken, refreshToken, profile, done) => { + User.findOne( + { google: profile._json.emails[0].value }, + (findByGoogleErr, existingUser) => { + if (existingUser) { + if (req.user && req.user.email !== existingUser.email) { + done( + new Error( + 'Google account is already linked to another account.' + ) + ); + return; + } + done(null, existingUser); + return; + } - const primaryEmail = profile._json.emails[0].value; + const primaryEmail = profile._json.emails[0].value; - if (req.user) { - if (!req.user.google) { - req.user.google = profile._json.emails[0].value; - req.user.tokens.push({ kind: 'google', accessToken }); - req.user.verified = User.EmailConfirmation.Verified; - } - req.user.save(saveErr => done(null, req.user)); - } else { - User.findByEmail(primaryEmail, (findByEmailErr, existingEmailUser) => { - let username = profile._json.emails[0].value.split('@')[0]; - User.findByUsername(username, { caseInsensitive: true }, (findByUsernameErr, existingUsernameUser) => { - if (existingUsernameUser) { - username = generateUniqueUsername(username); - } - // what if a username is already taken from the display name too? - // then, append a random friendly word? - if (existingEmailUser) { - existingEmailUser.email = existingEmailUser.email || primaryEmail; - existingEmailUser.google = profile._json.emails[0].value; - existingEmailUser.username = existingEmailUser.username || username; - existingEmailUser.tokens.push({ kind: 'google', accessToken }); - existingEmailUser.name = existingEmailUser.name || profile._json.displayName; - existingEmailUser.verified = User.EmailConfirmation.Verified; - existingEmailUser.save((saveErr) => { - if (saveErr) { - console.log(saveErr); - } - done(null, existingEmailUser); - }); + if (req.user) { + if (!req.user.google) { + req.user.google = profile._json.emails[0].value; + req.user.tokens.push({ kind: 'google', accessToken }); + req.user.verified = User.EmailConfirmation.Verified; + } + req.user.save((saveErr) => done(null, req.user)); } else { - const user = new User(); - user.email = primaryEmail; - user.google = profile._json.emails[0].value; - user.username = username; - user.tokens.push({ kind: 'google', accessToken }); - user.name = profile._json.displayName; - user.verified = User.EmailConfirmation.Verified; - user.save((saveErr) => { - if (saveErr) { - console.log(saveErr); + User.findByEmail( + primaryEmail, + (findByEmailErr, existingEmailUser) => { + let username = profile._json.emails[0].value.split('@')[0]; + User.findByUsername( + username, + { caseInsensitive: true }, + (findByUsernameErr, existingUsernameUser) => { + if (existingUsernameUser) { + username = generateUniqueUsername(username); + } + // what if a username is already taken from the display name too? + // then, append a random friendly word? + if (existingEmailUser) { + existingEmailUser.email = + existingEmailUser.email || primaryEmail; + existingEmailUser.google = profile._json.emails[0].value; + existingEmailUser.username = + existingEmailUser.username || username; + existingEmailUser.tokens.push({ + kind: 'google', + accessToken + }); + existingEmailUser.name = + existingEmailUser.name || profile._json.displayName; + existingEmailUser.verified = + User.EmailConfirmation.Verified; + existingEmailUser.save((saveErr) => { + if (saveErr) { + console.log(saveErr); + } + done(null, existingEmailUser); + }); + } else { + const user = new User(); + user.email = primaryEmail; + user.google = profile._json.emails[0].value; + user.username = username; + user.tokens.push({ kind: 'google', accessToken }); + user.name = profile._json.displayName; + user.verified = User.EmailConfirmation.Verified; + user.save((saveErr) => { + if (saveErr) { + console.log(saveErr); + } + done(null, user); + }); + } + } + ); } - done(null, user); - }); + ); } - }); - }); + } + ); } - }); -})); + ) +); diff --git a/server/controllers/aws.controller.js b/server/controllers/aws.controller.js index fd16232420..3b3eab03f1 100644 --- a/server/controllers/aws.controller.js +++ b/server/controllers/aws.controller.js @@ -17,15 +17,16 @@ const client = s3.createClient({ accessKeyId: `${process.env.AWS_ACCESS_KEY}`, secretAccessKey: `${process.env.AWS_SECRET_KEY}`, region: `${process.env.AWS_REGION}` - }, + } }); -const s3Bucket = process.env.S3_BUCKET_URL_BASE || +const s3Bucket = + process.env.S3_BUCKET_URL_BASE || `https://s3-${process.env.AWS_REGION}.amazonaws.com/${process.env.S3_BUCKET}/`; function getExtension(filename) { const i = filename.lastIndexOf('.'); - return (i < 0) ? '' : filename.substr(i); + return i < 0 ? '' : filename.substr(i); } export function getObjectKey(url) { @@ -44,8 +45,8 @@ export function deleteObjectsFromS3(keyList, callback) { const params = { Bucket: `${process.env.S3_BUCKET}`, Delete: { - Objects: keys, - }, + Objects: keys + } }; const del = client.deleteObjects(params); del.on('end', () => { @@ -74,7 +75,9 @@ export function deleteObjectFromS3(req, res) { export function signS3(req, res) { const limit = process.env.UPLOAD_LIMIT || 250000000; if (req.user.totalSize > limit) { - res.status(403).send({ message: 'user has uploaded the maximum size of assets.' }); + res + .status(403) + .send({ message: 'user has uploaded the maximum size of assets.' }); return; } const fileExtension = getExtension(req.body.name); @@ -104,7 +107,11 @@ export function copyObjectInS3(url, userId) { }; client.s3.headObject(headParams, (headErr) => { if (headErr) { - reject(new Error(`Object with key ${process.env.S3_BUCKET}/${objectKey} does not exist.`)); + reject( + new Error( + `Object with key ${process.env.S3_BUCKET}/${objectKey} does not exist.` + ) + ); return; } const params = { @@ -142,7 +149,11 @@ export function moveObjectToUserInS3(url, userId) { }; client.s3.headObject(headParams, (headErr) => { if (headErr) { - reject(new Error(`Object with key ${process.env.S3_BUCKET}/${objectKey} does not exist.`)); + reject( + new Error( + `Object with key ${process.env.S3_BUCKET}/${objectKey} does not exist.` + ) + ); return; } const params = { @@ -171,48 +182,57 @@ export function listObjectsInS3ForUser(userId) { Prefix: `${userId}/` } }; - client.listObjects(params) + client + .listObjects(params) .on('data', (data) => { - assets = assets.concat(data.Contents.map(object => ({ key: object.Key, size: object.Size }))); + assets = assets.concat( + data.Contents.map((object) => ({ + key: object.Key, + size: object.Size + })) + ); }) .on('end', () => { resolve(); }); - }).then(() => getProjectsForUserId(userId)).then((projects) => { - const projectAssets = []; - let totalSize = 0; - assets.forEach((asset) => { - const name = asset.key.split('/').pop(); - const foundAsset = { - key: asset.key, - name, - size: asset.size, - url: `${process.env.S3_BUCKET_URL_BASE}${asset.key}` - }; - totalSize += asset.size; - projects.some((project) => { - let found = false; - project.files.some((file) => { - if (!file.url) return false; - if (file.url.includes(asset.key)) { - found = true; - foundAsset.name = file.name; - foundAsset.sketchName = project.name; - foundAsset.sketchId = project.id; - foundAsset.url = file.url; - return true; - } - return false; + }) + .then(() => getProjectsForUserId(userId)) + .then((projects) => { + const projectAssets = []; + let totalSize = 0; + assets.forEach((asset) => { + const name = asset.key.split('/').pop(); + const foundAsset = { + key: asset.key, + name, + size: asset.size, + url: `${process.env.S3_BUCKET_URL_BASE}${asset.key}` + }; + totalSize += asset.size; + projects.some((project) => { + let found = false; + project.files.some((file) => { + if (!file.url) return false; + if (file.url.includes(asset.key)) { + found = true; + foundAsset.name = file.name; + foundAsset.sketchName = project.name; + foundAsset.sketchId = project.id; + foundAsset.url = file.url; + return true; + } + return false; + }); + return found; }); - return found; + projectAssets.push(foundAsset); }); - projectAssets.push(foundAsset); + return Promise.resolve({ assets: projectAssets, totalSize }); + }) + .catch((err) => { + console.log('got an error'); + console.log(err); }); - return Promise.resolve({ assets: projectAssets, totalSize }); - }).catch((err) => { - console.log('got an error'); - console.log(err); - }); } export function listObjectsInS3ForUserRequestHandler(req, res) { diff --git a/server/controllers/collection.controller/addProjectToCollection.js b/server/controllers/collection.controller/addProjectToCollection.js index 9816661f2d..74a4c3db38 100644 --- a/server/controllers/collection.controller/addProjectToCollection.js +++ b/server/controllers/collection.controller/addProjectToCollection.js @@ -5,7 +5,10 @@ export default function addProjectToCollection(req, res) { const owner = req.user._id; const { id: collectionId, projectId } = req.params; - const collectionPromise = Collection.findById(collectionId).populate('items.project', '_id'); + const collectionPromise = Collection.findById(collectionId).populate( + 'items.project', + '_id' + ); const projectPromise = Project.findById(projectId); function sendFailure(code, message) { @@ -32,7 +35,9 @@ export default function addProjectToCollection(req, res) { return null; } - const projectInCollection = collection.items.find(p => p.projectId === project._id); + const projectInCollection = collection.items.find( + (p) => p.projectId === project._id + ); if (projectInCollection) { sendFailure(404, 'Project already in collection'); @@ -51,19 +56,17 @@ export default function addProjectToCollection(req, res) { } function populateReferences(collection) { - return Collection.populate( - collection, - [ - { path: 'owner', select: ['id', 'username'] }, - { - path: 'items.project', - select: ['id', 'name', 'slug'], - populate: { - path: 'user', select: ['username'] - } + return Collection.populate(collection, [ + { path: 'owner', select: ['id', 'username'] }, + { + path: 'items.project', + select: ['id', 'name', 'slug'], + populate: { + path: 'user', + select: ['username'] } - ] - ); + } + ]); } return Promise.all([collectionPromise, projectPromise]) diff --git a/server/controllers/collection.controller/collectionForUserExists.js b/server/controllers/collection.controller/collectionForUserExists.js index 4dd2d40ff2..61893c6224 100644 --- a/server/controllers/collection.controller/collectionForUserExists.js +++ b/server/controllers/collection.controller/collectionForUserExists.js @@ -1,7 +1,11 @@ import Collection from '../../models/collection'; import User from '../../models/user'; -export default function collectionForUserExists(username, collectionId, callback) { +export default function collectionForUserExists( + username, + collectionId, + callback +) { function sendFailure() { callback(false); } @@ -22,8 +26,5 @@ export default function collectionForUserExists(username, collectionId, callback return Collection.findOne({ _id: collectionId, owner }); } - return findUser() - .then(findCollection) - .then(sendSuccess) - .catch(sendFailure); + return findUser().then(findCollection).then(sendSuccess).catch(sendFailure); } diff --git a/server/controllers/collection.controller/createCollection.js b/server/controllers/collection.controller/createCollection.js index 9fd97be3e9..61838ccd87 100644 --- a/server/controllers/collection.controller/createCollection.js +++ b/server/controllers/collection.controller/createCollection.js @@ -20,19 +20,17 @@ export default function createCollection(req, res) { } function populateReferences(newCollection) { - return Collection.populate( - newCollection, - [ - { path: 'owner', select: ['id', 'username'] }, - { - path: 'items.project', - select: ['id', 'name', 'slug'], - populate: { - path: 'user', select: ['username'] - } + return Collection.populate(newCollection, [ + { path: 'owner', select: ['id', 'username'] }, + { + path: 'items.project', + select: ['id', 'name', 'slug'], + populate: { + path: 'user', + select: ['username'] } - ] - ); + } + ]); } if (owner == null) { diff --git a/server/controllers/collection.controller/listCollections.js b/server/controllers/collection.controller/listCollections.js index b1a60ee9da..fb5c70cff8 100644 --- a/server/controllers/collection.controller/listCollections.js +++ b/server/controllers/collection.controller/listCollections.js @@ -3,8 +3,7 @@ import User from '../../models/user'; async function getOwnerUserId(req) { if (req.params.username) { - const user = - await User.findByUsername(req.params.username); + const user = await User.findByUsername(req.params.username); if (user && user._id) { return user._id; } @@ -29,17 +28,17 @@ export default function listCollections(req, res) { sendFailure({ code: 404, message: 'User not found' }); } - return Collection.find({ owner }) - .populate([ - { path: 'owner', select: ['id', 'username'] }, - { - path: 'items.project', - select: ['id', 'name', 'slug'], - populate: { - path: 'user', select: ['username'] - } + return Collection.find({ owner }).populate([ + { path: 'owner', select: ['id', 'username'] }, + { + path: 'items.project', + select: ['id', 'name', 'slug'], + populate: { + path: 'user', + select: ['username'] } - ]); + } + ]); } return getOwnerUserId(req) diff --git a/server/controllers/collection.controller/removeCollection.js b/server/controllers/collection.controller/removeCollection.js index dffff87667..640f664573 100644 --- a/server/controllers/collection.controller/removeCollection.js +++ b/server/controllers/collection.controller/removeCollection.js @@ -1,6 +1,5 @@ import Collection from '../../models/collection'; - export default function createCollection(req, res) { const { id: collectionId } = req.params; const owner = req.user._id; @@ -15,7 +14,10 @@ export default function createCollection(req, res) { function removeCollection(collection) { if (collection == null) { - sendFailure({ code: 404, message: 'Not found, or you user does not own this collection' }); + sendFailure({ + code: 404, + message: 'Not found, or you user does not own this collection' + }); return null; } diff --git a/server/controllers/collection.controller/removeProjectFromCollection.js b/server/controllers/collection.controller/removeProjectFromCollection.js index f2df7bbe76..56a83df297 100644 --- a/server/controllers/collection.controller/removeProjectFromCollection.js +++ b/server/controllers/collection.controller/removeProjectFromCollection.js @@ -23,7 +23,7 @@ export default function addProjectToCollection(req, res) { return null; } - const project = collection.items.find(p => p.projectId === projectId); + const project = collection.items.find((p) => p.projectId === projectId); if (project != null) { project.remove(); @@ -37,19 +37,17 @@ export default function addProjectToCollection(req, res) { } function populateReferences(collection) { - return Collection.populate( - collection, - [ - { path: 'owner', select: ['id', 'username'] }, - { - path: 'items.project', - select: ['id', 'name', 'slug'], - populate: { - path: 'user', select: ['username'] - } + return Collection.populate(collection, [ + { path: 'owner', select: ['id', 'username'] }, + { + path: 'items.project', + select: ['id', 'name', 'slug'], + populate: { + path: 'user', + select: ['username'] } - ] - ); + } + ]); } return Collection.findById(collectionId) diff --git a/server/controllers/collection.controller/updateCollection.js b/server/controllers/collection.controller/updateCollection.js index 365b8d24d1..525346c0a5 100644 --- a/server/controllers/collection.controller/updateCollection.js +++ b/server/controllers/collection.controller/updateCollection.js @@ -23,7 +23,10 @@ export default function createCollection(req, res) { function sendSuccess(collection) { if (collection == null) { - sendFailure({ code: 404, message: 'Not found, or you user does not own this collection' }); + sendFailure({ + code: 404, + message: 'Not found, or you user does not own this collection' + }); return; } @@ -32,23 +35,24 @@ export default function createCollection(req, res) { async function findAndUpdateCollection() { // Only update if owner matches current user - return Collection.findOneAndUpdate( - { _id: collectionId, owner }, - values, - { new: true, runValidators: true, setDefaultsOnInsert: true } - ).populate([ - { path: 'owner', select: ['id', 'username'] }, - { - path: 'items.project', - select: ['id', 'name', 'slug'], - populate: { - path: 'user', select: ['username'] + return Collection.findOneAndUpdate({ _id: collectionId, owner }, values, { + new: true, + runValidators: true, + setDefaultsOnInsert: true + }) + .populate([ + { path: 'owner', select: ['id', 'username'] }, + { + path: 'items.project', + select: ['id', 'name', 'slug'], + populate: { + path: 'user', + select: ['username'] + } } - } - ]).exec(); + ]) + .exec(); } - return findAndUpdateCollection() - .then(sendSuccess) - .catch(sendFailure); + return findAndUpdateCollection().then(sendSuccess).catch(sendFailure); } diff --git a/server/controllers/embed.controller.js b/server/controllers/embed.controller.js index 2d8bef6acb..b8c11447cc 100644 --- a/server/controllers/embed.controller.js +++ b/server/controllers/embed.controller.js @@ -9,35 +9,36 @@ import { import { get404Sketch } from '../views/404Page'; export function serveProject(req, res) { - Project.findById(req.params.project_id) - .exec((err, project) => { - if (err || !project) { - get404Sketch(html => res.send(html)); - return; - } - // TODO this does not parse html - const { files } = project; - const htmlFile = files.find(file => file.name.match(/\.html$/i)).content; - const filesToInject = files.filter(file => file.name.match(/\.(js|css)$/i)); - injectMediaUrls(filesToInject, files, req.params.project_id); + Project.findById(req.params.project_id).exec((err, project) => { + if (err || !project) { + get404Sketch((html) => res.send(html)); + return; + } + // TODO this does not parse html + const { files } = project; + const htmlFile = files.find((file) => file.name.match(/\.html$/i)).content; + const filesToInject = files.filter((file) => + file.name.match(/\.(js|css)$/i) + ); + injectMediaUrls(filesToInject, files, req.params.project_id); - jsdom.env(htmlFile, (innerErr, window) => { - const sketchDoc = window.document; + jsdom.env(htmlFile, (innerErr, window) => { + const sketchDoc = window.document; - const base = sketchDoc.createElement('base'); - const fullUrl = `${req.protocol}://${req.get('host')}${req.originalUrl}`; - base.href = `${fullUrl}/`; - sketchDoc.head.appendChild(base); + const base = sketchDoc.createElement('base'); + const fullUrl = `${req.protocol}://${req.get('host')}${req.originalUrl}`; + base.href = `${fullUrl}/`; + sketchDoc.head.appendChild(base); - resolvePathsForElementsWithAttribute('src', sketchDoc, files); - resolvePathsForElementsWithAttribute('href', sketchDoc, files); - resolveScripts(sketchDoc, files); - resolveStyles(sketchDoc, files); + resolvePathsForElementsWithAttribute('src', sketchDoc, files); + resolvePathsForElementsWithAttribute('href', sketchDoc, files); + resolveScripts(sketchDoc, files); + resolveStyles(sketchDoc, files); - res.setHeader('Cache-Control', 'public, max-age=0'); - res.send(serializeDocument(sketchDoc)); - }); + res.setHeader('Cache-Control', 'public, max-age=0'); + res.send(serializeDocument(sketchDoc)); }); + }); } export default serveProject; diff --git a/server/controllers/file.controller.js b/server/controllers/file.controller.js index 1745b53207..c3d0d15f64 100644 --- a/server/controllers/file.controller.js +++ b/server/controllers/file.controller.js @@ -21,10 +21,14 @@ export function createFile(req, res) { }, { new: true - }, (err, updatedProject) => { + }, + (err, updatedProject) => { if (err || !updatedProject) { console.log(err); - res.status(403).send({ success: false, message: 'Project does not exist, or user does not match owner.' }); + res.status(403).send({ + success: false, + message: 'Project does not exist, or user does not match owner.' + }); return; } const newFile = updatedProject.files[updatedProject.files.length - 1]; @@ -35,48 +39,62 @@ export function createFile(req, res) { res.json({ success: false }); return; } - savedProject.populate({ path: 'user', select: 'username' }, (_, populatedProject) => { - res.json({ - updatedFile: updatedProject.files[updatedProject.files.length - 1], - project: populatedProject - }); - }); + savedProject.populate( + { path: 'user', select: 'username' }, + (_, populatedProject) => { + res.json({ + updatedFile: + updatedProject.files[updatedProject.files.length - 1], + project: populatedProject + }); + } + ); }); } ); } function getAllDescendantIds(files, nodeId) { - const parentFile = files.find(file => file.id === nodeId); + const parentFile = files.find((file) => file.id === nodeId); if (!parentFile) return []; - return parentFile.children - .reduce((acc, childId) => ( - [...acc, childId, ...getAllDescendantIds(files, childId)] - ), []); + return parentFile.children.reduce( + (acc, childId) => [...acc, childId, ...getAllDescendantIds(files, childId)], + [] + ); } function deleteMany(files, ids) { const objectKeys = []; - each(ids, (id, cb) => { - if (files.id(id).url) { - if (!process.env.S3_DATE - || (process.env.S3_DATE && isBefore(new Date(process.env.S3_DATE), new Date(files.id(id).createdAt)))) { - const objectKey = getObjectKey(files.id(id).url); - objectKeys.push(objectKey); + each( + ids, + (id, cb) => { + if (files.id(id).url) { + if ( + !process.env.S3_DATE || + (process.env.S3_DATE && + isBefore( + new Date(process.env.S3_DATE), + new Date(files.id(id).createdAt) + )) + ) { + const objectKey = getObjectKey(files.id(id).url); + objectKeys.push(objectKey); + } } + files.id(id).remove(); + cb(); + }, + (err) => { + deleteObjectsFromS3(objectKeys); } - files.id(id).remove(); - cb(); - }, (err) => { - deleteObjectsFromS3(objectKeys); - }); + ); } function deleteChild(files, parentId, id) { return files.map((file) => { if (file.id === parentId) { - file.children = file.children.filter(child => child !== id); + file.children = file.children.filter((child) => child !== id); return file; } return file; @@ -86,23 +104,36 @@ function deleteChild(files, parentId, id) { export function deleteFile(req, res) { Project.findById(req.params.project_id, (err, project) => { if (!project) { - res.status(404).send({ success: false, message: 'Project does not exist.' }); + res + .status(404) + .send({ success: false, message: 'Project does not exist.' }); } if (!project.user.equals(req.user._id)) { - res.status(403).send({ success: false, message: 'Session does not match owner of project.' }); + res.status(403).send({ + success: false, + message: 'Session does not match owner of project.' + }); return; } // make sure file exists for project - const fileToDelete = project.files.find(file => file.id === req.params.file_id); + const fileToDelete = project.files.find( + (file) => file.id === req.params.file_id + ); if (!fileToDelete) { - res.status(404).send({ success: false, message: 'File does not exist in project.' }); + res + .status(404) + .send({ success: false, message: 'File does not exist in project.' }); return; } const idsToDelete = getAllDescendantIds(project.files, req.params.file_id); deleteMany(project.files, [req.params.file_id, ...idsToDelete]); - project.files = deleteChild(project.files, req.query.parentId, req.params.file_id); + project.files = deleteChild( + project.files, + req.query.parentId, + req.params.file_id + ); project.save((innerErr, savedProject) => { res.json({ project: savedProject }); }); @@ -112,16 +143,23 @@ export function deleteFile(req, res) { export function getFileContent(req, res) { Project.findById(req.params.project_id, (err, project) => { if (err || project === null) { - res.status(404).send({ success: false, message: 'Project with that id does not exist.' }); + res.status(404).send({ + success: false, + message: 'Project with that id does not exist.' + }); return; } const filePath = req.params[0]; const resolvedFile = resolvePathToFile(filePath, project.files); if (!resolvedFile) { - res.status(404).send({ success: false, message: 'File with that name and path does not exist.' }); + res.status(404).send({ + success: false, + message: 'File with that name and path does not exist.' + }); return; } - const contentType = mime.lookup(resolvedFile.name) || 'application/octet-stream'; + const contentType = + mime.lookup(resolvedFile.name) || 'application/octet-stream'; res.set('Content-Type', contentType); res.send(resolvedFile.content); }); diff --git a/server/controllers/project.controller.js b/server/controllers/project.controller.js index fa323fba93..8066604401 100644 --- a/server/controllers/project.controller.js +++ b/server/controllers/project.controller.js @@ -10,18 +10,33 @@ import User from '../models/user'; import { resolvePathToFile } from '../utils/filePath'; import generateFileSystemSafeName from '../utils/generateFileSystemSafeName'; -export { default as createProject, apiCreateProject } from './project.controller/createProject'; +export { + default as createProject, + apiCreateProject +} from './project.controller/createProject'; export { default as deleteProject } from './project.controller/deleteProject'; -export { default as getProjectsForUser, apiGetProjectsForUser } from './project.controller/getProjectsForUser'; +export { + default as getProjectsForUser, + apiGetProjectsForUser +} from './project.controller/getProjectsForUser'; export function updateProject(req, res) { Project.findById(req.params.project_id, (findProjectErr, project) => { if (!project.user.equals(req.user._id)) { - res.status(403).send({ success: false, message: 'Session does not match owner of project.' }); + res.status(403).send({ + success: false, + message: 'Session does not match owner of project.' + }); return; } - if (req.body.updatedAt && isAfter(new Date(project.updatedAt), new Date(req.body.updatedAt))) { - res.status(409).send({ success: false, message: 'Attempted to save stale version of project.' }); + if ( + req.body.updatedAt && + isAfter(new Date(project.updatedAt), new Date(req.body.updatedAt)) + ) { + res.status(409).send({ + success: false, + message: 'Attempted to save stale version of project.' + }); return; } Project.findByIdAndUpdate( @@ -41,10 +56,15 @@ export function updateProject(req, res) { res.status(400).json({ success: false }); return; } - if (req.body.files && updatedProject.files.length !== req.body.files.length) { - const oldFileIds = updatedProject.files.map(file => file.id); - const newFileIds = req.body.files.map(file => file.id); - const staleIds = oldFileIds.filter(id => newFileIds.indexOf(id) === -1); + if ( + req.body.files && + updatedProject.files.length !== req.body.files.length + ) { + const oldFileIds = updatedProject.files.map((file) => file.id); + const newFileIds = req.body.files.map((file) => file.id); + const staleIds = oldFileIds.filter( + (id) => newFileIds.indexOf(id) === -1 + ); staleIds.forEach((staleId) => { updatedProject.files.id(staleId).remove(); }); @@ -67,14 +87,21 @@ export function getProject(req, res) { const { project_id: projectId, username } = req.params; User.findByUsername(username, (err, user) => { // eslint-disable-line if (!user) { - return res.status(404).send({ message: 'Project with that username does not exist' }); + return res + .status(404) + .send({ message: 'Project with that username does not exist' }); } - Project.findOne({ user: user._id, $or: [{ _id: projectId }, { slug: projectId }] }) + Project.findOne({ + user: user._id, + $or: [{ _id: projectId }, { slug: projectId }] + }) .populate('user', 'username') .exec((err, project) => { // eslint-disable-line if (err) { console.log(err); - return res.status(404).send({ message: 'Project with that id does not exist' }); + return res + .status(404) + .send({ message: 'Project with that id does not exist' }); } return res.json(project); }); @@ -100,10 +127,14 @@ export function getProjectAsset(req, res) { .populate('user', 'username') .exec((err, project) => { // eslint-disable-line if (err) { - return res.status(404).send({ message: 'Project with that id does not exist' }); + return res + .status(404) + .send({ message: 'Project with that id does not exist' }); } if (!project) { - return res.status(404).send({ message: 'Project with that id does not exist' }); + return res + .status(404) + .send({ message: 'Project with that id does not exist' }); } const filePath = req.params[0]; @@ -114,21 +145,23 @@ export function getProjectAsset(req, res) { if (!resolvedFile.url) { return res.send(resolvedFile.content); } - request({ method: 'GET', url: resolvedFile.url, encoding: null }, (innerErr, response, body) => { - if (innerErr) { - return res.status(404).send({ message: 'Asset does not exist' }); + request( + { method: 'GET', url: resolvedFile.url, encoding: null }, + (innerErr, response, body) => { + if (innerErr) { + return res.status(404).send({ message: 'Asset does not exist' }); + } + return res.send(body); } - return res.send(body); - }); + ); }); } export function getProjects(req, res) { if (req.user) { - getProjectsForUserId(req.user._id) - .then((projects) => { - res.json(projects); - }); + getProjectsForUserId(req.user._id).then((projects) => { + res.json(projects); + }); } else { // could just move this to client side res.json([]); @@ -136,9 +169,9 @@ export function getProjects(req, res) { } export function projectExists(projectId, callback) { - Project.findById(projectId, (err, project) => ( + Project.findById(projectId, (err, project) => project ? callback(true) : callback(false) - )); + ); } export function projectForUserExists(username, projectId, callback) { @@ -147,18 +180,21 @@ export function projectForUserExists(username, projectId, callback) { callback(false); return; } - Project.findOne({ user: user._id, $or: [{ _id: projectId }, { slug: projectId }] }, (innerErr, project) => { - if (!project) { - callback(false); - return; + Project.findOne( + { user: user._id, $or: [{ _id: projectId }, { slug: projectId }] }, + (innerErr, project) => { + if (!project) { + callback(false); + return; + } + callback(true); } - callback(true); - }); + ); }); } function bundleExternalLibs(project, zip, callback) { - const indexHtml = project.files.find(file => file.name.match(/\.html$/)); + const indexHtml = project.files.find((file) => file.name.match(/\.html$/)); let numScriptsResolved = 0; let numScriptTags = 0; @@ -176,20 +212,23 @@ function bundleExternalLibs(project, zip, callback) { return; } - request({ method: 'GET', url: src, encoding: null }, (err, response, body) => { - if (err) { - console.log(err); - } else { - zip.append(body, { name: filename }); - scriptTag.src = filename; - } + request( + { method: 'GET', url: src, encoding: null }, + (err, response, body) => { + if (err) { + console.log(err); + } else { + zip.append(body, { name: filename }); + scriptTag.src = filename; + } - numScriptsResolved += 1; - if (numScriptsResolved === numScriptTags) { - indexHtml.content = serializeDocument(document); - callback(); + numScriptsResolved += 1; + if (numScriptsResolved === numScriptTags) { + indexHtml.content = serializeDocument(document); + callback(); + } } - }); + ); } jsdom.env(indexHtml.content, (innerErr, window) => { @@ -208,8 +247,9 @@ function bundleExternalLibs(project, zip, callback) { function buildZip(project, req, res) { const zip = archiver('zip'); - const rootFile = project.files.find(file => file.name === 'root'); - const numFiles = project.files.filter(file => file.fileType !== 'folder').length; + const rootFile = project.files.find((file) => file.name === 'root'); + const numFiles = project.files.filter((file) => file.fileType !== 'folder') + .length; const { files } = project; let numCompletedFiles = 0; @@ -219,26 +259,31 @@ function buildZip(project, req, res) { const currentTime = format(new Date(), 'yyyy_MM_dd_HH_mm_ss'); project.slug = slugify(project.name, '_'); - res.attachment(`${generateFileSystemSafeName(project.slug)}_${currentTime}.zip`); + res.attachment( + `${generateFileSystemSafeName(project.slug)}_${currentTime}.zip` + ); zip.pipe(res); function addFileToZip(file, path) { if (file.fileType === 'folder') { const newPath = file.name === 'root' ? path : `${path}${file.name}/`; file.children.forEach((fileId) => { - const childFile = files.find(f => f.id === fileId); + const childFile = files.find((f) => f.id === fileId); (() => { addFileToZip(childFile, newPath); })(); }); } else if (file.url) { - request({ method: 'GET', url: file.url, encoding: null }, (err, response, body) => { - zip.append(body, { name: `${path}${file.name}` }); - numCompletedFiles += 1; - if (numCompletedFiles === numFiles) { - zip.finalize(); + request( + { method: 'GET', url: file.url, encoding: null }, + (err, response, body) => { + zip.append(body, { name: `${path}${file.name}` }); + numCompletedFiles += 1; + if (numCompletedFiles === numFiles) { + zip.finalize(); + } } - }); + ); } else { zip.append(file.content, { name: `${path}${file.name}` }); numCompletedFiles += 1; diff --git a/server/controllers/project.controller/__test__/createProject.test.js b/server/controllers/project.controller/__test__/createProject.test.js index 68025a0cb5..393e5eccf7 100644 --- a/server/controllers/project.controller/__test__/createProject.test.js +++ b/server/controllers/project.controller/__test__/createProject.test.js @@ -3,7 +3,10 @@ */ import { Response } from 'jest-express'; -import Project, { createMock, createInstanceMock } from '../../../models/project'; +import Project, { + createMock, + createInstanceMock +} from '../../../models/project'; import createProject, { apiCreateProject } from '../createProject'; jest.mock('../../../models/project'); @@ -23,9 +26,7 @@ describe('project.controller', () => { it('fails if create fails', (done) => { const error = new Error('An error'); - ProjectMock - .expects('create') - .rejects(error); + ProjectMock.expects('create').rejects(error); const request = { user: {} }; const response = new Response(); @@ -51,9 +52,7 @@ describe('project.controller', () => { }; const response = new Response(); - - ProjectMock - .expects('create') + ProjectMock.expects('create') .withArgs({ user: 'abc123', name: 'Wriggly worm', @@ -92,13 +91,11 @@ describe('project.controller', () => { user: {} }; - ProjectMock - .expects('create') + ProjectMock.expects('create') .withArgs({ user: 'abc123' }) .resolves(result); - ProjectMock - .expects('populate') + ProjectMock.expects('populate') .withArgs(result) .yields(null, resultWithUser) .resolves(resultWithUser); @@ -132,14 +129,9 @@ describe('project.controller', () => { const error = new Error('An error'); - ProjectMock - .expects('create') - .resolves(result); + ProjectMock.expects('create').resolves(result); - ProjectMock - .expects('populate') - .yields(error) - .resolves(error); + ProjectMock.expects('populate').yields(error).resolves(error); const promise = createProject(request, response); @@ -186,11 +178,12 @@ describe('project.controller', () => { files: [] }; - ProjectInstanceMock.expects('isSlugUnique') - .resolves({ isUnique: true, conflictingIds: [] }); + ProjectInstanceMock.expects('isSlugUnique').resolves({ + isUnique: true, + conflictingIds: [] + }); - ProjectInstanceMock.expects('save') - .resolves(new Project(result)); + ProjectInstanceMock.expects('save').resolves(new Project(result)); const promise = apiCreateProject(request, response); @@ -231,11 +224,12 @@ describe('project.controller', () => { files: [] }; - ProjectInstanceMock.expects('isSlugUnique') - .resolves({ isUnique: false, conflictingIds: ['cde123'] }); + ProjectInstanceMock.expects('isSlugUnique').resolves({ + isUnique: false, + conflictingIds: ['cde123'] + }); - ProjectInstanceMock.expects('save') - .resolves(new Project(result)); + ProjectInstanceMock.expects('save').resolves(new Project(result)); const promise = apiCreateProject(request, response); @@ -260,7 +254,7 @@ describe('project.controller', () => { const request = { user: { _id: 'abc123', username: 'alice' }, params: { - username: 'dana', + username: 'dana' }, body: { name: 'My sketch', @@ -278,11 +272,12 @@ describe('project.controller', () => { files: [] }; - ProjectInstanceMock.expects('isSlugUnique') - .resolves({ isUnique: true, conflictingIds: [] }); + ProjectInstanceMock.expects('isSlugUnique').resolves({ + isUnique: true, + conflictingIds: [] + }); - ProjectInstanceMock.expects('save') - .resolves(new Project(result)); + ProjectInstanceMock.expects('save').resolves(new Project(result)); const promise = apiCreateProject(request, response); @@ -323,7 +318,7 @@ describe('project.controller', () => { expect(responseBody.detail).not.toBeNull(); expect(responseBody.errors.length).toBe(1); expect(responseBody.errors).toEqual([ - { name: 'index.html', message: 'missing \'url\' or \'content\'' } + { name: 'index.html', message: "missing 'url' or 'content'" } ]); done(); @@ -352,7 +347,7 @@ describe('project.controller', () => { expect(response.status).toHaveBeenCalledWith(422); expect(responseBody.message).toBe('File Validation Failed'); - expect(responseBody.detail).toBe('\'files\' must be an object'); + expect(responseBody.detail).toBe("'files' must be an object"); done(); } @@ -365,7 +360,7 @@ describe('project.controller', () => { user: { _id: 'abc123', username: 'alice' }, params: { username: 'alice' }, body: { - name: 'Wriggly worm', + name: 'Wriggly worm' // files: {} is missing } }; @@ -380,7 +375,7 @@ describe('project.controller', () => { expect(response.status).toHaveBeenCalledWith(422); expect(responseBody.message).toBe('File Validation Failed'); - expect(responseBody.detail).toBe('\'files\' must be an object'); + expect(responseBody.detail).toBe("'files' must be an object"); done(); } diff --git a/server/controllers/project.controller/__test__/deleteProject.test.js b/server/controllers/project.controller/__test__/deleteProject.test.js index 84a6824fe8..ec03debab9 100644 --- a/server/controllers/project.controller/__test__/deleteProject.test.js +++ b/server/controllers/project.controller/__test__/deleteProject.test.js @@ -3,12 +3,14 @@ */ import { Request, Response } from 'jest-express'; -import Project, { createMock, createInstanceMock } from '../../../models/project'; +import Project, { + createMock, + createInstanceMock +} from '../../../models/project'; import User from '../../../models/user'; import deleteProject from '../../project.controller/deleteProject'; import { deleteObjectsFromS3 } from '../../aws.controller'; - jest.mock('../../../models/project'); jest.mock('../../aws.controller'); @@ -38,9 +40,7 @@ describe('project.controller', () => { const response = new Response(); - ProjectMock - .expects('findById') - .resolves(project); + ProjectMock.expects('findById').resolves(project); const promise = deleteProject(request, response); @@ -67,9 +67,7 @@ describe('project.controller', () => { const response = new Response(); - ProjectMock - .expects('findById') - .resolves(null); + ProjectMock.expects('findById').resolves(null); const promise = deleteProject(request, response); @@ -96,12 +94,9 @@ describe('project.controller', () => { const response = new Response(); - ProjectMock - .expects('findById') - .resolves(project); + ProjectMock.expects('findById').resolves(project); - ProjectInstanceMock.expects('remove') - .yields(); + ProjectInstanceMock.expects('remove').yields(); const promise = deleteProject(request, response); diff --git a/server/controllers/project.controller/__test__/getProjectsForUser.test.js b/server/controllers/project.controller/__test__/getProjectsForUser.test.js index 05a0cf7166..121a2184fd 100644 --- a/server/controllers/project.controller/__test__/getProjectsForUser.test.js +++ b/server/controllers/project.controller/__test__/getProjectsForUser.test.js @@ -4,7 +4,9 @@ import { Request, Response } from 'jest-express'; import { createMock } from '../../../models/user'; -import getProjectsForUser, { apiGetProjectsForUser } from '../../project.controller/getProjectsForUser'; +import getProjectsForUser, { + apiGetProjectsForUser +} from '../../project.controller/getProjectsForUser'; jest.mock('../../../models/user'); jest.mock('../../aws.controller'); @@ -43,8 +45,7 @@ describe('project.controller', () => { request.setParams({ username: 'abc123' }); const response = new Response(); - UserMock - .expects('findOne') + UserMock.expects('findOne') .withArgs({ username: 'abc123' }) .yields(null, null); @@ -52,7 +53,9 @@ describe('project.controller', () => { function expectations() { expect(response.status).toHaveBeenCalledWith(404); - expect(response.json).toHaveBeenCalledWith({ message: 'User with that username does not exist.' }); + expect(response.json).toHaveBeenCalledWith({ + message: 'User with that username does not exist.' + }); done(); } @@ -65,8 +68,7 @@ describe('project.controller', () => { request.setParams({ username: 'abc123' }); const response = new Response(); - UserMock - .expects('findOne') + UserMock.expects('findOne') .withArgs({ username: 'abc123' }) .yields(new Error(), null); @@ -74,7 +76,9 @@ describe('project.controller', () => { function expectations() { expect(response.status).toHaveBeenCalledWith(500); - expect(response.json).toHaveBeenCalledWith({ message: 'Error fetching projects' }); + expect(response.json).toHaveBeenCalledWith({ + message: 'Error fetching projects' + }); done(); } @@ -103,14 +107,12 @@ describe('project.controller', () => { promise.then(expectations, expectations).catch(expectations); }); - it('returns 404 if user does not exist', (done) => { const request = new Request(); request.setParams({ username: 'abc123' }); const response = new Response(); - UserMock - .expects('findOne') + UserMock.expects('findOne') .withArgs({ username: 'abc123' }) .yields(null, null); @@ -118,7 +120,9 @@ describe('project.controller', () => { function expectations() { expect(response.status).toHaveBeenCalledWith(404); - expect(response.json).toHaveBeenCalledWith({ message: 'User with that username does not exist.' }); + expect(response.json).toHaveBeenCalledWith({ + message: 'User with that username does not exist.' + }); done(); } @@ -131,8 +135,7 @@ describe('project.controller', () => { request.setParams({ username: 'abc123' }); const response = new Response(); - UserMock - .expects('findOne') + UserMock.expects('findOne') .withArgs({ username: 'abc123' }) .yields(new Error(), null); @@ -140,7 +143,9 @@ describe('project.controller', () => { function expectations() { expect(response.status).toHaveBeenCalledWith(500); - expect(response.json).toHaveBeenCalledWith({ message: 'Error fetching projects' }); + expect(response.json).toHaveBeenCalledWith({ + message: 'Error fetching projects' + }); done(); } diff --git a/server/controllers/project.controller/createProject.js b/server/controllers/project.controller/createProject.js index 74529c0d31..5db6f3ceb6 100644 --- a/server/controllers/project.controller/createProject.js +++ b/server/controllers/project.controller/createProject.js @@ -1,5 +1,9 @@ import Project from '../../models/project'; -import { toModel, FileValidationError, ProjectValidationError } from '../../domain-objects/Project'; +import { + toModel, + FileValidationError, + ProjectValidationError +} from '../../domain-objects/Project'; export default function createProject(req, res) { let projectValues = { @@ -26,7 +30,6 @@ export default function createProject(req, res) { ); } - return Project.create(projectValues) .then(populateUserData) .catch(sendFailure); @@ -40,7 +43,7 @@ export function apiCreateProject(req, res) { res.status(code).json({ message: `${type} Validation Failed`, detail: err.message, - errors: err.files, + errors: err.files }); } @@ -62,7 +65,9 @@ export function apiCreateProject(req, res) { function checkUserHasPermission() { if (req.user.username !== req.params.username) { console.log('no permission'); - const error = new ProjectValidationError(`'${req.user.username}' does not have permission to create for '${req.params.username}'`); + const error = new ProjectValidationError( + `'${req.user.username}' does not have permission to create for '${req.params.username}'` + ); error.code = 401; throw error; @@ -74,16 +79,20 @@ export function apiCreateProject(req, res) { const model = toModel(params); - return model.isSlugUnique() + return model + .isSlugUnique() .then(({ isUnique, conflictingIds }) => { if (isUnique) { - return model.save() - .then((newProject) => { - res.status(201).json({ id: newProject.id }); - }); + return model.save().then((newProject) => { + res.status(201).json({ id: newProject.id }); + }); } - const error = new ProjectValidationError(`Slug "${model.slug}" is not unique. Check ${conflictingIds.join(', ')}`); + const error = new ProjectValidationError( + `Slug "${model.slug}" is not unique. Check ${conflictingIds.join( + ', ' + )}` + ); error.code = 409; throw error; diff --git a/server/controllers/project.controller/deleteProject.js b/server/controllers/project.controller/deleteProject.js index 1a1bd65d4c..92e4e85168 100644 --- a/server/controllers/project.controller/deleteProject.js +++ b/server/controllers/project.controller/deleteProject.js @@ -3,20 +3,27 @@ import Project from '../../models/project'; import { deleteObjectsFromS3, getObjectKey } from '../aws.controller'; import createApplicationErrorClass from '../../utils/createApplicationErrorClass'; -const ProjectDeletionError = createApplicationErrorClass('ProjectDeletionError'); +const ProjectDeletionError = createApplicationErrorClass( + 'ProjectDeletionError' +); function deleteFilesFromS3(files) { - deleteObjectsFromS3(files.filter((file) => { - if (file.url) { - if (!process.env.S3_DATE || ( - process.env.S3_DATE && - isBefore(new Date(process.env.S3_DATE), new Date(file.createdAt)))) { - return true; - } - } - return false; - }) - .map(file => getObjectKey(file.url))); + deleteObjectsFromS3( + files + .filter((file) => { + if (file.url) { + if ( + !process.env.S3_DATE || + (process.env.S3_DATE && + isBefore(new Date(process.env.S3_DATE), new Date(file.createdAt))) + ) { + return true; + } + } + return false; + }) + .map((file) => getObjectKey(file.url)) + ); } export default function deleteProject(req, res) { @@ -25,7 +32,11 @@ export default function deleteProject(req, res) { } function sendProjectNotFound() { - sendFailure(new ProjectDeletionError('Project with that id does not exist', { code: 404 })); + sendFailure( + new ProjectDeletionError('Project with that id does not exist', { + code: 404 + }) + ); } function handleProjectDeletion(project) { @@ -35,7 +46,12 @@ export default function deleteProject(req, res) { } if (!project.user.equals(req.user._id)) { - sendFailure(new ProjectDeletionError('Authenticated user does not match owner of project', { code: 403 })); + sendFailure( + new ProjectDeletionError( + 'Authenticated user does not match owner of project', + { code: 403 } + ) + ); return; } diff --git a/server/controllers/project.controller/getProjectsForUser.js b/server/controllers/project.controller/getProjectsForUser.js index 7b563b2c8e..5579559915 100644 --- a/server/controllers/project.controller/getProjectsForUser.js +++ b/server/controllers/project.controller/getProjectsForUser.js @@ -36,10 +36,12 @@ function getProjectsForUserName(username) { export default function getProjectsForUser(req, res) { if (req.params.username) { return getProjectsForUserName(req.params.username) - .then(projects => res.json(projects)) + .then((projects) => res.json(projects)) .catch((err) => { if (err instanceof UserNotFoundError) { - res.status(404).json({ message: 'User with that username does not exist.' }); + res + .status(404) + .json({ message: 'User with that username does not exist.' }); } else { res.status(500).json({ message: 'Error fetching projects' }); } @@ -55,12 +57,14 @@ export function apiGetProjectsForUser(req, res) { if (req.params.username) { return getProjectsForUserName(req.params.username) .then((projects) => { - const asApiObjects = projects.map(p => toApiProjectObject(p)); + const asApiObjects = projects.map((p) => toApiProjectObject(p)); res.json({ sketches: asApiObjects }); }) .catch((err) => { if (err instanceof UserNotFoundError) { - res.status(404).json({ message: 'User with that username does not exist.' }); + res + .status(404) + .json({ message: 'User with that username does not exist.' }); } else { res.status(500).json({ message: 'Error fetching projects' }); } diff --git a/server/controllers/session.controller.js b/server/controllers/session.controller.js index bd4a252a64..55f116cb00 100644 --- a/server/controllers/session.controller.js +++ b/server/controllers/session.controller.js @@ -3,14 +3,19 @@ import passport from 'passport'; import { userResponse } from './user.controller'; export function createSession(req, res, next) { - passport.authenticate('local', (err, user) => { // eslint-disable-line consistent-return - if (err) { return next(err); } + passport.authenticate('local', (err, user) => { + // eslint-disable-line consistent-return + if (err) { + return next(err); + } if (!user) { return res.status(401).json({ message: 'Invalid username or password.' }); } req.logIn(user, (innerErr) => { - if (innerErr) { return next(innerErr); } + if (innerErr) { + return next(innerErr); + } return res.json(userResponse(req.user)); }); })(req, res, next); @@ -27,4 +32,3 @@ export function destroySession(req, res) { req.logout(); res.json({ success: true }); } - diff --git a/server/controllers/user.controller.js b/server/controllers/user.controller.js index dac4015dce..19d5b6a823 100644 --- a/server/controllers/user.controller.js +++ b/server/controllers/user.controller.js @@ -3,10 +3,7 @@ import async from 'async'; import User from '../models/user'; import mail from '../utils/mail'; -import { - renderEmailConfirmation, - renderResetPassword, -} from '../views/mail'; +import { renderEmailConfirmation, renderResetPassword } from '../views/mail'; export * from './user.controller/apiKey'; @@ -41,7 +38,7 @@ export function createUser(req, res, next) { const { username, email } = req.body; const { password } = req.body; const emailLowerCase = email.toLowerCase(); - const EMAIL_VERIFY_TOKEN_EXPIRY_TIME = Date.now() + (3600000 * 24); // 24 hours + const EMAIL_VERIFY_TOKEN_EXPIRY_TIME = Date.now() + 3600000 * 24; // 24 hours random((tokenError, token) => { const user = new User({ username, @@ -49,7 +46,7 @@ export function createUser(req, res, next) { password, verified: User.EmailConfirmation.Sent, verifiedToken: token, - verifiedTokenExpires: EMAIL_VERIFY_TOKEN_EXPIRY_TIME, + verifiedTokenExpires: EMAIL_VERIFY_TOKEN_EXPIRY_TIME }); User.findByEmailAndUsername(email, username, (err, existingUser) => { @@ -59,7 +56,10 @@ export function createUser(req, res, next) { } if (existingUser) { - const fieldInUse = existingUser.email.toLowerCase() === emailLowerCase ? 'Email' : 'Username'; + const fieldInUse = + existingUser.email.toLowerCase() === emailLowerCase + ? 'Email' + : 'Username'; res.status(422).send({ error: `${fieldInUse} is in use` }); return; } @@ -74,16 +74,18 @@ export function createUser(req, res, next) { return; } - const protocol = process.env.NODE_ENV === 'production' ? 'https' : 'http'; + const protocol = + process.env.NODE_ENV === 'production' ? 'https' : 'http'; const mailOptions = renderEmailConfirmation({ body: { domain: `${protocol}://${req.headers.host}`, link: `${protocol}://${req.headers.host}/verify?t=${token}` }, - to: req.user.email, + to: req.user.email }); - mail.send(mailOptions, (mailErr, result) => { // eslint-disable-line no-unused-vars + mail.send(mailOptions, (mailErr, result) => { + // eslint-disable-line no-unused-vars res.json(userResponse(req.user)); }); }); @@ -122,7 +124,11 @@ export function updatePreferences(req, res) { return; } - const preferences = Object.assign({}, user.preferences, req.body.preferences); + const preferences = Object.assign( + {}, + user.preferences, + req.body.preferences + ); user.preferences = preferences; user.save((saveErr) => { @@ -137,52 +143,73 @@ export function updatePreferences(req, res) { } export function resetPasswordInitiate(req, res) { - async.waterfall([ - random, - (token, done) => { - User.findByEmail(req.body.email, (err, user) => { - if (!user) { - res.json({ success: true, message: 'If the email is registered with the editor, an email has been sent.' }); - return; - } - user.resetPasswordToken = token; - user.resetPasswordExpires = Date.now() + 3600000; // 1 hour + async.waterfall( + [ + random, + (token, done) => { + User.findByEmail(req.body.email, (err, user) => { + if (!user) { + res.json({ + success: true, + message: + 'If the email is registered with the editor, an email has been sent.' + }); + return; + } + user.resetPasswordToken = token; + user.resetPasswordExpires = Date.now() + 3600000; // 1 hour - user.save((saveErr) => { - done(saveErr, token, user); + user.save((saveErr) => { + done(saveErr, token, user); + }); + }); + }, + (token, user, done) => { + const protocol = + process.env.NODE_ENV === 'production' ? 'https' : 'http'; + const mailOptions = renderResetPassword({ + body: { + domain: `${protocol}://${req.headers.host}`, + link: `${protocol}://${req.headers.host}/reset-password/${token}` + }, + to: user.email }); - }); - }, - (token, user, done) => { - const protocol = process.env.NODE_ENV === 'production' ? 'https' : 'http'; - const mailOptions = renderResetPassword({ - body: { - domain: `${protocol}://${req.headers.host}`, - link: `${protocol}://${req.headers.host}/reset-password/${token}`, - }, - to: user.email, - }); - mail.send(mailOptions, done); - } - ], (err) => { - if (err) { - console.log(err); - res.json({ success: false }); - return; + mail.send(mailOptions, done); + } + ], + (err) => { + if (err) { + console.log(err); + res.json({ success: false }); + return; + } + res.json({ + success: true, + message: + 'If the email is registered with the editor, an email has been sent.' + }); } - res.json({ success: true, message: 'If the email is registered with the editor, an email has been sent.' }); - }); + ); } export function validateResetPasswordToken(req, res) { - User.findOne({ resetPasswordToken: req.params.token, resetPasswordExpires: { $gt: Date.now() } }, (err, user) => { - if (!user) { - res.status(401).json({ success: false, message: 'Password reset token is invalid or has expired.' }); - return; + User.findOne( + { + resetPasswordToken: req.params.token, + resetPasswordExpires: { $gt: Date.now() } + }, + (err, user) => { + if (!user) { + res.status(401).json({ + success: false, + message: 'Password reset token is invalid or has expired.' + }); + return; + } + res.json({ success: true }); } - res.json({ success: true }); - }); + ); } export function emailVerificationInitiate(req, res) { @@ -204,20 +231,22 @@ export function emailVerificationInitiate(req, res) { return; } - const protocol = process.env.NODE_ENV === 'production' ? 'https' : 'http'; + const protocol = + process.env.NODE_ENV === 'production' ? 'https' : 'http'; const mailOptions = renderEmailConfirmation({ body: { domain: `${protocol}://${req.headers.host}`, link: `${protocol}://${req.headers.host}/verify?t=${token}` }, - to: user.email, + to: user.email }); - mail.send(mailOptions, (mailErr, result) => { // eslint-disable-line no-unused-vars + mail.send(mailOptions, (mailErr, result) => { + // eslint-disable-line no-unused-vars if (mailErr != null) { res.status(500).send({ error: 'Error sending mail' }); } else { - const EMAIL_VERIFY_TOKEN_EXPIRY_TIME = Date.now() + (3600000 * 24); // 24 hours + const EMAIL_VERIFY_TOKEN_EXPIRY_TIME = Date.now() + 3600000 * 24; // 24 hours user.verified = User.EmailConfirmation.Resent; user.verifiedToken = token; user.verifiedTokenExpires = EMAIL_VERIFY_TOKEN_EXPIRY_TIME; // 24 hours @@ -227,52 +256,67 @@ export function emailVerificationInitiate(req, res) { } }); }); - }, + } ]); } export function verifyEmail(req, res) { const token = req.query.t; - User.findOne({ verifiedToken: token, verifiedTokenExpires: { $gt: new Date() } }, (err, user) => { - if (!user) { - res.status(401).json({ success: false, message: 'Token is invalid or has expired.' }); - return; - } + User.findOne( + { verifiedToken: token, verifiedTokenExpires: { $gt: new Date() } }, + (err, user) => { + if (!user) { + res.status(401).json({ + success: false, + message: 'Token is invalid or has expired.' + }); + return; + } - user.verified = User.EmailConfirmation.Verified; - user.verifiedToken = null; - user.verifiedTokenExpires = null; - user.save() - .then((result) => { // eslint-disable-line + user.verified = User.EmailConfirmation.Verified; + user.verifiedToken = null; + user.verifiedTokenExpires = null; + user.save().then((result) => { + // eslint-disable-line res.json({ success: true }); }); - }); + } + ); } export function updatePassword(req, res) { - User.findOne({ resetPasswordToken: req.params.token, resetPasswordExpires: { $gt: Date.now() } }, (err, user) => { - if (!user) { - res.status(401).json({ success: false, message: 'Password reset token is invalid or has expired.' }); - return; - } + User.findOne( + { + resetPasswordToken: req.params.token, + resetPasswordExpires: { $gt: Date.now() } + }, + (err, user) => { + if (!user) { + res.status(401).json({ + success: false, + message: 'Password reset token is invalid or has expired.' + }); + return; + } - user.password = req.body.password; - user.resetPasswordToken = undefined; - user.resetPasswordExpires = undefined; + user.password = req.body.password; + user.resetPasswordToken = undefined; + user.resetPasswordExpires = undefined; - user.save((saveErr) => { - req.logIn(user, loginErr => res.json(userResponse(req.user))); - }); - }); + user.save((saveErr) => { + req.logIn(user, (loginErr) => res.json(userResponse(req.user))); + }); + } + ); // eventually send email that the password has been reset } export function userExists(username, callback) { - User.findByUsername(username, (err, user) => ( + User.findByUsername(username, (err, user) => user ? callback(true) : callback(false) - )); + ); } export function saveUser(res, user) { @@ -310,7 +354,7 @@ export function updateSettings(req, res) { saveUser(res, user); }); } else if (user.email !== req.body.email) { - const EMAIL_VERIFY_TOKEN_EXPIRY_TIME = Date.now() + (3600000 * 24); // 24 hours + const EMAIL_VERIFY_TOKEN_EXPIRY_TIME = Date.now() + 3600000 * 24; // 24 hours user.verified = User.EmailConfirmation.Sent; user.email = req.body.email; @@ -321,13 +365,14 @@ export function updateSettings(req, res) { saveUser(res, user); - const protocol = process.env.NODE_ENV === 'production' ? 'https' : 'http'; + const protocol = + process.env.NODE_ENV === 'production' ? 'https' : 'http'; const mailOptions = renderEmailConfirmation({ body: { domain: `${protocol}://${req.headers.host}`, link: `${protocol}://${req.headers.host}/verify?t=${token}` }, - to: user.email, + to: user.email }); mail.send(mailOptions); @@ -341,20 +386,29 @@ export function updateSettings(req, res) { export function unlinkGithub(req, res) { if (req.user) { req.user.github = undefined; - req.user.tokens = req.user.tokens.filter(token => token.kind !== 'github'); + req.user.tokens = req.user.tokens.filter( + (token) => token.kind !== 'github' + ); saveUser(res, req.user); return; } - res.status(404).json({ success: false, message: 'You must be logged in to complete this action.' }); + res.status(404).json({ + success: false, + message: 'You must be logged in to complete this action.' + }); } export function unlinkGoogle(req, res) { if (req.user) { req.user.google = undefined; - req.user.tokens = req.user.tokens.filter(token => token.kind !== 'google'); + req.user.tokens = req.user.tokens.filter( + (token) => token.kind !== 'google' + ); saveUser(res, req.user); return; } - res.status(404).json({ success: false, message: 'You must be logged in to complete this action.' }); + res.status(404).json({ + success: false, + message: 'You must be logged in to complete this action.' + }); } - diff --git a/server/controllers/user.controller/__tests__/apiKey.test.js b/server/controllers/user.controller/__tests__/apiKey.test.js index 7e396288b0..59e9e8e554 100644 --- a/server/controllers/user.controller/__tests__/apiKey.test.js +++ b/server/controllers/user.controller/__tests__/apiKey.test.js @@ -22,16 +22,12 @@ describe('user.controller', () => { UserInstanceMock.restore(); }); - describe('createApiKey', () => { - it('returns an error if user doesn\'t exist', () => { + it("returns an error if user doesn't exist", () => { const request = { user: { id: '1234' } }; const response = new Response(); - UserMock - .expects('findById') - .withArgs('1234') - .yields(null, null); + UserMock.expects('findById').withArgs('1234').yields(null, null); createApiKey(request, response); @@ -45,16 +41,13 @@ describe('user.controller', () => { const request = { user: { id: '1234' }, body: {} }; const response = new Response(); - UserMock - .expects('findById') - .withArgs('1234') - .yields(null, new User()); + UserMock.expects('findById').withArgs('1234').yields(null, new User()); createApiKey(request, response); expect(response.status).toHaveBeenCalledWith(400); expect(response.json).toHaveBeenCalledWith({ - error: 'Expected field \'label\' was not present in request body' + error: "Expected field 'label' was not present in request body" }); }); @@ -67,13 +60,9 @@ describe('user.controller', () => { const user = new User(); - UserMock - .expects('findById') - .withArgs('1234') - .yields(null, user); + UserMock.expects('findById').withArgs('1234').yields(null, user); - UserInstanceMock.expects('save') - .yields(); + UserInstanceMock.expects('save').yields(); function expectations() { const lastKey = last(user.apiKeys); @@ -101,14 +90,11 @@ describe('user.controller', () => { }); describe('removeApiKey', () => { - it('returns an error if user doesn\'t exist', () => { + it("returns an error if user doesn't exist", () => { const request = { user: { id: '1234' } }; const response = new Response(); - UserMock - .expects('findById') - .withArgs('1234') - .yields(null, null); + UserMock.expects('findById').withArgs('1234').yields(null, null); removeApiKey(request, response); @@ -118,7 +104,7 @@ describe('user.controller', () => { }); }); - it('returns an error if specified key doesn\'t exist', () => { + it("returns an error if specified key doesn't exist", () => { const request = { user: { id: '1234' }, params: { keyId: 'not-a-real-key' } @@ -126,10 +112,7 @@ describe('user.controller', () => { const response = new Response(); const user = new User(); - UserMock - .expects('findById') - .withArgs('1234') - .yields(null, user); + UserMock.expects('findById').withArgs('1234').yields(null, user); removeApiKey(request, response); @@ -153,14 +136,9 @@ describe('user.controller', () => { }; const response = new Response(); - UserMock - .expects('findById') - .withArgs('1234') - .yields(null, user); + UserMock.expects('findById').withArgs('1234').yields(null, user); - UserInstanceMock - .expects('save') - .yields(); + UserInstanceMock.expects('save').yields(); removeApiKey(request, response); diff --git a/server/controllers/user.controller/apiKey.js b/server/controllers/user.controller/apiKey.js index 5d45b123e2..8ac26e2b0f 100644 --- a/server/controllers/user.controller/apiKey.js +++ b/server/controllers/user.controller/apiKey.js @@ -32,13 +32,19 @@ export function createApiKey(req, res) { } if (!req.body.label) { - sendFailure(400, 'Expected field \'label\' was not present in request body'); + sendFailure( + 400, + "Expected field 'label' was not present in request body" + ); return; } const keyToBeHashed = await generateApiKey(); - const addedApiKeyIndex = user.apiKeys.push({ label: req.body.label, hashedKey: keyToBeHashed }); + const addedApiKeyIndex = user.apiKeys.push({ + label: req.body.label, + hashedKey: keyToBeHashed + }); user.save((saveErr) => { if (saveErr) { @@ -46,15 +52,14 @@ export function createApiKey(req, res) { return; } - const apiKeys = user.apiKeys - .map((apiKey, index) => { - const fields = apiKey.toObject(); - const shouldIncludeToken = index === addedApiKeyIndex - 1; + const apiKeys = user.apiKeys.map((apiKey, index) => { + const fields = apiKey.toObject(); + const shouldIncludeToken = index === addedApiKeyIndex - 1; - return shouldIncludeToken ? - { ...fields, token: keyToBeHashed } : - fields; - }); + return shouldIncludeToken + ? { ...fields, token: keyToBeHashed } + : fields; + }); res.json({ apiKeys }); resolve(); @@ -81,7 +86,9 @@ export function removeApiKey(req, res) { return; } - const keyToDelete = user.apiKeys.find(key => key.id === req.params.keyId); + const keyToDelete = user.apiKeys.find( + (key) => key.id === req.params.keyId + ); if (!keyToDelete) { sendFailure(404, 'Key does not exist for user'); return; diff --git a/server/domain-objects/Project.js b/server/domain-objects/Project.js index f0de023f2a..ec2618e903 100644 --- a/server/domain-objects/Project.js +++ b/server/domain-objects/Project.js @@ -5,8 +5,12 @@ import createId from '../utils/createId'; import createApplicationErrorClass from '../utils/createApplicationErrorClass'; import createDefaultFiles from './createDefaultFiles'; -export const FileValidationError = createApplicationErrorClass('FileValidationError'); -export const ProjectValidationError = createApplicationErrorClass('ProjectValidationError'); +export const FileValidationError = createApplicationErrorClass( + 'FileValidationError' +); +export const ProjectValidationError = createApplicationErrorClass( + 'ProjectValidationError' +); /** * This converts between a mongoose Project model @@ -16,7 +20,7 @@ export const ProjectValidationError = createApplicationErrorClass('ProjectValida export function toApi(model) { return { id: model.id, - name: model.name, + name: model.name }; } @@ -63,7 +67,7 @@ function transformFilesInner(tree = {}, parentNode) { } else if (typeof params.content === 'string') { file.content = params.content; } else { - errors.push({ name, message: 'missing \'url\' or \'content\'' }); + errors.push({ name, message: "missing 'url' or 'content'" }); } files.push(file); @@ -89,10 +93,12 @@ export function transformFiles(tree = {}) { const { files, errors } = transformFilesInner(withRoot); if (errors.length > 0) { - const message = `${errors.length} files failed validation. See error.files for individual errors. + const message = `${ + errors.length + } files failed validation. See error.files for individual errors. Errors: - ${errors.map(e => `* ${e.name}: ${e.message}`).join('\n')} + ${errors.map((e) => `* ${e.name}: ${e.message}`).join('\n')} `; const error = new FileValidationError(message); error.files = errors; @@ -104,7 +110,7 @@ export function transformFiles(tree = {}) { } export function containsRootHtmlFile(tree) { - return Object.keys(tree).find(name => /\.html$/.test(name)) != null; + return Object.keys(tree).find((name) => /\.html$/.test(name)) != null; } /** @@ -123,7 +129,7 @@ export function toModel(object) { files = transformFiles(tree); } else { - throw new FileValidationError('\'files\' must be an object'); + throw new FileValidationError("'files' must be an object"); } const projectValues = pick(object, ['user', 'name', 'slug']); diff --git a/server/domain-objects/__test__/Project.test.js b/server/domain-objects/__test__/Project.test.js index 0f3c9dd979..1fbb7ff3b5 100644 --- a/server/domain-objects/__test__/Project.test.js +++ b/server/domain-objects/__test__/Project.test.js @@ -1,6 +1,11 @@ import find from 'lodash/find'; -import { containsRootHtmlFile, toModel, transformFiles, FileValidationError } from '../Project'; +import { + containsRootHtmlFile, + toModel, + transformFiles, + FileValidationError +} from '../Project'; jest.mock('../../utils/createId'); @@ -12,8 +17,12 @@ describe('domain-objects/Project', () => { it('returns true for at least one root .html', () => { expect(containsRootHtmlFile({ 'index.html': {} })).toBe(true); expect(containsRootHtmlFile({ 'another-one.html': {} })).toBe(true); - expect(containsRootHtmlFile({ 'one.html': {}, 'two.html': {} })).toBe(true); - expect(containsRootHtmlFile({ 'one.html': {}, 'sketch.js': {} })).toBe(true); + expect(containsRootHtmlFile({ 'one.html': {}, 'two.html': {} })).toBe( + true + ); + expect(containsRootHtmlFile({ 'one.html': {}, 'sketch.js': {} })).toBe( + true + ); }); it('returns false anything else', () => { @@ -21,13 +30,15 @@ describe('domain-objects/Project', () => { }); it('ignores nested html', () => { - expect(containsRootHtmlFile({ - examples: { - files: { - 'index.html': {} + expect( + containsRootHtmlFile({ + examples: { + files: { + 'index.html': {} + } } - } - })).toBe(false); + }) + ).toBe(false); }); }); @@ -131,18 +142,20 @@ describe('transformFiles', () => { it('creates an empty root with no data', () => { const tree = {}; - expect(transformFiles(tree)).toEqual([{ - _id: '0', - fileType: 'folder', - name: 'root', - children: [] - }]); + expect(transformFiles(tree)).toEqual([ + { + _id: '0', + fileType: 'folder', + name: 'root', + children: [] + } + ]); }); it('converts tree-shaped files into list', () => { const tree = { 'index.html': { - content: 'some contents', + content: 'some contents' } }; @@ -189,7 +202,7 @@ describe('transformFiles', () => { const tree = { 'a-folder': { files: {} - }, + } }; expect(transformFiles(tree)).toEqual([ @@ -211,7 +224,7 @@ describe('transformFiles', () => { it('walks the tree processing files', () => { const tree = { 'index.html': { - content: 'some contents', + content: 'some contents' }, 'a-folder': { files: { @@ -222,7 +235,7 @@ describe('transformFiles', () => { content: 'blah blah' } } - }, + } }; expect(transformFiles(tree)).toEqual([ @@ -275,7 +288,7 @@ describe('transformFiles', () => { } } } - }, + } }; expect(transformFiles(tree)).toEqual([ @@ -312,19 +325,18 @@ describe('transformFiles', () => { ]); }); - it('allows duplicate names in different folder', () => { const tree = { 'index.html': { - content: 'some contents', + content: 'some contents' }, - 'data': { + "data": { files: { 'index.html': { content: 'different file' } } - }, + } }; expect(transformFiles(tree)).toEqual([ @@ -377,8 +389,8 @@ describe('transformFiles', () => { } catch (err) { expect(err).toBeInstanceOf(FileValidationError); expect(err.files).toEqual([ - { name: 'index.html', message: 'missing \'url\' or \'content\'' }, - { name: 'something.js', message: 'missing \'url\' or \'content\'' } + { name: 'index.html', message: "missing 'url' or 'content'" }, + { name: 'something.js', message: "missing 'url' or 'content'" } ]); } }); diff --git a/server/domain-objects/createDefaultFiles.js b/server/domain-objects/createDefaultFiles.js index b901caaf70..b1455e0f68 100644 --- a/server/domain-objects/createDefaultFiles.js +++ b/server/domain-objects/createDefaultFiles.js @@ -6,8 +6,7 @@ function draw() { background(220); }`; -const defaultHTML = - `<!DOCTYPE html> +const defaultHTML = `<!DOCTYPE html> <html> <head> <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.1.9/p5.js"></script> @@ -22,8 +21,7 @@ const defaultHTML = </html> `; -const defaultCSS = - `html, body { +const defaultCSS = `html, body { margin: 0; padding: 0; } diff --git a/server/migrations/emailConsolidation.js b/server/migrations/emailConsolidation.js index 8343377c5f..aaae0386c2 100644 --- a/server/migrations/emailConsolidation.js +++ b/server/migrations/emailConsolidation.js @@ -3,19 +3,26 @@ import fs from 'fs'; import User from '../models/user'; import Project from '../models/project'; import Collection from '../models/collection'; -import { moveObjectToUserInS3, copyObjectInS3 } from '../controllers/aws.controller'; +import { + moveObjectToUserInS3, + copyObjectInS3 +} from '../controllers/aws.controller'; import mail from '../utils/mail'; import { renderAccountConsolidation } from '../views/mail'; - const mongoConnectionString = process.env.MONGO_URL; const { ObjectId } = mongoose.Types; // Connect to MongoDB mongoose.Promise = global.Promise; -mongoose.connect(mongoConnectionString, { useNewUrlParser: true, useUnifiedTopology: true }); +mongoose.connect(mongoConnectionString, { + useNewUrlParser: true, + useUnifiedTopology: true +}); mongoose.set('useCreateIndex', true); mongoose.connection.on('error', () => { - console.error('MongoDB Connection Error. Please make sure that MongoDB is running.'); + console.error( + 'MongoDB Connection Error. Please make sure that MongoDB is running.' + ); process.exit(1); }); @@ -28,32 +35,32 @@ const agg = [ { $project: { email: { - $toLower: [ - '$email' - ] + $toLower: ['$email'] } } - }, { + }, + { $group: { _id: '$email', total: { $sum: 1 } } - }, { + }, + { $match: { total: { $gt: 1 } } - }, { + }, + { $sort: { total: -1 } } ]; - // steps to make this work // iterate through the results // check if any files are on AWS @@ -89,30 +96,41 @@ fs.readFile('duplicates.json', async (err, file) => { }); async function consolidateAccount(email) { - return User.find({ email }).collation({ locale: 'en', strength: 2 }) - .sort({ createdAt: 1 }).exec() + return User.find({ email }) + .collation({ locale: 'en', strength: 2 }) + .sort({ createdAt: 1 }) + .exec() .then((result) => { [currentUser, ...duplicates] = result; console.log('Current User: ', currentUser._id, ' ', currentUser.email); - duplicates = duplicates.map(dup => dup._id); + duplicates = duplicates.map((dup) => dup._id); console.log('Duplicates: ', duplicates); return Project.find({ user: { $in: duplicates } }).exec(); - }).then((sketches) => { + }) + .then((sketches) => { const saveSketchPromises = []; sketches.forEach((sketch) => { console.log('SketchId: ', sketch._id); console.log('UserId: ', sketch.user); const moveSketchFilesPromises = []; sketch.files.forEach((file) => { - // if the file url contains sketch user - if (file.url && file.url.includes(process.env.S3_BUCKET_URL_BASE) && !file.url.includes(currentUser._id)) { + // if the file url contains sketch user + if ( + file.url && + file.url.includes(process.env.S3_BUCKET_URL_BASE) && + !file.url.includes(currentUser._id) + ) { if (file.url.includes(sketch.user)) { - const fileSavePromise = moveObjectToUserInS3(file.url, currentUser._id) + const fileSavePromise = moveObjectToUserInS3( + file.url, + currentUser._id + ) .then((newUrl) => { file.url = newUrl; - }).catch((err) => { + }) + .catch((err) => { console.log('Move Error:'); console.log(err); }); @@ -121,7 +139,8 @@ async function consolidateAccount(email) { const fileSavePromise = copyObjectInS3(file.url, currentUser._id) .then((newUrl) => { file.url = newUrl; - }).catch((err) => { + }) + .catch((err) => { console.log('Copy Error:'); console.log(err); }); @@ -129,27 +148,33 @@ async function consolidateAccount(email) { } } }); - const sketchSavePromise = Promise.all(moveSketchFilesPromises).then(() => { - sketch.user = ObjectId(currentUser._id); - return sketch.save(); - }); + const sketchSavePromise = Promise.all(moveSketchFilesPromises).then( + () => { + sketch.user = ObjectId(currentUser._id); + return sketch.save(); + } + ); saveSketchPromises.push(sketchSavePromise); }); return Promise.all(saveSketchPromises); - }).then(() => { + }) + .then(() => { console.log('Moved and updated all sketches.'); return Collection.updateMany( { owner: { $in: duplicates } }, { $set: { owner: ObjectId(currentUser.id) } } ); - }).then(() => { + }) + .then(() => { console.log('Moved and updated all collections.'); return User.deleteMany({ _id: { $in: duplicates } }); - }).then(() => { + }) + .then(() => { console.log('Deleted other user accounts.'); currentUser.email = currentUser.email.toLowerCase(); return currentUser.save(); - }).then(() => { + }) + .then(() => { console.log('Migrated email to lowercase.'); // const protocol = process.env.NODE_ENV === 'production' ? 'https' : 'http'; const mailOptions = renderAccountConsolidation({ @@ -158,7 +183,7 @@ async function consolidateAccount(email) { username: currentUser.username, email: currentUser.email }, - to: currentUser.email, + to: currentUser.email }); return new Promise((resolve, reject) => { @@ -170,13 +195,13 @@ async function consolidateAccount(email) { return resolve(result); }); }); - }).catch((err) => { + }) + .catch((err) => { console.log(err); process.exit(1); }); } - // let duplicates = [ // "5ce3d936e0f9df0022d8330c", // "5cff843f091745001e83c070", @@ -211,7 +236,6 @@ async function consolidateAccount(email) { // }); // }); - // import s3 from '@auth0/s3'; // const client = s3.createClient({ diff --git a/server/models/__mocks__/project.js b/server/models/__mocks__/project.js index 2590046f3c..7143fd0edd 100644 --- a/server/models/__mocks__/project.js +++ b/server/models/__mocks__/project.js @@ -18,12 +18,12 @@ export function createInstanceMock() { // See: https://stackoverflow.com/questions/40962960/sinon-mock-of-mongoose-save-method-for-all-future-instances-of-a-model-with-pro Object.defineProperty(Project.prototype, 'save', { value: Project.prototype.save, - configurable: true, + configurable: true }); Object.defineProperty(Project.prototype, 'remove', { value: Project.prototype.remove, - configurable: true, + configurable: true }); return sinon.mock(Project.prototype); diff --git a/server/models/__mocks__/user.js b/server/models/__mocks__/user.js index fcd73f32a1..d9741da0f2 100644 --- a/server/models/__mocks__/user.js +++ b/server/models/__mocks__/user.js @@ -18,13 +18,12 @@ export function createInstanceMock() { // See: https://stackoverflow.com/questions/40962960/sinon-mock-of-mongoose-save-method-for-all-future-instances-of-a-model-with-pro Object.defineProperty(User.prototype, 'save', { value: User.prototype.save, - configurable: true, + configurable: true }); return sinon.mock(User.prototype); } - // Re-export the model, it will be // altered by mockingoose whenever // we call methods on the MockConfig diff --git a/server/models/__test__/project.test.js b/server/models/__test__/project.test.js index 2d470bc484..3c4de698dc 100644 --- a/server/models/__test__/project.test.js +++ b/server/models/__test__/project.test.js @@ -3,7 +3,8 @@ import differenceInSeconds from 'date-fns/differenceInSeconds'; import Project from '../project'; -const datesWithinSeconds = (first, second) => differenceInSeconds(first, second) < 2; +const datesWithinSeconds = (first, second) => + differenceInSeconds(first, second) < 2; describe('models/project', () => { beforeEach(() => { @@ -96,6 +97,5 @@ describe('models/project', () => { }); }); - describe('fileSchema', () => { - }); + describe('fileSchema', () => {}); }); diff --git a/server/models/collection.js b/server/models/collection.js index bbd40ea4ee..066e9b90cc 100644 --- a/server/models/collection.js +++ b/server/models/collection.js @@ -6,7 +6,7 @@ const { Schema } = mongoose; const collectedProjectSchema = new Schema( { - project: { type: Schema.Types.ObjectId, ref: 'Project' }, + project: { type: Schema.Types.ObjectId, ref: 'Project' } }, { timestamps: true, _id: true, usePushEach: true } ); diff --git a/server/models/project.js b/server/models/project.js index 3581e99885..3244cc8cce 100644 --- a/server/models/project.js +++ b/server/models/project.js @@ -29,7 +29,11 @@ fileSchema.set('toJSON', { const projectSchema = new Schema( { - name: { type: String, default: "Hello p5.js, it's the server", maxlength: 128 }, + name: { + type: String, + default: "Hello p5.js, it's the server", + maxlength: 128 + }, user: { type: Schema.Types.ObjectId, ref: 'User' }, serveSecure: { type: Boolean, default: false }, files: { type: [fileSchema] }, @@ -65,13 +69,14 @@ projectSchema.methods.isSlugUnique = async function isSlugUnique(cb) { const hasCallback = typeof cb === 'function'; try { - const docsWithSlug = await project.model('Project') + const docsWithSlug = await project + .model('Project') .find({ user: project.user, slug: project.slug }, '_id') .exec(); const result = { isUnique: docsWithSlug.length === 0, - conflictingIds: docsWithSlug.map(d => d._id) || [] + conflictingIds: docsWithSlug.map((d) => d._id) || [] }; if (hasCallback) { diff --git a/server/models/user.js b/server/models/user.js index 8e25b9e675..a582ebf942 100644 --- a/server/models/user.js +++ b/server/models/user.js @@ -5,16 +5,19 @@ const bcrypt = require('bcrypt-nodejs'); const EmailConfirmationStates = { Verified: 'verified', Sent: 'sent', - Resent: 'resent', + Resent: 'resent' }; const { Schema } = mongoose; -const apiKeySchema = new Schema({ - label: { type: String, default: 'API Key' }, - lastUsedAt: { type: Date }, - hashedKey: { type: String, required: true }, -}, { timestamps: true, _id: true }); +const apiKeySchema = new Schema( + { + label: { type: String, default: 'API Key' }, + lastUsedAt: { type: Date }, + hashedKey: { type: String, required: true } + }, + { timestamps: true, _id: true } +); apiKeySchema.virtual('id').get(function getApiKeyId() { return this._id.toHexString(); @@ -24,10 +27,13 @@ apiKeySchema.virtual('id').get(function getApiKeyId() { * When serialising an APIKey instance, the `hashedKey` field * should never be exposed to the client. So we only return * a safe list of fields when toObject and toJSON are called. -*/ + */ function apiKeyMetadata(doc, ret, options) { return { - id: doc.id, label: doc.label, lastUsedAt: doc.lastUsedAt, createdAt: doc.createdAt + id: doc.id, + label: doc.label, + lastUsedAt: doc.lastUsedAt, + createdAt: doc.createdAt }; } @@ -40,49 +46,60 @@ apiKeySchema.set('toJSON', { transform: apiKeyMetadata }); -const userSchema = new Schema({ - name: { type: String, default: '' }, - username: { type: String, required: true, unique: true }, - password: { type: String }, - resetPasswordToken: String, - resetPasswordExpires: Date, - verified: { type: String }, - verifiedToken: String, - verifiedTokenExpires: Date, - github: { type: String }, - google: { type: String }, - email: { type: String, unique: true }, - tokens: Array, - apiKeys: { type: [apiKeySchema] }, - preferences: { - fontSize: { type: Number, default: 18 }, - lineNumbers: { type: Boolean, default: true }, - indentationAmount: { type: Number, default: 2 }, - isTabIndent: { type: Boolean, default: false }, - autosave: { type: Boolean, default: true }, - linewrap: { type: Boolean, default: true }, - lintWarning: { type: Boolean, default: false }, - textOutput: { type: Boolean, default: false }, - gridOutput: { type: Boolean, default: false }, - soundOutput: { type: Boolean, default: false }, - theme: { type: String, default: 'light' }, - autorefresh: { type: Boolean, default: false }, - language: { type: String, default: 'en-US' }, - autocloseBracketsQuotes: { type: Boolean, default: true } +const userSchema = new Schema( + { + name: { type: String, default: '' }, + username: { type: String, required: true, unique: true }, + password: { type: String }, + resetPasswordToken: String, + resetPasswordExpires: Date, + verified: { type: String }, + verifiedToken: String, + verifiedTokenExpires: Date, + github: { type: String }, + google: { type: String }, + email: { type: String, unique: true }, + tokens: Array, + apiKeys: { type: [apiKeySchema] }, + preferences: { + fontSize: { type: Number, default: 18 }, + lineNumbers: { type: Boolean, default: true }, + indentationAmount: { type: Number, default: 2 }, + isTabIndent: { type: Boolean, default: false }, + autosave: { type: Boolean, default: true }, + linewrap: { type: Boolean, default: true }, + lintWarning: { type: Boolean, default: false }, + textOutput: { type: Boolean, default: false }, + gridOutput: { type: Boolean, default: false }, + soundOutput: { type: Boolean, default: false }, + theme: { type: String, default: 'light' }, + autorefresh: { type: Boolean, default: false }, + language: { type: String, default: 'en-US' }, + autocloseBracketsQuotes: { type: Boolean, default: true } + }, + totalSize: { type: Number, default: 0 } }, - totalSize: { type: Number, default: 0 } -}, { timestamps: true, usePushEach: true }); + { timestamps: true, usePushEach: true } +); /** * Password hash middleware. */ -userSchema.pre('save', function checkPassword(next) { // eslint-disable-line consistent-return +userSchema.pre('save', function checkPassword(next) { + // eslint-disable-line consistent-return const user = this; - if (!user.isModified('password')) { return next(); } - bcrypt.genSalt(10, (err, salt) => { // eslint-disable-line consistent-return - if (err) { return next(err); } + if (!user.isModified('password')) { + return next(); + } + bcrypt.genSalt(10, (err, salt) => { + // eslint-disable-line consistent-return + if (err) { + return next(err); + } bcrypt.hash(user.password, salt, null, (innerErr, hash) => { - if (innerErr) { return next(innerErr); } + if (innerErr) { + return next(innerErr); + } user.password = hash; return next(); }); @@ -92,17 +109,25 @@ userSchema.pre('save', function checkPassword(next) { // eslint-disable-line con /** * API keys hash middleware */ -userSchema.pre('save', function checkApiKey(next) { // eslint-disable-line consistent-return +userSchema.pre('save', function checkApiKey(next) { + // eslint-disable-line consistent-return const user = this; - if (!user.isModified('apiKeys')) { return next(); } + if (!user.isModified('apiKeys')) { + return next(); + } let hasNew = false; user.apiKeys.forEach((k) => { if (k.isNew) { hasNew = true; - bcrypt.genSalt(10, (err, salt) => { // eslint-disable-line consistent-return - if (err) { return next(err); } + bcrypt.genSalt(10, (err, salt) => { + // eslint-disable-line consistent-return + if (err) { + return next(err); + } bcrypt.hash(k.hashedKey, salt, null, (innerErr, hash) => { - if (innerErr) { return next(innerErr); } + if (innerErr) { + return next(innerErr); + } k.hashedKey = hash; return next(); }); @@ -123,8 +148,11 @@ userSchema.set('toJSON', { /** * Helper method for validating user's password. */ -userSchema.methods.comparePassword = function comparePassword(candidatePassword, cb) { -// userSchema.methods.comparePassword = (candidatePassword, cb) => { +userSchema.methods.comparePassword = function comparePassword( + candidatePassword, + cb +) { + // userSchema.methods.comparePassword = (candidatePassword, cb) => { bcrypt.compare(candidatePassword, this.password, (err, isMatch) => { cb(err, isMatch); }); @@ -133,7 +161,10 @@ userSchema.methods.comparePassword = function comparePassword(candidatePassword, /** * Helper method for validating a user's api key */ -userSchema.methods.findMatchingKey = function findMatchingKey(candidateKey, cb) { +userSchema.methods.findMatchingKey = function findMatchingKey( + candidateKey, + cb +) { let foundOne = false; this.apiKeys.forEach((k) => { if (bcrypt.compareSync(candidateKey, k.hashedKey)) { @@ -178,13 +209,23 @@ userSchema.statics.findByEmail = function findByEmail(email, cb) { * @callback [cb] - Optional error-first callback that passes User document * @return {Promise<Object>} - Returns Promise fulfilled by User document */ -userSchema.statics.findByUsername = function findByUsername(username, options, cb) { +userSchema.statics.findByUsername = function findByUsername( + username, + options, + cb +) { const query = { username }; - if ((arguments.length === 3 && options.caseInsensitive) - || (arguments.length === 2 && typeof options === 'object' && options.caseInsensitive)) { - return this.findOne(query).collation({ locale: 'en', strength: 2 }).exec(cb); + if ( + (arguments.length === 3 && options.caseInsensitive) || + (arguments.length === 2 && + typeof options === 'object' && + options.caseInsensitive) + ) { + return this.findOne(query) + .collation({ locale: 'en', strength: 2 }) + .exec(cb); } const callback = typeof options === 'function' ? options : cb; return this.findOne(query, callback); @@ -205,7 +246,11 @@ userSchema.statics.findByUsername = function findByUsername(username, options, c * @callback [cb] - Optional error-first callback that passes User document * @return {Promise<Object>} - Returns Promise fulfilled by User document */ -userSchema.statics.findByEmailOrUsername = function findByEmailOrUsername(value, options, cb) { +userSchema.statics.findByEmailOrUsername = function findByEmailOrUsername( + value, + options, + cb +) { let isEmail; if (options && options.valueType) { isEmail = options.valueType === 'email'; @@ -213,10 +258,16 @@ userSchema.statics.findByEmailOrUsername = function findByEmailOrUsername(value, isEmail = value.indexOf('@') > -1; } // do the case insensitive stuff - if ((arguments.length === 3 && options.caseInsensitive) - || (arguments.length === 2 && typeof options === 'object' && options.caseInsensitive)) { + if ( + (arguments.length === 3 && options.caseInsensitive) || + (arguments.length === 2 && + typeof options === 'object' && + options.caseInsensitive) + ) { const query = isEmail ? { email: value } : { username: value }; - return this.findOne(query).collation({ locale: 'en', strength: 2 }).exec(cb); + return this.findOne(query) + .collation({ locale: 'en', strength: 2 }) + .exec(cb); } const callback = typeof options === 'function' ? options : cb; if (isEmail) { @@ -235,12 +286,13 @@ userSchema.statics.findByEmailOrUsername = function findByEmailOrUsername(value, * @callback [cb] - Optional error-first callback that passes User document * @return {Promise<Object>} - Returns Promise fulfilled by User document */ -userSchema.statics.findByEmailAndUsername = function findByEmailAndUsername(email, username, cb) { +userSchema.statics.findByEmailAndUsername = function findByEmailAndUsername( + email, + username, + cb +) { const query = { - $or: [ - { email }, - { username } - ] + $or: [{ email }, { username }] }; return this.findOne(query).collation({ locale: 'en', strength: 2 }).exec(cb); }; diff --git a/server/routes/aws.routes.js b/server/routes/aws.routes.js index 6bbf37955b..7a1c6d7063 100644 --- a/server/routes/aws.routes.js +++ b/server/routes/aws.routes.js @@ -5,8 +5,16 @@ import isAuthenticated from '../utils/isAuthenticated'; const router = new Router(); router.post('/S3/sign', isAuthenticated, AWSController.signS3); -router.post('/S3/copy', isAuthenticated, AWSController.copyObjectInS3RequestHandler); -router.delete('/S3/:userId?/:objectKey', isAuthenticated, AWSController.deleteObjectFromS3); +router.post( + '/S3/copy', + isAuthenticated, + AWSController.copyObjectInS3RequestHandler +); +router.delete( + '/S3/:userId?/:objectKey', + isAuthenticated, + AWSController.deleteObjectFromS3 +); router.get('/S3/objects', AWSController.listObjectsInS3ForUserRequestHandler); export default router; diff --git a/server/routes/collection.routes.js b/server/routes/collection.routes.js index d4f7ff9a42..31316d6f5b 100644 --- a/server/routes/collection.routes.js +++ b/server/routes/collection.routes.js @@ -5,16 +5,40 @@ import isAuthenticated from '../utils/isAuthenticated'; const router = new Router(); // List collections -router.get('/collections', isAuthenticated, CollectionController.listCollections); +router.get( + '/collections', + isAuthenticated, + CollectionController.listCollections +); router.get('/:username/collections', CollectionController.listCollections); // Create, modify, delete collection -router.post('/collections', isAuthenticated, CollectionController.createCollection); -router.patch('/collections/:id', isAuthenticated, CollectionController.updateCollection); -router.delete('/collections/:id', isAuthenticated, CollectionController.removeCollection); +router.post( + '/collections', + isAuthenticated, + CollectionController.createCollection +); +router.patch( + '/collections/:id', + isAuthenticated, + CollectionController.updateCollection +); +router.delete( + '/collections/:id', + isAuthenticated, + CollectionController.removeCollection +); // Add and remove projects to collection -router.post('/collections/:id/:projectId', isAuthenticated, CollectionController.addProjectToCollection); -router.delete('/collections/:id/:projectId', isAuthenticated, CollectionController.removeProjectFromCollection); +router.post( + '/collections/:id/:projectId', + isAuthenticated, + CollectionController.addProjectToCollection +); +router.delete( + '/collections/:id/:projectId', + isAuthenticated, + CollectionController.removeProjectFromCollection +); export default router; diff --git a/server/routes/file.routes.js b/server/routes/file.routes.js index 36139c46ef..4f545d0862 100644 --- a/server/routes/file.routes.js +++ b/server/routes/file.routes.js @@ -4,7 +4,15 @@ import isAuthenticated from '../utils/isAuthenticated'; const router = new Router(); -router.post('/projects/:project_id/files', isAuthenticated, FileController.createFile); -router.delete('/projects/:project_id/files/:file_id', isAuthenticated, FileController.deleteFile); +router.post( + '/projects/:project_id/files', + isAuthenticated, + FileController.createFile +); +router.delete( + '/projects/:project_id/files/:file_id', + isAuthenticated, + FileController.deleteFile +); export default router; diff --git a/server/routes/passport.routes.js b/server/routes/passport.routes.js index ff256effe1..da9daaecc1 100644 --- a/server/routes/passport.routes.js +++ b/server/routes/passport.routes.js @@ -5,40 +5,48 @@ const router = new Router(); router.get('/auth/github', passport.authenticate('github')); router.get('/auth/github/callback', (req, res, next) => { - passport.authenticate('github', { failureRedirect: '/login' }, (err, user) => { - if (err) { - // use query string param to show error; - res.redirect('/account?error=github'); - return; - } - - req.logIn(user, (loginErr) => { - if (loginErr) { - next(loginErr); + passport.authenticate( + 'github', + { failureRedirect: '/login' }, + (err, user) => { + if (err) { + // use query string param to show error; + res.redirect('/account?error=github'); return; } - res.redirect('/'); - }); - })(req, res, next); + + req.logIn(user, (loginErr) => { + if (loginErr) { + next(loginErr); + return; + } + res.redirect('/'); + }); + } + )(req, res, next); }); router.get('/auth/google', passport.authenticate('google')); router.get('/auth/google/callback', (req, res, next) => { - passport.authenticate('google', { failureRedirect: '/login' }, (err, user) => { - if (err) { - // use query string param to show error; - res.redirect('/account?error=google'); - return; - } - - req.logIn(user, (loginErr) => { - if (loginErr) { - next(loginErr); + passport.authenticate( + 'google', + { failureRedirect: '/login' }, + (err, user) => { + if (err) { + // use query string param to show error; + res.redirect('/account?error=google'); return; } - res.redirect('/'); - }); - })(req, res, next); + + req.logIn(user, (loginErr) => { + if (loginErr) { + next(loginErr); + return; + } + res.redirect('/'); + }); + } + )(req, res, next); }); export default router; diff --git a/server/routes/project.routes.js b/server/routes/project.routes.js index 073ab90d80..469eb43e92 100644 --- a/server/routes/project.routes.js +++ b/server/routes/project.routes.js @@ -6,11 +6,19 @@ const router = new Router(); router.post('/projects', isAuthenticated, ProjectController.createProject); -router.put('/projects/:project_id', isAuthenticated, ProjectController.updateProject); +router.put( + '/projects/:project_id', + isAuthenticated, + ProjectController.updateProject +); router.get('/:username/projects/:project_id', ProjectController.getProject); -router.delete('/projects/:project_id', isAuthenticated, ProjectController.deleteProject); +router.delete( + '/projects/:project_id', + isAuthenticated, + ProjectController.deleteProject +); router.get('/projects', ProjectController.getProjects); diff --git a/server/routes/server.routes.js b/server/routes/server.routes.js index 7fab75fc7d..0a5d2b30aa 100644 --- a/server/routes/server.routes.js +++ b/server/routes/server.routes.js @@ -2,12 +2,16 @@ import { Router } from 'express'; import { renderIndex } from '../views/index'; import { get404Sketch } from '../views/404Page'; import { userExists } from '../controllers/user.controller'; -import { projectExists, projectForUserExists } from '../controllers/project.controller'; +import { + projectExists, + projectForUserExists +} from '../controllers/project.controller'; import { collectionForUserExists } from '../controllers/collection.controller'; const router = new Router(); -const fallback404 = res => (exists => (exists ? res.send(renderIndex()) : get404Sketch(html => res.send(html)))); +const fallback404 = (res) => (exists) => + exists ? res.send(renderIndex()) : get404Sketch((html) => res.send(html)); // this is intended to be a temporary file // until i figure out isomorphic rendering @@ -24,39 +28,39 @@ router.get('/signup', (req, res) => { }); router.get('/projects/:project_id', (req, res) => { - projectExists(req.params.project_id, exists => ( - exists ? res.send(renderIndex()) : get404Sketch(html => res.send(html)) - )); + projectExists(req.params.project_id, (exists) => + exists ? res.send(renderIndex()) : get404Sketch((html) => res.send(html)) + ); }); router.get('/:username/sketches/:project_id/add-to-collection', (req, res) => { - projectForUserExists(req.params.username, req.params.project_id, exists => ( - exists ? res.send(renderIndex()) : get404Sketch(html => res.send(html)) - )); + projectForUserExists(req.params.username, req.params.project_id, (exists) => + exists ? res.send(renderIndex()) : get404Sketch((html) => res.send(html)) + ); }); router.get('/:username/sketches/:project_id', (req, res) => { - projectForUserExists(req.params.username, req.params.project_id, exists => ( - exists ? res.send(renderIndex()) : get404Sketch(html => res.send(html)) - )); + projectForUserExists(req.params.username, req.params.project_id, (exists) => + exists ? res.send(renderIndex()) : get404Sketch((html) => res.send(html)) + ); }); router.get('/:username/sketches', (req, res) => { - userExists(req.params.username, exists => ( - exists ? res.send(renderIndex()) : get404Sketch(html => res.send(html)) - )); + userExists(req.params.username, (exists) => + exists ? res.send(renderIndex()) : get404Sketch((html) => res.send(html)) + ); }); router.get('/:username/full/:project_id', (req, res) => { - projectForUserExists(req.params.username, req.params.project_id, exists => ( - exists ? res.send(renderIndex()) : get404Sketch(html => res.send(html)) - )); + projectForUserExists(req.params.username, req.params.project_id, (exists) => + exists ? res.send(renderIndex()) : get404Sketch((html) => res.send(html)) + ); }); router.get('/full/:project_id', (req, res) => { - projectExists(req.params.project_id, exists => ( - exists ? res.send(renderIndex()) : get404Sketch(html => res.send(html)) - )); + projectExists(req.params.project_id, (exists) => + exists ? res.send(renderIndex()) : get404Sketch((html) => res.send(html)) + ); }); router.get('/login', (req, res) => { @@ -96,11 +100,12 @@ router.get('/assets', (req, res) => { router.get('/:username/assets', (req, res) => { userExists(req.params.username, (exists) => { - const isLoggedInUser = req.user && req.user.username === req.params.username; + const isLoggedInUser = + req.user && req.user.username === req.params.username; const canAccess = exists && isLoggedInUser; - return canAccess ? - res.send(renderIndex()) : - get404Sketch(html => res.send(html)); + return canAccess + ? res.send(renderIndex()) + : get404Sketch((html) => res.send(html)); }); }); @@ -118,31 +123,31 @@ router.get('/about', (req, res) => { router.get('/:username/collections/create', (req, res) => { userExists(req.params.username, (exists) => { - const isLoggedInUser = req.user && req.user.username === req.params.username; + const isLoggedInUser = + req.user && req.user.username === req.params.username; const canAccess = exists && isLoggedInUser; - return canAccess ? - res.send(renderIndex()) : - get404Sketch(html => res.send(html)); + return canAccess + ? res.send(renderIndex()) + : get404Sketch((html) => res.send(html)); }); }); router.get('/:username/collections/create', (req, res) => { - userExists(req.params.username, exists => ( - exists ? res.send(renderIndex()) : get404Sketch(html => res.send(html)) - )); + userExists(req.params.username, (exists) => + exists ? res.send(renderIndex()) : get404Sketch((html) => res.send(html)) + ); }); router.get('/:username/collections/:id', (req, res) => { - collectionForUserExists(req.params.username, req.params.id, exists => ( - exists ? res.send(renderIndex()) : get404Sketch(html => res.send(html)) - )); + collectionForUserExists(req.params.username, req.params.id, (exists) => + exists ? res.send(renderIndex()) : get404Sketch((html) => res.send(html)) + ); }); router.get('/:username/collections', (req, res) => { - userExists(req.params.username, exists => ( - exists ? res.send(renderIndex()) : get404Sketch(html => res.send(html)) - )); + userExists(req.params.username, (exists) => + exists ? res.send(renderIndex()) : get404Sketch((html) => res.send(html)) + ); }); - export default router; diff --git a/server/routes/user.routes.js b/server/routes/user.routes.js index fc58f8f527..a52facf963 100644 --- a/server/routes/user.routes.js +++ b/server/routes/user.routes.js @@ -20,7 +20,11 @@ router.put('/account', isAuthenticated, UserController.updateSettings); router.post('/account/api-keys', isAuthenticated, UserController.createApiKey); -router.delete('/account/api-keys/:keyId', isAuthenticated, UserController.removeApiKey); +router.delete( + '/account/api-keys/:keyId', + isAuthenticated, + UserController.removeApiKey +); router.post('/verify/send', UserController.emailVerificationInitiate); diff --git a/server/scripts/examples-gg-latest.js b/server/scripts/examples-gg-latest.js index 1e4f01e992..8e8025a6ff 100644 --- a/server/scripts/examples-gg-latest.js +++ b/server/scripts/examples-gg-latest.js @@ -17,8 +17,7 @@ const branchRef = `?ref=${branchName}`; const clientId = process.env.GITHUB_ID; const clientSecret = process.env.GITHUB_SECRET; -const defaultHTML = - `<!DOCTYPE html> +const defaultHTML = `<!DOCTYPE html> <html lang="en"> <head> <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.7.3/p5.min.js"></script> @@ -47,8 +46,7 @@ const defaultHTML = </html> `; -const defaultCSS = - `html, body { +const defaultCSS = `html, body { padding: 0; margin: 0; } @@ -61,10 +59,15 @@ const headers = { 'User-Agent': 'p5js-web-editor/0.0.1' }; const mongoConnectionString = process.env.MONGO_URL; -mongoose.connect(mongoConnectionString, { useNewUrlParser: true, useUnifiedTopology: true }); +mongoose.connect(mongoConnectionString, { + useNewUrlParser: true, + useUnifiedTopology: true +}); mongoose.set('useCreateIndex', true); mongoose.connection.on('error', () => { - console.error('MongoDB Connection Error. Please make sure that MongoDB is running.'); + console.error( + 'MongoDB Connection Error. Please make sure that MongoDB is running.' + ); process.exit(1); }); @@ -78,10 +81,10 @@ const insert = function insert(_mainString, _insString, _pos) { const mainString = _mainString; let pos = _pos; - if (typeof (pos) === 'undefined') { + if (typeof pos === 'undefined') { pos = 0; } - if (typeof (insString) === 'undefined') { + if (typeof insString === 'undefined') { insString = ''; } return mainString.slice(0, pos) + insString + mainString.slice(pos); @@ -112,52 +115,67 @@ function getCodePackage() { method: 'GET', headers: { ...headers, - Authorization: `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}` + Authorization: `Basic ${Buffer.from( + `${clientId}:${clientSecret}` + ).toString('base64')}` }, json: true }; - return rp(options).then((res) => { - res.forEach((metadata) => { - if (metadata.name.endsWith('P') === true || metadata.name.endsWith('M') === true) { - sketchRootList.push(metadata); - } - }); + return rp(options) + .then((res) => { + res.forEach((metadata) => { + if ( + metadata.name.endsWith('P') === true || + metadata.name.endsWith('M') === true + ) { + sketchRootList.push(metadata); + } + }); - return sketchRootList; - }).catch((err) => { - throw err; - }); + return sketchRootList; + }) + .catch((err) => { + throw err; + }); } // 2. get the list of all the top-level sketch directories in P and M function getSketchDirectories(sketchRootList) { // console.log(sketchRootList); - return Q.all(sketchRootList.map((sketches) => { - // console.log(sketches) - const options = { - url: `https://api.github.com/repos/generative-design/Code-Package-p5.js/contents/${sketches.path}${branchRef}`, - method: 'GET', - headers: { - ...headers, - Authorization: `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}` - }, - json: true - }; + return Q.all( + sketchRootList.map((sketches) => { + // console.log(sketches) + const options = { + url: `https://api.github.com/repos/generative-design/Code-Package-p5.js/contents/${sketches.path}${branchRef}`, + method: 'GET', + headers: { + ...headers, + Authorization: `Basic ${Buffer.from( + `${clientId}:${clientSecret}` + ).toString('base64')}` + }, + json: true + }; - return rp(options).then((res) => { - const sketchDirs = flatten(res); + return rp(options) + .then((res) => { + const sketchDirs = flatten(res); - return sketchDirs; - }).catch((err) => { - throw err; - }); - })).then((output) => { + return sketchDirs; + }) + .catch((err) => { + throw err; + }); + }) + ).then((output) => { const sketchList = []; output.forEach((l) => { l.forEach((i) => { - if (i.type === 'dir') { sketchList.push(i); } + if (i.type === 'dir') { + sketchList.push(i); + } }); }); @@ -165,27 +183,30 @@ function getSketchDirectories(sketchRootList) { }); } - // 3. For each sketch item in the sketchList, append the tree contents to each item function appendSketchItemLinks(sketchList) { - return Q.all(sketchList.map((sketches) => { - const options = { - // url: `${sketches.url}?client_id=${clientId}&client_secret=${clientSecret}`, - url: `https://api.github.com/repos/generative-design/Code-Package-p5.js/contents/${sketches.path}${branchRef}`, - method: 'GET', - headers: { - ...headers, - Authorization: `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}` - }, - json: true - }; + return Q.all( + sketchList.map((sketches) => { + const options = { + // url: `${sketches.url}?client_id=${clientId}&client_secret=${clientSecret}`, + url: `https://api.github.com/repos/generative-design/Code-Package-p5.js/contents/${sketches.path}${branchRef}`, + method: 'GET', + headers: { + ...headers, + Authorization: `Basic ${Buffer.from( + `${clientId}:${clientSecret}` + ).toString('base64')}` + }, + json: true + }; - return rp(options).then((res) => { - sketches.tree = res; + return rp(options).then((res) => { + sketches.tree = res; - return sketchList; - }); - })); + return sketchList; + }); + }) + ); } // 4. for each sketch item @@ -235,48 +256,46 @@ function formatSketchForStorage(sketch, user) { const c = objectID().toHexString(); const r = objectID().toHexString(); - const newProject = new Project({ name: sketch.name, user: user._id, - files: [{ - name: 'root', - id: r, - _id: r, - children: [a, b, c], - fileType: 'folder' - }, - { - name: 'sketch.js', - content: getSketchDownloadUrl(sketch), - id: a, - _id: a, - isSelectedFile: true, - fileType: 'file', - children: [] - }, - { - name: 'index.html', - content: defaultHTML, - id: b, - _id: b, - fileType: 'file', - children: [] - }, - { - name: 'style.css', - content: defaultCSS, - id: c, - _id: c, - fileType: 'file', - children: [] - } + files: [ + { + name: 'root', + id: r, + _id: r, + children: [a, b, c], + fileType: 'folder' + }, + { + name: 'sketch.js', + content: getSketchDownloadUrl(sketch), + id: a, + _id: a, + isSelectedFile: true, + fileType: 'file', + children: [] + }, + { + name: 'index.html', + content: defaultHTML, + id: b, + _id: b, + fileType: 'file', + children: [] + }, + { + name: 'style.css', + content: defaultCSS, + id: c, + _id: c, + fileType: 'file', + children: [] + } ], _id: shortid.generate() - }); - // get any additional js files url // TODO: this could probably be optimized - so many loops! function addAdditionalJs(_sketch, newProjectObject) { @@ -301,7 +320,8 @@ function formatSketchForStorage(sketch, user) { output[0].children.push(projectItem.id); // add the JS reference to the defaultHTML output[2].content = insert( - output[2].content, `<script src='${item.name}'></script>`, + output[2].content, + `<script src='${item.name}'></script>`, output[2].content.search('<!-- sketch additions -->') ); } @@ -376,7 +396,6 @@ function formatAllSketches(sketchList) { }); } - // get all the sketch data content and download to the newProjects array function getAllSketchContent(newProjectList) { /* eslint-disable */ @@ -450,7 +469,6 @@ function createProjectsInP5user(newProjectList) { }); } - /* --- Main --- */ // remove any of the old files and add the new stuffs to the UI function getp5User() { @@ -480,19 +498,20 @@ function getp5User() { }); }); - if (testMake === true) { // Run for Testing // Run for production - return getCodePackage() - .then(getSketchDirectories) - .then(appendSketchItemLinks) - .then(getSketchItems) - // .then(saveRetrievalToFile) - .then(formatAllSketches) - .then(getAllSketchContent) - // .then(saveNewProjectsToFile) - .then(createProjectsInP5user); + return ( + getCodePackage() + .then(getSketchDirectories) + .then(appendSketchItemLinks) + .then(getSketchItems) + // .then(saveRetrievalToFile) + .then(formatAllSketches) + .then(getAllSketchContent) + // .then(saveNewProjectsToFile) + .then(createProjectsInP5user) + ); } // Run for production return getCodePackage() @@ -507,7 +526,6 @@ function getp5User() { // Run the entire process getp5User(); - /* --- Tester Functions --- */ /** * Tester Functions - IGNORE BELOW @ below are functions for testing diff --git a/server/scripts/examples-ml5.js b/server/scripts/examples-ml5.js index 878bd0b5b6..801f25501c 100644 --- a/server/scripts/examples-ml5.js +++ b/server/scripts/examples-ml5.js @@ -28,7 +28,9 @@ const githubRequestOptions = { method: 'GET', headers: { ...headers, - Authorization: `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}` + Authorization: `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString( + 'base64' + )}` }, json: true }; @@ -38,7 +40,9 @@ const editorRequestOptions = { method: 'GET', headers: { ...headers, - Authorization: `Basic ${Buffer.from(`${editorUsername}:${personalAccessToken}`).toString('base64')}` + Authorization: `Basic ${Buffer.from( + `${editorUsername}:${personalAccessToken}` + ).toString('base64')}` }, json: true }; @@ -65,10 +69,7 @@ async function fetchFileContent(item) { const file = { url: item.url }; // if it is an html or js file - if ( - (file.url != null && name.endsWith('.html')) || - name.endsWith('.js') - ) { + if ((file.url != null && name.endsWith('.html')) || name.endsWith('.js')) { const options = Object.assign({}, githubRequestOptions); options.url = `${file.url}`; @@ -88,14 +89,15 @@ async function fetchFileContent(item) { } if (file.url) { - const cdnRef = `https://cdn.jsdelivr.net/gh/ml5js/ml5-examples@${branchName}${file.url.split(branchName)[1]}`; + const cdnRef = `https://cdn.jsdelivr.net/gh/ml5js/ml5-examples@${branchName}${ + file.url.split(branchName)[1] + }`; file.url = cdnRef; } return file; } - /** * STEP 1: Get the top level cateogories */ @@ -162,7 +164,7 @@ async function traverseSketchTree(parentObject) { output.tree = await rp(options); - output.tree = output.tree.map(file => traverseSketchTree(file)); + output.tree = output.tree.map((file) => traverseSketchTree(file)); output.tree = await Q.all(output.tree); @@ -174,7 +176,9 @@ async function traverseSketchTree(parentObject) { * @param {*} categoryExamples - all of the categories in an array */ async function traverseSketchTreeAll(categoryExamples) { - const sketches = categoryExamples.map(async sketch => traverseSketchTree(sketch)); + const sketches = categoryExamples.map(async (sketch) => + traverseSketchTree(sketch) + ); const result = await Q.all(sketches); return result; @@ -219,22 +223,19 @@ function traverseAndFormat(parentObject) { * @param {*} projectFileTree */ async function traverseAndDownload(projectFileTree) { - return projectFileTree.reduce( - async (previousPromise, item, idx) => { - const result = await previousPromise; - - if (Array.isArray(item.children)) { - result[item.name] = { - files: await traverseAndDownload(item.children) - }; - } else { - result[item.name] = await fetchFileContent(item); - } + return projectFileTree.reduce(async (previousPromise, item, idx) => { + const result = await previousPromise; - return result; - }, - {} - ); + if (Array.isArray(item.children)) { + result[item.name] = { + files: await traverseAndDownload(item.children) + }; + } else { + result[item.name] = await fetchFileContent(item); + } + + return result; + }, {}); } /** @@ -261,7 +262,7 @@ async function formatSketchForStorage(sketch, user) { function formatSketchForStorageAll(sketchWithItems, user) { let sketchList = sketchWithItems.slice(0); - sketchList = sketchList.map(sketch => formatSketchForStorage(sketch, user)); + sketchList = sketchList.map((sketch) => formatSketchForStorage(sketch, user)); return Promise.all(sketchList); } @@ -363,8 +364,12 @@ async function make() { const categories = await getCategories(); const categoryExamples = await getCategoryExamples(categories); - const examplesWithResourceTree = await traverseSketchTreeAll(categoryExamples); - const formattedSketchList = await formatSketchForStorageAll(examplesWithResourceTree); + const examplesWithResourceTree = await traverseSketchTreeAll( + categoryExamples + ); + const formattedSketchList = await formatSketchForStorageAll( + examplesWithResourceTree + ); await createProjectsInP5User(formattedSketchList); console.log('done!'); @@ -382,9 +387,13 @@ async function make() { // eslint-disable-next-line no-unused-vars async function test() { // read from file while testing - const examplesWithResourceTree = JSON.parse(fs.readFileSync('./ml5-examplesWithResourceTree.json')); + const examplesWithResourceTree = JSON.parse( + fs.readFileSync('./ml5-examplesWithResourceTree.json') + ); - const formattedSketchList = await formatSketchForStorageAll(examplesWithResourceTree); + const formattedSketchList = await formatSketchForStorageAll( + examplesWithResourceTree + ); await createProjectsInP5User(formattedSketchList); console.log('done!'); diff --git a/server/scripts/examples.js b/server/scripts/examples.js index 2b31c4dcbf..0122d4e06d 100644 --- a/server/scripts/examples.js +++ b/server/scripts/examples.js @@ -6,8 +6,7 @@ import shortid from 'shortid'; import User from '../models/user'; import Project from '../models/project'; -const defaultHTML = - `<!DOCTYPE html> +const defaultHTML = `<!DOCTYPE html> <html lang="en"> <head> <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.1.9/p5.js"></script> @@ -21,8 +20,7 @@ const defaultHTML = </html> `; -const defaultCSS = - `html, body { +const defaultCSS = `html, body { margin: 0; padding: 0; } @@ -38,21 +36,29 @@ const headers = { 'User-Agent': 'p5js-web-editor/0.0.1' }; const mongoConnectionString = process.env.MONGO_URL; -mongoose.connect(mongoConnectionString, { useNewUrlParser: true, useUnifiedTopology: true }); +mongoose.connect(mongoConnectionString, { + useNewUrlParser: true, + useUnifiedTopology: true +}); mongoose.set('useCreateIndex', true); mongoose.connection.on('error', () => { - console.error('MongoDB Connection Error. Please make sure that MongoDB is running.'); + console.error( + 'MongoDB Connection Error. Please make sure that MongoDB is running.' + ); process.exit(1); }); async function getCategories() { const categories = []; const options = { - url: 'https://api.github.com/repos/processing/p5.js-website/contents/src/data/examples/en', + url: + 'https://api.github.com/repos/processing/p5.js-website/contents/src/data/examples/en', method: 'GET', headers: { ...headers, - Authorization: `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}` + Authorization: `Basic ${Buffer.from( + `${clientId}:${clientSecret}` + ).toString('base64')}` }, json: true }; @@ -72,76 +78,107 @@ async function getCategories() { } function getSketchesInCategories(categories) { - return Q.all(categories.map(async (category) => { - const options = { - url: `${category.url.replace('?ref=main', '')}`, - method: 'GET', - headers: { - ...headers, - Authorization: `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}` - }, - json: true - }; - try { - const res = await rp(options); - const projectsInOneCategory = []; - res.forEach((example) => { - let projectName; - if (example.name === '02_Instance_Container.js') { - for (let i = 1; i < 5; i += 1) { - const instanceProjectName = `${category.name}: Instance Container ${i}`; - projectsInOneCategory.push({ sketchUrl: example.download_url, projectName: instanceProjectName }); - } - } else { - if (example.name.split('_')[1]) { - projectName = `${category.name}: ${example.name.split('_').slice(1).join(' ').replace('.js', '')}`; + return Q.all( + categories.map(async (category) => { + const options = { + url: `${category.url.replace('?ref=main', '')}`, + method: 'GET', + headers: { + ...headers, + Authorization: `Basic ${Buffer.from( + `${clientId}:${clientSecret}` + ).toString('base64')}` + }, + json: true + }; + try { + const res = await rp(options); + const projectsInOneCategory = []; + res.forEach((example) => { + let projectName; + if (example.name === '02_Instance_Container.js') { + for (let i = 1; i < 5; i += 1) { + const instanceProjectName = `${category.name}: Instance Container ${i}`; + projectsInOneCategory.push({ + sketchUrl: example.download_url, + projectName: instanceProjectName + }); + } } else { - projectName = `${category.name}: ${example.name.replace('.js', '')}`; + if (example.name.split('_')[1]) { + projectName = `${category.name}: ${example.name + .split('_') + .slice(1) + .join(' ') + .replace('.js', '')}`; + } else { + projectName = `${category.name}: ${example.name.replace( + '.js', + '' + )}`; + } + projectsInOneCategory.push({ + sketchUrl: example.download_url, + projectName + }); } - projectsInOneCategory.push({ sketchUrl: example.download_url, projectName }); - } - }); - return projectsInOneCategory; - } catch (error) { - throw error; - } - })); + }); + return projectsInOneCategory; + } catch (error) { + throw error; + } + }) + ); } function getSketchContent(projectsInAllCategories) { - return Q.all(projectsInAllCategories.map(projectsInOneCategory => Q.all(projectsInOneCategory.map(async (project) => { - const options = { - url: `${project.sketchUrl.replace('?ref=main', '')}`, - method: 'GET', - headers: { - ...headers, - Authorization: `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}` - } - }; - try { - const res = await rp(options); - const noNumberprojectName = project.projectName.replace(/(\d+)/g, ''); - if (noNumberprojectName === 'Instance Mode: Instance Container ') { - for (let i = 0; i < 4; i += 1) { - const splitedRes = `${res.split('*/')[1].split('</html>')[i]}</html>\n`; - project.sketchContent = splitedRes.replace( - 'p5.js', - 'https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.9.0/p5.js' - ); - } - } else { - project.sketchContent = res; - } - return project; - } catch (error) { - throw error; - } - })))); + return Q.all( + projectsInAllCategories.map((projectsInOneCategory) => + Q.all( + projectsInOneCategory.map(async (project) => { + const options = { + url: `${project.sketchUrl.replace('?ref=main', '')}`, + method: 'GET', + headers: { + ...headers, + Authorization: `Basic ${Buffer.from( + `${clientId}:${clientSecret}` + ).toString('base64')}` + } + }; + try { + const res = await rp(options); + const noNumberprojectName = project.projectName.replace( + /(\d+)/g, + '' + ); + if (noNumberprojectName === 'Instance Mode: Instance Container ') { + for (let i = 0; i < 4; i += 1) { + const splitedRes = `${ + res.split('*/')[1].split('</html>')[i] + }</html>\n`; + project.sketchContent = splitedRes.replace( + 'p5.js', + 'https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.9.0/p5.js' + ); + } + } else { + project.sketchContent = res; + } + return project; + } catch (error) { + throw error; + } + }) + ) + ) + ); } async function addAssetsToProject(assets, response, project) { /* eslint-disable no-await-in-loop */ - for (let i = 0; i < assets.length; i += 1) { // iterate through each asset in the project in series (async/await functionality would not work with forEach() ) + for (let i = 0; i < assets.length; i += 1) { + // iterate through each asset in the project in series (async/await functionality would not work with forEach() ) const assetNamePath = assets[i]; let assetName = assetNamePath.split('assets/')[1]; let assetUrl = ''; @@ -170,13 +207,16 @@ async function addAssetsToProject(assets, response, project) { const fileID = objectID().toHexString(); - if (assetName.slice(-5) === '.vert' || assetName.slice(-5) === '.frag') { // check if the file has .vert or .frag extension + if (assetName.slice(-5) === '.vert' || assetName.slice(-5) === '.frag') { + // check if the file has .vert or .frag extension const assetOptions = { url: assetUrl, method: 'GET', headers: { ...headers, - Authorization: `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}` + Authorization: `Basic ${Buffer.from( + `${clientId}:${clientSecret}` + ).toString('base64')}` }, json: true }; @@ -204,7 +244,8 @@ async function addAssetsToProject(assets, response, project) { console.log(`create assets: ${assetName}`); // add asset file inside the newly created assets folder at index 4 project.files[4].children.push(fileID); - } else { // for assets files that are not .vert or .frag extension + } else { + // for assets files that are not .vert or .frag extension project.files.push({ name: assetName, url: `https://cdn.jsdelivr.net/gh/processing/p5.js-website@main/src/data/examples/assets/${assetName}`, @@ -222,14 +263,16 @@ async function addAssetsToProject(assets, response, project) { /* eslint-disable no-await-in-loop */ } - async function createProjectsInP5user(projectsInAllCategories) { const options = { - url: 'https://api.github.com/repos/processing/p5.js-website/contents/src/data/examples/assets', + url: + 'https://api.github.com/repos/processing/p5.js-website/contents/src/data/examples/assets', method: 'GET', headers: { ...headers, - Authorization: `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}` + Authorization: `Basic ${Buffer.from( + `${clientId}:${clientSecret}` + ).toString('base64')}` }, json: true }; @@ -237,106 +280,120 @@ async function createProjectsInP5user(projectsInAllCategories) { try { const res = await rp(options); const user = await User.findOne({ username: 'p5' }).exec(); - await Q.all(projectsInAllCategories.map(projectsInOneCategory => Q.all(projectsInOneCategory.map(async (project) => { - let newProject; - const a = objectID().toHexString(); - const b = objectID().toHexString(); - const c = objectID().toHexString(); - const r = objectID().toHexString(); - const noNumberprojectName = project.projectName.replace(/(\d+)/g, ''); - if (noNumberprojectName === 'Instance Mode: Instance Container ') { - newProject = new Project({ - name: project.projectName, - user: user._id, - files: [ - { - name: 'root', - id: r, - _id: r, - children: [a, b, c], - fileType: 'folder' - }, - { - name: 'sketch.js', - content: '// Instance Mode: Instance Container, please check its index.html file', - id: a, - _id: a, - fileType: 'file', - children: [] - }, - { - name: 'index.html', - content: project.sketchContent, - isSelectedFile: true, - id: b, - _id: b, - fileType: 'file', - children: [] - }, - { - name: 'style.css', - content: defaultCSS, - id: c, - _id: c, - fileType: 'file', - children: [] - } - ], - _id: shortid.generate() - }); - } else { - newProject = new Project({ - name: project.projectName, - user: user._id, - files: [ - { - name: 'root', - id: r, - _id: r, - children: [a, b, c], - fileType: 'folder' - }, - { - name: 'sketch.js', - content: project.sketchContent, - id: a, - _id: a, - isSelectedFile: true, - fileType: 'file', - children: [] - }, - { - name: 'index.html', - content: defaultHTML, - id: b, - _id: b, - fileType: 'file', - children: [] - }, - { - name: 'style.css', - content: defaultCSS, - id: c, - _id: c, - fileType: 'file', - children: [] + await Q.all( + projectsInAllCategories.map((projectsInOneCategory) => + Q.all( + projectsInOneCategory.map(async (project) => { + let newProject; + const a = objectID().toHexString(); + const b = objectID().toHexString(); + const c = objectID().toHexString(); + const r = objectID().toHexString(); + const noNumberprojectName = project.projectName.replace( + /(\d+)/g, + '' + ); + if (noNumberprojectName === 'Instance Mode: Instance Container ') { + newProject = new Project({ + name: project.projectName, + user: user._id, + files: [ + { + name: 'root', + id: r, + _id: r, + children: [a, b, c], + fileType: 'folder' + }, + { + name: 'sketch.js', + content: + '// Instance Mode: Instance Container, please check its index.html file', + id: a, + _id: a, + fileType: 'file', + children: [] + }, + { + name: 'index.html', + content: project.sketchContent, + isSelectedFile: true, + id: b, + _id: b, + fileType: 'file', + children: [] + }, + { + name: 'style.css', + content: defaultCSS, + id: c, + _id: c, + fileType: 'file', + children: [] + } + ], + _id: shortid.generate() + }); + } else { + newProject = new Project({ + name: project.projectName, + user: user._id, + files: [ + { + name: 'root', + id: r, + _id: r, + children: [a, b, c], + fileType: 'folder' + }, + { + name: 'sketch.js', + content: project.sketchContent, + id: a, + _id: a, + isSelectedFile: true, + fileType: 'file', + children: [] + }, + { + name: 'index.html', + content: defaultHTML, + id: b, + _id: b, + fileType: 'file', + children: [] + }, + { + name: 'style.css', + content: defaultCSS, + id: c, + _id: c, + fileType: 'file', + children: [] + } + ], + _id: shortid.generate() + }); } - ], - _id: shortid.generate() - }); - } - const assetsInProject = project.sketchContent.match(/assets\/[\w-]+\.[\w]*/g) - || project.sketchContent.match(/asset\/[\w-]*/g) || []; + const assetsInProject = + project.sketchContent.match(/assets\/[\w-]+\.[\w]*/g) || + project.sketchContent.match(/asset\/[\w-]*/g) || + []; - try { - await addAssetsToProject(assetsInProject, res, newProject); - const savedProject = await newProject.save(); - console.log(`Created a new project in p5 user: ${savedProject.name}`); - } catch (error) { - throw error; - } - })))); + try { + await addAssetsToProject(assetsInProject, res, newProject); + const savedProject = await newProject.save(); + console.log( + `Created a new project in p5 user: ${savedProject.name}` + ); + } catch (error) { + throw error; + } + }) + ) + ) + ); process.exit(); } catch (error) { throw error; diff --git a/server/scripts/update-syntax-highlighting.js b/server/scripts/update-syntax-highlighting.js index d9b1d564ed..00701fd5b1 100644 --- a/server/scripts/update-syntax-highlighting.js +++ b/server/scripts/update-syntax-highlighting.js @@ -28,20 +28,27 @@ request('https://p5js.org/reference/data.json', (err, res) => { p5FunctionPart = p5FunctionPart.replace(/"p5Function"/g, 'p5Function'); let generatedCode = '/* eslint-disable */ \n'; - generatedCode += '/* generated: do not edit! helper file for syntax highlighting. generated by update-syntax-highlighting script */ \n'; - generatedCode += 'var p5Function = {type: "variable", style: "p5-function"};\n'; - generatedCode += 'var p5Variable = {type: "variable", style: "p5-variable"};\n'; + generatedCode += + '/* generated: do not edit! helper file for syntax highlighting. generated by update-syntax-highlighting script */ \n'; + generatedCode += + 'var p5Function = {type: "variable", style: "p5-function"};\n'; + generatedCode += + 'var p5Variable = {type: "variable", style: "p5-variable"};\n'; generatedCode += `let p5VariableKeywords = ${p5VariablePart}; \n`; generatedCode += `let p5FunctionKeywords = ${p5FunctionPart}; \n`; generatedCode += 'exports.p5FunctionKeywords = p5FunctionKeywords;\n'; generatedCode += 'exports.p5VariableKeywords = p5VariableKeywords;\n'; - fs.writeFile(`${process.cwd()}/client/utils/p5-keywords.js`, generatedCode, (error) => { - if (error) { - console.log("Error!! Couldn't write to the file", error); - } else { - console.log('Syntax highlighting files updated successfully'); + fs.writeFile( + `${process.cwd()}/client/utils/p5-keywords.js`, + generatedCode, + (error) => { + if (error) { + console.log("Error!! Couldn't write to the file", error); + } else { + console.log('Syntax highlighting files updated successfully'); + } } - }); + ); } else { console.log("Error!! Couldn't fetch the data.json file"); } diff --git a/server/server.js b/server/server.js index 6003678868..bc39147f8a 100644 --- a/server/server.js +++ b/server/server.js @@ -37,9 +37,7 @@ const MongoStore = connectMongo(session); app.get('/health', (req, res) => res.json({ success: true })); -const allowedCorsOrigins = [ - /p5js\.org$/, -]; +const allowedCorsOrigins = [/p5js\.org$/]; // to allow client-only development if (process.env.CORS_ALLOW_LOCALHOST === 'true') { @@ -49,7 +47,12 @@ if (process.env.CORS_ALLOW_LOCALHOST === 'true') { // Run Webpack dev server in development mode if (process.env.NODE_ENV === 'development') { const compiler = webpack(config); - app.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: config.output.publicPath })); + app.use( + webpackDevMiddleware(compiler, { + noInfo: true, + publicPath: config.output.publicPath + }) + ); app.use(webpackHotMiddleware(compiler)); } @@ -59,7 +62,7 @@ app.set('trust proxy', true); // Enable Cross-Origin Resource Sharing (CORS) for all origins const corsMiddleware = cors({ credentials: true, - origin: allowedCorsOrigins, + origin: allowedCorsOrigins }); app.use(corsMiddleware); // Enable pre-flight OPTIONS route for all end-points @@ -68,21 +71,23 @@ app.options('*', corsMiddleware); app.use(bodyParser.urlencoded({ limit: '50mb', extended: true })); app.use(bodyParser.json({ limit: '50mb' })); app.use(cookieParser()); -app.use(session({ - resave: true, - saveUninitialized: false, - secret: process.env.SESSION_SECRET, - proxy: true, - name: 'sessionId', - cookie: { - httpOnly: true, - secure: false, - }, - store: new MongoStore({ - mongooseConnection: mongoose.connection, - autoReconnect: true +app.use( + session({ + resave: true, + saveUninitialized: false, + secret: process.env.SESSION_SECRET, + proxy: true, + name: 'sessionId', + cookie: { + httpOnly: true, + secure: false + }, + store: new MongoStore({ + mongooseConnection: mongoose.connection, + autoReconnect: true + }) }) -})); +); app.use('/api/v1', requestsOfTypeJSON(), api); // This is a temporary way to test access via Personal Access Tokens @@ -90,35 +95,39 @@ app.use('/api/v1', requestsOfTypeJSON(), api); // return the user's information. app.get( '/api/v1/auth/access-check', - passport.authenticate('basic', { session: false }), (req, res) => res.json(req.user) + passport.authenticate('basic', { session: false }), + (req, res) => res.json(req.user) ); // For basic auth, but can't have double basic auth for API if (process.env.BASIC_USERNAME && process.env.BASIC_PASSWORD) { - app.use(basicAuth({ - users: { - [process.env.BASIC_USERNAME]: process.env.BASIC_PASSWORD - }, - challenge: true - })); + app.use( + basicAuth({ + users: { + [process.env.BASIC_USERNAME]: process.env.BASIC_PASSWORD + }, + challenge: true + }) + ); } // Body parser, cookie parser, sessions, serve public assets app.use( '/locales', - Express.static( - path.resolve(__dirname, '../dist/static/locales'), - { - // Browsers must revalidate for changes to the locale files - // It doesn't actually mean "don't cache this file" - // See: https://jakearchibald.com/2016/caching-best-practices/ - setHeaders: res => res.setHeader('Cache-Control', 'no-cache') - } - ) + Express.static(path.resolve(__dirname, '../dist/static/locales'), { + // Browsers must revalidate for changes to the locale files + // It doesn't actually mean "don't cache this file" + // See: https://jakearchibald.com/2016/caching-best-practices/ + setHeaders: (res) => res.setHeader('Cache-Control', 'no-cache') + }) +); +app.use( + Express.static(path.resolve(__dirname, '../dist/static'), { + maxAge: + process.env.STATIC_MAX_AGE || + (process.env.NODE_ENV === 'production' ? '1d' : '0') + }) ); -app.use(Express.static(path.resolve(__dirname, '../dist/static'), { - maxAge: process.env.STATIC_MAX_AGE || (process.env.NODE_ENV === 'production' ? '1d' : '0') -})); app.use(passport.initialize()); app.use(passport.session()); @@ -143,10 +152,15 @@ require('./config/passport'); // Connect to MongoDB mongoose.Promise = global.Promise; -mongoose.connect(mongoConnectionString, { useNewUrlParser: true, useUnifiedTopology: true }); +mongoose.connect(mongoConnectionString, { + useNewUrlParser: true, + useUnifiedTopology: true +}); mongoose.set('useCreateIndex', true); mongoose.connection.on('error', () => { - console.error('MongoDB Connection Error. Please make sure that MongoDB is running.'); + console.error( + 'MongoDB Connection Error. Please make sure that MongoDB is running.' + ); process.exit(1); }); @@ -164,12 +178,11 @@ app.use('/api', (error, req, res, next) => { next(error); }); - // Handle missing routes. app.get('*', (req, res) => { res.status(404); if (req.accepts('html')) { - get404Sketch(html => res.send(html)); + get404Sketch((html) => res.send(html)); return; } if (req.accepts('json')) { diff --git a/server/utils/filePath.js b/server/utils/filePath.js index 4e12a82c46..88e0730b13 100644 --- a/server/utils/filePath.js +++ b/server/utils/filePath.js @@ -5,7 +5,7 @@ export function resolvePathToFile(filePath, files) { const filePathArray = filePath.split('/'); let resolvedFile; - let currentFile = files.find(file => file.name === 'root'); + let currentFile = files.find((file) => file.name === 'root'); filePathArray.some((filePathSegment, index) => { if (filePathSegment === '' || filePathSegment === '.') { return false; @@ -14,9 +14,11 @@ export function resolvePathToFile(filePath, files) { } let foundChild = false; - const childFiles = currentFile.children.map(childFileId => - files.find(file => - file._id.valueOf().toString() === childFileId.valueOf())); + const childFiles = currentFile.children.map((childFileId) => + files.find( + (file) => file._id.valueOf().toString() === childFileId.valueOf() + ) + ); childFiles.some((childFile) => { if (childFile.name === filePathSegment) { currentFile = childFile; diff --git a/server/utils/fileUtils.js b/server/utils/fileUtils.js index d61c0b7556..f06b846462 100644 --- a/server/utils/fileUtils.js +++ b/server/utils/fileUtils.js @@ -1,22 +1,66 @@ - - -export const fileExtensionsArray = ['gif', 'jpg', 'jpeg', 'png', 'bmp', 'wav', 'flac', 'ogg', - 'oga', 'mp4', 'm4p', 'mp3', 'm4a', 'aiff', 'aif', 'm4v', 'aac', 'webm', 'mpg', 'mp2', - 'mpeg', 'mpe', 'mpv', 'js', 'jsx', 'html', 'htm', 'css', 'json', 'csv', 'tsv', 'obj', 'svg', - 'otf', 'ttf', 'txt', 'mov', 'vert', 'frag', 'bin']; +export const fileExtensionsArray = [ + 'gif', + 'jpg', + 'jpeg', + 'png', + 'bmp', + 'wav', + 'flac', + 'ogg', + 'oga', + 'mp4', + 'm4p', + 'mp3', + 'm4a', + 'aiff', + 'aif', + 'm4v', + 'aac', + 'webm', + 'mpg', + 'mp2', + 'mpeg', + 'mpe', + 'mpv', + 'js', + 'jsx', + 'html', + 'htm', + 'css', + 'json', + 'csv', + 'tsv', + 'obj', + 'svg', + 'otf', + 'ttf', + 'txt', + 'mov', + 'vert', + 'frag', + 'bin' +]; export const mimeTypes = `image/*,audio/*,text/javascript,text/html,text/css, application/json,application/x-font-ttf,application/x-font-truetype,text/plain, text/csv,.obj,video/webm,video/ogg,video/quicktime,video/mp4`; -export const fileExtensions = fileExtensionsArray.map(ext => `.${ext}`).join(','); +export const fileExtensions = fileExtensionsArray + .map((ext) => `.${ext}`) + .join(','); export const fileExtensionsAndMimeTypes = `${fileExtensions},${mimeTypes}`; -export const MEDIA_FILE_REGEX = - new RegExp(`^(?!(http:\\/\\/|https:\\/\\/)).*\\.(${fileExtensionsArray.join('|')})$`, 'i'); +export const MEDIA_FILE_REGEX = new RegExp( + `^(?!(http:\\/\\/|https:\\/\\/)).*\\.(${fileExtensionsArray.join('|')})$`, + 'i' +); -export const MEDIA_FILE_QUOTED_REGEX = - new RegExp(`^('|")(?!(http:\\/\\/|https:\\/\\/)).*\\.(${fileExtensionsArray.join('|')})('|")$`, 'i'); +export const MEDIA_FILE_QUOTED_REGEX = new RegExp( + `^('|")(?!(http:\\/\\/|https:\\/\\/)).*\\.(${fileExtensionsArray.join( + '|' + )})('|")$`, + 'i' +); export const STRING_REGEX = /(['"])((\\\1|.)*?)\1/gm; // these are files that have to be linked to with a blob url diff --git a/server/utils/isAuthenticated.js b/server/utils/isAuthenticated.js index bd3f345481..865075d864 100644 --- a/server/utils/isAuthenticated.js +++ b/server/utils/isAuthenticated.js @@ -8,4 +8,3 @@ export default function isAuthenticated(req, res, next) { message: 'You must be logged in in order to perform the requested action.' }); } - diff --git a/server/utils/mail.js b/server/utils/mail.js index 42c9b317c9..3c17411c5a 100644 --- a/server/utils/mail.js +++ b/server/utils/mail.js @@ -7,14 +7,14 @@ import mg from 'nodemailer-mailgun-transport'; const auth = { api_key: process.env.MAILGUN_KEY, - domain: process.env.MAILGUN_DOMAIN, + domain: process.env.MAILGUN_DOMAIN }; class Mail { constructor() { this.client = nodemailer.createTransport(mg({ auth })); this.sendOptions = { - from: process.env.EMAIL_SENDER, + from: process.env.EMAIL_SENDER }; } @@ -31,13 +31,12 @@ class Mail { to: data.to, subject: data.subject, from: this.sendOptions.from, - html: data.html, + html: data.html }; - return this.sendMail(mailOptions) - .then((err, res) => { - callback(err, res); - }); + return this.sendMail(mailOptions).then((err, res) => { + callback(err, res); + }); } send(data, callback) { diff --git a/server/utils/previewGeneration.js b/server/utils/previewGeneration.js index a8f46e8ddc..22dbcd3a02 100644 --- a/server/utils/previewGeneration.js +++ b/server/utils/previewGeneration.js @@ -61,7 +61,10 @@ export function resolveScripts(sketchDoc, files, projectId) { const scriptsInHTML = sketchDoc.getElementsByTagName('script'); const scriptsInHTMLArray = Array.prototype.slice.call(scriptsInHTML); scriptsInHTMLArray.forEach((script) => { - if (script.getAttribute('src') && script.getAttribute('src').match(NOT_EXTERNAL_LINK_REGEX) !== null) { + if ( + script.getAttribute('src') && + script.getAttribute('src').match(NOT_EXTERNAL_LINK_REGEX) !== null + ) { const resolvedFile = resolvePathToFile(script.getAttribute('src'), files); if (resolvedFile) { if (resolvedFile.url) { @@ -71,8 +74,17 @@ export function resolveScripts(sketchDoc, files, projectId) { script.innerHTML = resolvedFile.content; } } - } else if (!(script.getAttribute('src') && script.getAttribute('src').match(EXTERNAL_LINK_REGEX) !== null)) { - script.innerHTML = resolveLinksInString(script.innerHTML, files, projectId); + } else if ( + !( + script.getAttribute('src') && + script.getAttribute('src').match(EXTERNAL_LINK_REGEX) !== null + ) + ) { + script.innerHTML = resolveLinksInString( + script.innerHTML, + files, + projectId + ); } }); } @@ -87,7 +99,10 @@ export function resolveStyles(sketchDoc, files, projectId) { const cssLinksInHTML = sketchDoc.querySelectorAll('link[rel="stylesheet"]'); const cssLinksInHTMLArray = Array.prototype.slice.call(cssLinksInHTML); cssLinksInHTMLArray.forEach((css) => { - if (css.getAttribute('href') && css.getAttribute('href').match(NOT_EXTERNAL_LINK_REGEX) !== null) { + if ( + css.getAttribute('href') && + css.getAttribute('href').match(NOT_EXTERNAL_LINK_REGEX) !== null + ) { const resolvedFile = resolvePathToFile(css.getAttribute('href'), files); if (resolvedFile) { if (resolvedFile.url) { diff --git a/server/utils/requestsOfType.js b/server/utils/requestsOfType.js index 43bfeb1dc9..f069547218 100644 --- a/server/utils/requestsOfType.js +++ b/server/utils/requestsOfType.js @@ -3,15 +3,21 @@ response if an incoming request's Content-Type header does not match `type` */ -const requestsOfType = type => (req, res, next) => { - const hasContentType = req.get('content-type') !== undefined && req.get('content-type') !== null; +const requestsOfType = (type) => (req, res, next) => { + const hasContentType = + req.get('content-type') !== undefined && req.get('content-type') !== null; const isCorrectType = req.is(type) === null || req.is(type) === type; if (hasContentType && !isCorrectType) { if (process.env.NODE_ENV === 'development') { - console.error(`Requests with a body must be of Content-Type "${type}". Sending HTTP 406`); + console.error( + `Requests with a body must be of Content-Type "${type}". Sending HTTP 406` + ); } - return next({ code: 406, message: `Requests with a body must be of Content-Type "${type}"` }); // 406 UNACCEPTABLE + return next({ + code: 406, + message: `Requests with a body must be of Content-Type "${type}"` + }); // 406 UNACCEPTABLE } return next(); diff --git a/server/views/404Page.js b/server/views/404Page.js index cffa7fa462..7f74372535 100644 --- a/server/views/404Page.js +++ b/server/views/404Page.js @@ -68,37 +68,50 @@ function insertErrorMessage(htmlFile) { } export function get404Sketch(callback) { - User.findOne({ username: 'p5' }, (userErr, user) => { // Find p5 user + User.findOne({ username: 'p5' }, (userErr, user) => { + // Find p5 user if (userErr) { throw userErr; } else if (user) { - Project.find({ user: user._id }, (projErr, projects) => { // Find example projects + Project.find({ user: user._id }, (projErr, projects) => { + // Find example projects // Choose a random sketch const randomIndex = Math.floor(Math.random() * projects.length); const sketch = projects[randomIndex]; let instanceMode = false; // Get sketch files - let htmlFile = sketch.files.filter(file => file.name.match(/.*\.html$/i))[0].content; - const jsFiles = sketch.files.filter(file => file.name.match(/.*\.js$/i)); - const cssFiles = sketch.files.filter(file => file.name.match(/.*\.css$/i)); - const linkedFiles = sketch.files.filter(file => file.url); + let htmlFile = sketch.files.filter((file) => + file.name.match(/.*\.html$/i) + )[0].content; + const jsFiles = sketch.files.filter((file) => + file.name.match(/.*\.js$/i) + ); + const cssFiles = sketch.files.filter((file) => + file.name.match(/.*\.css$/i) + ); + const linkedFiles = sketch.files.filter((file) => file.url); - instanceMode = jsFiles.find(file => file.name === 'sketch.js').content.includes('Instance Mode'); + instanceMode = jsFiles + .find((file) => file.name === 'sketch.js') + .content.includes('Instance Mode'); - jsFiles.forEach((file) => { // Add js files as script tags + jsFiles.forEach((file) => { + // Add js files as script tags const html = htmlFile.split('</body>'); html[0] = `${html[0]}<script>${file.content}</script>`; htmlFile = html.join('</body>'); }); - cssFiles.forEach((file) => { // Add css files as style tags + cssFiles.forEach((file) => { + // Add css files as style tags const html = htmlFile.split('</head>'); html[0] = `${html[0]}<style>${file.content}</style>`; htmlFile = html.join('</head>'); }); - linkedFiles.forEach((file) => { // Add linked files as link tags + linkedFiles.forEach((file) => { + // Add linked files as link tags const html = htmlFile.split('<head>'); html[1] = `<link href=${file.url}>${html[1]}`; htmlFile = html.join('<head>'); @@ -118,22 +131,26 @@ export function get404Sketch(callback) { ); // Change canvas size - htmlFile = htmlFile.replace(/createCanvas\(\d+, ?\d+/g, instanceMode ? - 'createCanvas(p.windowWidth, p.windowHeight' - : - 'createCanvas(windowWidth, windowHeight'); + htmlFile = htmlFile.replace( + /createCanvas\(\d+, ?\d+/g, + instanceMode + ? 'createCanvas(p.windowWidth, p.windowHeight' + : 'createCanvas(windowWidth, windowHeight' + ); callback(htmlFile); }); } else { - callback(insertErrorMessage(`<!DOCTYPE html> + callback( + insertErrorMessage(`<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> </head> <body> </body> - </html>`)); + </html>`) + ); } }); } diff --git a/server/views/consolidationMailLayout.js b/server/views/consolidationMailLayout.js index fa98602485..d2cc518275 100644 --- a/server/views/consolidationMailLayout.js +++ b/server/views/consolidationMailLayout.js @@ -11,7 +11,7 @@ export default ({ resetPasswordText, noteText, meta -}) => ( +}) => ` <mjml> <mj-head> @@ -74,5 +74,4 @@ export default ({ </mj-container> </mj-body> </mjml> -` -); +`; diff --git a/server/views/mail.js b/server/views/mail.js index 07e5c5e5aa..0d478fff94 100644 --- a/server/views/mail.js +++ b/server/views/mail.js @@ -24,8 +24,9 @@ export const renderAccountConsolidation = (data) => { p5.js Web Editor!`, meta: { keywords: 'p5.js, p5.js web editor, web editor, processing, code editor', - description: 'A web editor for p5.js, a JavaScript library with the goal' - + ' of making coding accessible to artists, designers, educators, and beginners.' + description: + 'A web editor for p5.js, a JavaScript library with the goal' + + ' of making coding accessible to artists, designers, educators, and beginners.' } }; @@ -36,11 +37,7 @@ export const renderAccountConsolidation = (data) => { const html = renderMjml(template); // Return options to send mail - return Object.assign( - {}, - data, - { html, subject }, - ); + return Object.assign({}, data, { html, subject }); }; export const renderResetPassword = (data) => { @@ -49,15 +46,18 @@ export const renderResetPassword = (data) => { domain: data.body.domain, headingText: 'Reset your password', greetingText: 'Hello,', - messageText: 'We received a request to reset the password for your account. To reset your password, click on the button below:', // eslint-disable-line max-len + messageText: + 'We received a request to reset the password for your account. To reset your password, click on the button below:', // eslint-disable-line max-len link: data.body.link, buttonText: 'Reset password', directLinkText: 'Or copy and paste the URL into your browser:', - noteText: 'If you did not request this, please ignore this email and your password will remain unchanged. Thanks for using the p5.js Web Editor!', // eslint-disable-line max-len + noteText: + 'If you did not request this, please ignore this email and your password will remain unchanged. Thanks for using the p5.js Web Editor!', // eslint-disable-line max-len meta: { keywords: 'p5.js, p5.js web editor, web editor, processing, code editor', - description: 'A web editor for p5.js, a JavaScript library with the goal' - + ' of making coding accessible to artists, designers, educators, and beginners.' + description: + 'A web editor for p5.js, a JavaScript library with the goal' + + ' of making coding accessible to artists, designers, educators, and beginners.' } }; @@ -68,11 +68,7 @@ export const renderResetPassword = (data) => { const html = renderMjml(template); // Return options to send mail - return Object.assign( - {}, - data, - { html, subject }, - ); + return Object.assign({}, data, { html, subject }); }; export const renderEmailConfirmation = (data) => { @@ -85,11 +81,13 @@ export const renderEmailConfirmation = (data) => { link: data.body.link, buttonText: 'Verify Email', directLinkText: 'Or copy and paste the URL into your browser:', - noteText: 'This link is only valid for the next 24 hours. Thanks for using the p5.js Web Editor!', + noteText: + 'This link is only valid for the next 24 hours. Thanks for using the p5.js Web Editor!', meta: { keywords: 'p5.js, p5.js web editor, web editor, processing, code editor', - description: 'A web editor for p5.js, a JavaScript library with the goal' - + ' of making coding accessible to artists, designers, educators, and beginners.' + description: + 'A web editor for p5.js, a JavaScript library with the goal' + + ' of making coding accessible to artists, designers, educators, and beginners.' } }; @@ -100,9 +98,5 @@ export const renderEmailConfirmation = (data) => { const html = renderMjml(template); // Return options to send mail - return Object.assign( - {}, - data, - { html, subject }, - ); + return Object.assign({}, data, { html, subject }); }; diff --git a/server/views/mailLayout.js b/server/views/mailLayout.js index 4fca954de0..6a21e77d26 100644 --- a/server/views/mailLayout.js +++ b/server/views/mailLayout.js @@ -8,7 +8,7 @@ export default ({ directLinkText, noteText, meta -}) => ( +}) => ` <mjml> <mj-head> @@ -62,5 +62,4 @@ export default ({ </mj-container> </mj-body> </mjml> -` -); +`; From 45ffaa26bcbf324078dcc5ae7b8c61ac133c416c Mon Sep 17 00:00:00 2001 From: Cassie Tarakajian <ctarakajian@gmail.com> Date: Wed, 27 Jan 2021 14:00:47 -0500 Subject: [PATCH 3/4] [#841] Remove eslint and prettier conflicts --- .eslintrc | 4 +- .prettierrc | 34 +++---- client/common/Button.jsx | 32 +++---- client/components/Nav.jsx | 60 ++++++------ client/components/mobile/Tab.jsx | 6 +- client/i18n.js | 4 +- .../CollectionList/CollectionList.jsx | 32 +++---- client/modules/IDE/components/Editor.jsx | 11 +-- client/modules/IDE/components/Feedback.jsx | 4 +- client/modules/IDE/components/Sidebar.jsx | 2 +- client/modules/IDE/components/SketchList.jsx | 24 ++--- client/modules/IDE/pages/IDEView.jsx | 3 +- client/modules/IDE/pages/MobileIDEView.jsx | 94 +++++++++---------- client/modules/User/components/Collection.jsx | 26 ++--- client/modules/User/pages/AccountView.jsx | 10 +- client/modules/User/pages/NewPasswordView.jsx | 2 +- .../modules/User/pages/ResetPasswordView.jsx | 2 +- server/config/passport.js | 21 +++-- server/controllers/session.controller.js | 12 ++- .../domain-objects/__test__/Project.test.js | 2 +- server/models/user.js | 26 ++--- 21 files changed, 209 insertions(+), 202 deletions(-) diff --git a/.eslintrc b/.eslintrc index b2be2ffa01..bc716af7d6 100644 --- a/.eslintrc +++ b/.eslintrc @@ -16,12 +16,12 @@ "import/no-unresolved": 0, "import/no-named-as-default": 2, "comma-dangle": 0, // not sure why airbnb turned this on. gross! - "indent": [2, 2, {"SwitchCase": 1}], + "indent": 0, "no-console": 0, "no-alert": 0, "no-underscore-dangle": 0, "max-len": [1, 120, 2, {"ignoreComments": true, "ignoreTemplateLiterals": true}], - "quote-props": [1, "consistent-as-needed"], + "quote-props": [1, "as-needed"], "no-unused-vars": [1, {"vars": "local", "args": "none"}], "consistent-return": ["error", { "treatUndefinedAsUnspecified": true }], "no-param-reassign": [2, { "props": false }], diff --git a/.prettierrc b/.prettierrc index 51e47c9b88..be8ed7938d 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,18 +1,18 @@ { - "arrowParens": "always", - "bracketSpacing": true, - "htmlWhitespaceSensitivity": "css", - "insertPragma": false, - "jsxBracketSameLine": false, - "jsxSingleQuote": false, - "parser": "babel", - "printWidth": 80, - "proseWrap": "never", - "requirePragma": false, - "semi": true, - "singleQuote": true, - "tabWidth": 2, - "trailingComma": "none", - "useTabs": false, - "quoteProps": "as-needed" - } \ No newline at end of file + "arrowParens": "always", + "bracketSpacing": true, + "htmlWhitespaceSensitivity": "css", + "insertPragma": false, + "jsxBracketSameLine": false, + "jsxSingleQuote": false, + "parser": "babel", + "printWidth": 80, + "proseWrap": "never", + "requirePragma": false, + "semi": true, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "none", + "useTabs": false, + "quoteProps": "as-needed" +} \ No newline at end of file diff --git a/client/common/Button.jsx b/client/common/Button.jsx index 09815ff997..3009ab338b 100644 --- a/client/common/Button.jsx +++ b/client/common/Button.jsx @@ -212,15 +212,15 @@ const Button = ({ }; Button.defaultProps = { - "children": null, - "disabled": false, - "iconAfter": null, - "iconBefore": null, - "kind": kinds.block, - "href": null, + children: null, + disabled: false, + iconAfter: null, + iconBefore: null, + kind: kinds.block, + href: null, 'aria-label': null, - "to": null, - "type": 'button' + to: null, + type: 'button' }; Button.kinds = kinds; @@ -230,27 +230,27 @@ Button.propTypes = { * The visible part of the button, telling the user what * the action is */ - "children": PropTypes.element, + children: PropTypes.element, /** If the button can be activated or not */ - "disabled": PropTypes.bool, + disabled: PropTypes.bool, /** * SVG icon to place after child content */ - "iconAfter": PropTypes.element, + iconAfter: PropTypes.element, /** * SVG icon to place before child content */ - "iconBefore": PropTypes.element, + iconBefore: PropTypes.element, /** * The kind of button - determines how it appears visually */ - "kind": PropTypes.oneOf(Object.values(kinds)), + kind: PropTypes.oneOf(Object.values(kinds)), /** * Specifying an href will use an <a> to link to the URL */ - "href": PropTypes.string, + href: PropTypes.string, /* * An ARIA Label used for accessibility */ @@ -258,11 +258,11 @@ Button.propTypes = { /** * Specifying a to URL will use a react-router Link */ - "to": PropTypes.string, + to: PropTypes.string, /** * If using a button, then type is defines the type of button */ - "type": PropTypes.oneOf(['button', 'submit']) + type: PropTypes.oneOf(['button', 'submit']) }; export default Button; diff --git a/client/components/Nav.jsx b/client/components/Nav.jsx index 1bb435e72e..9ad07edec3 100644 --- a/client/components/Nav.jsx +++ b/client/components/Nav.jsx @@ -317,19 +317,19 @@ class Nav extends React.PureComponent { </li> {getConfig('LOGIN_ENABLED') && (!this.props.project.owner || this.props.isUserOwner) && ( - <li className="nav__dropdown-item"> - <button - onClick={this.handleSave} - onFocus={this.handleFocusForFile} - onBlur={this.handleBlur} - > - {this.props.t('Common.Save')} - <span className="nav__keyboard-shortcut"> - {metaKeyName}+S - </span> - </button> - </li> - )} + <li className="nav__dropdown-item"> + <button + onClick={this.handleSave} + onFocus={this.handleFocusForFile} + onBlur={this.handleBlur} + > + {this.props.t('Common.Save')} + <span className="nav__keyboard-shortcut"> + {metaKeyName}+S + </span> + </button> + </li> + )} {this.props.project.id && this.props.user.authenticated && ( <li className="nav__dropdown-item"> <button @@ -378,17 +378,17 @@ class Nav extends React.PureComponent { {getConfig('UI_COLLECTIONS_ENABLED') && this.props.user.authenticated && this.props.project.id && ( - <li className="nav__dropdown-item"> - <Link - to={`/${this.props.user.username}/sketches/${this.props.project.id}/add-to-collection`} - onFocus={this.handleFocusForFile} - onBlur={this.handleBlur} - onClick={this.setDropdownForNone} - > - {this.props.t('Nav.File.AddToCollection')} - </Link> - </li> - )} + <li className="nav__dropdown-item"> + <Link + to={`/${this.props.user.username}/sketches/${this.props.project.id}/add-to-collection`} + onFocus={this.handleFocusForFile} + onBlur={this.handleBlur} + onClick={this.setDropdownForNone} + > + {this.props.t('Nav.File.AddToCollection')} + </Link> + </li> + )} {getConfig('EXAMPLES_ENABLED') && ( <li className="nav__dropdown-item"> <Link @@ -819,27 +819,27 @@ class Nav extends React.PureComponent { render() { const navDropdownState = { file: classNames({ - "nav__item": true, + nav__item: true, 'nav__item--open': this.state.dropdownOpen === 'file' }), edit: classNames({ - "nav__item": true, + nav__item: true, 'nav__item--open': this.state.dropdownOpen === 'edit' }), sketch: classNames({ - "nav__item": true, + nav__item: true, 'nav__item--open': this.state.dropdownOpen === 'sketch' }), help: classNames({ - "nav__item": true, + nav__item: true, 'nav__item--open': this.state.dropdownOpen === 'help' }), account: classNames({ - "nav__item": true, + nav__item: true, 'nav__item--open': this.state.dropdownOpen === 'account' }), lang: classNames({ - "nav__item": true, + nav__item: true, 'nav__item--open': this.state.dropdownOpen === 'lang' }) }; diff --git a/client/components/mobile/Tab.jsx b/client/components/mobile/Tab.jsx index cb29c43690..c7ed284f06 100644 --- a/client/components/mobile/Tab.jsx +++ b/client/components/mobile/Tab.jsx @@ -8,9 +8,9 @@ export default styled(Link)` background: transparent; /* border-top: ${remSize(4)} solid ${(props) => - prop( - props.selected ? 'colors.p5jsPink' : 'MobilePanel.default.background' - )}; */ + prop( + props.selected ? 'colors.p5jsPink' : 'MobilePanel.default.background' + )}; */ border-top: ${remSize(4)} solid ${(props) => (props.selected ? prop('TabHighlight') : 'transparent')}; diff --git a/client/i18n.js b/client/i18n.js index 102a0e8b9e..ddf6b88705 100644 --- a/client/i18n.js +++ b/client/i18n.js @@ -11,7 +11,7 @@ export function languageKeyToLabel(lang) { const languageMap = { 'en-US': 'English', 'es-419': 'Español', - "ja": '日本語' + ja: '日本語' }; return languageMap[lang]; } @@ -20,7 +20,7 @@ export function languageKeyToDateLocale(lang) { const languageMap = { 'en-US': enUS, 'es-419': es, - "ja": ja + ja }; return languageMap[lang]; } diff --git a/client/modules/IDE/components/CollectionList/CollectionList.jsx b/client/modules/IDE/components/CollectionList/CollectionList.jsx index 6fd5193004..646b9824b5 100644 --- a/client/modules/IDE/components/CollectionList/CollectionList.jsx +++ b/client/modules/IDE/components/CollectionList/CollectionList.jsx @@ -134,24 +134,24 @@ class CollectionList extends React.Component { <span className={headerClass}>{displayName}</span> {field === fieldName && direction === SortingActions.DIRECTION.ASC && ( - <ArrowUpIcon - role="img" - aria-label={this.props.t( - 'CollectionList.DirectionAscendingARIA' - )} - focusable="false" - /> - )} + <ArrowUpIcon + role="img" + aria-label={this.props.t( + 'CollectionList.DirectionAscendingARIA' + )} + focusable="false" + /> + )} {field === fieldName && direction === SortingActions.DIRECTION.DESC && ( - <ArrowDownIcon - role="img" - aria-label={this.props.t( - 'CollectionList.DirectionDescendingARIA' - )} - focusable="false" - /> - )} + <ArrowDownIcon + role="img" + aria-label={this.props.t( + 'CollectionList.DirectionDescendingARIA' + )} + focusable="false" + /> + )} </button> </th> ); diff --git a/client/modules/IDE/components/Editor.jsx b/client/modules/IDE/components/Editor.jsx index 2594564c80..6ad3593aac 100644 --- a/client/modules/IDE/components/Editor.jsx +++ b/client/modules/IDE/components/Editor.jsx @@ -114,10 +114,10 @@ class Editor extends React.Component { this.updateLintingMessageAccessibility(annotations); }, options: { - "asi": true, - "eqeqeq": false, + asi: true, + eqeqeq: false, '-W041': false, - "esversion": 7 + esversion: 7 } } }); @@ -365,7 +365,7 @@ class Editor extends React.Component { render() { const editorSectionClass = classNames({ - "editor": true, + editor: true, 'sidebar--contracted': !this.props.isExpanded, 'editor--options': this.props.editorOptionsVisible }); @@ -417,8 +417,7 @@ class Editor extends React.Component { this.codemirrorContainer = element; }} className={editorHolderClass} - > - </article> + /> <EditorAccessibility lintMessages={this.props.lintMessages} /> </section> ); diff --git a/client/modules/IDE/components/Feedback.jsx b/client/modules/IDE/components/Feedback.jsx index bcd5efea77..9739ad1252 100644 --- a/client/modules/IDE/components/Feedback.jsx +++ b/client/modules/IDE/components/Feedback.jsx @@ -12,8 +12,8 @@ function Feedback(props) { <div className="feedback__content-pane"> <h2 className="feedback__content-pane-header">Via Github Issues</h2> <p className="feedback__content-pane-copy"> - If you're familiar with Github, this is our preferred method for - receiving bug reports and feedback. + {`If you're familiar with Github, this is our preferred method for + receiving bug reports and feedback.`} </p> <p className="feedback__content-pane-copy"> <a diff --git a/client/modules/IDE/components/Sidebar.jsx b/client/modules/IDE/components/Sidebar.jsx index 885907578c..1553c40361 100644 --- a/client/modules/IDE/components/Sidebar.jsx +++ b/client/modules/IDE/components/Sidebar.jsx @@ -65,7 +65,7 @@ class Sidebar extends React.Component { render() { const canEditProject = this.userCanEditProject(); const sidebarClass = classNames({ - "sidebar": true, + sidebar: true, 'sidebar--contracted': !this.props.isExpanded, 'sidebar--project-options': this.props.projectOptionsVisible, 'sidebar--cant-edit': !canEditProject diff --git a/client/modules/IDE/components/SketchList.jsx b/client/modules/IDE/components/SketchList.jsx index 98abd67340..757bacd904 100644 --- a/client/modules/IDE/components/SketchList.jsx +++ b/client/modules/IDE/components/SketchList.jsx @@ -453,20 +453,20 @@ class SketchList extends React.Component { <span className={headerClass}>{displayName}</span> {field === fieldName && direction === SortingActions.DIRECTION.ASC && ( - <ArrowUpIcon - role="img" - aria-label={this.props.t('SketchList.DirectionAscendingARIA')} - focusable="false" - /> - )} + <ArrowUpIcon + role="img" + aria-label={this.props.t('SketchList.DirectionAscendingARIA')} + focusable="false" + /> + )} {field === fieldName && direction === SortingActions.DIRECTION.DESC && ( - <ArrowDownIcon - role="img" - aria-label={this.props.t('SketchList.DirectionDescendingARIA')} - focusable="false" - /> - )} + <ArrowDownIcon + role="img" + aria-label={this.props.t('SketchList.DirectionDescendingARIA')} + focusable="false" + /> + )} </button> </th> ); diff --git a/client/modules/IDE/pages/IDEView.jsx b/client/modules/IDE/pages/IDEView.jsx index fd1036e772..91323cbb2c 100644 --- a/client/modules/IDE/pages/IDEView.jsx +++ b/client/modules/IDE/pages/IDEView.jsx @@ -362,8 +362,7 @@ class IDEView extends React.Component { ref={(element) => { this.overlay = element; }} - > - </div> + /> <div> {((this.props.preferences.textOutput || this.props.preferences.gridOutput || diff --git a/client/modules/IDE/pages/MobileIDEView.jsx b/client/modules/IDE/pages/MobileIDEView.jsx index 7ca51eae7c..f504eb7560 100644 --- a/client/modules/IDE/pages/MobileIDEView.jsx +++ b/client/modules/IDE/pages/MobileIDEView.jsx @@ -85,54 +85,54 @@ const getNavOptions = ( const { t } = useTranslation(); return username ? [ - { - icon: PreferencesIcon, - title: t('MobileIDEView.Preferences'), - href: '/preferences' - }, - { - icon: PreferencesIcon, - title: t('MobileIDEView.MyStuff'), - href: `/${username}/sketches` - }, - { - icon: PreferencesIcon, - title: t('MobileIDEView.Examples'), - href: '/p5/sketches' - }, - { - icon: PreferencesIcon, - title: t('MobileIDEView.OriginalEditor'), - action: toggleForceDesktop - }, - { - icon: PreferencesIcon, - title: t('MobileIDEView.Logout'), - action: logoutUser - } - ] + { + icon: PreferencesIcon, + title: t('MobileIDEView.Preferences'), + href: '/preferences' + }, + { + icon: PreferencesIcon, + title: t('MobileIDEView.MyStuff'), + href: `/${username}/sketches` + }, + { + icon: PreferencesIcon, + title: t('MobileIDEView.Examples'), + href: '/p5/sketches' + }, + { + icon: PreferencesIcon, + title: t('MobileIDEView.OriginalEditor'), + action: toggleForceDesktop + }, + { + icon: PreferencesIcon, + title: t('MobileIDEView.Logout'), + action: logoutUser + } + ] : [ - { - icon: PreferencesIcon, - title: t('MobileIDEView.Preferences'), - href: '/preferences' - }, - { - icon: PreferencesIcon, - title: t('MobileIDEView.Examples'), - href: '/p5/sketches' - }, - { - icon: PreferencesIcon, - title: t('MobileIDEView.OriginalEditor'), - action: toggleForceDesktop - }, - { - icon: PreferencesIcon, - title: t('MobileIDEView.Login'), - href: '/login' - } - ]; + { + icon: PreferencesIcon, + title: t('MobileIDEView.Preferences'), + href: '/preferences' + }, + { + icon: PreferencesIcon, + title: t('MobileIDEView.Examples'), + href: '/p5/sketches' + }, + { + icon: PreferencesIcon, + title: t('MobileIDEView.OriginalEditor'), + action: toggleForceDesktop + }, + { + icon: PreferencesIcon, + title: t('MobileIDEView.Login'), + href: '/login' + } + ]; }; const canSaveProject = (isUserOwner, project, user) => diff --git a/client/modules/User/components/Collection.jsx b/client/modules/User/components/Collection.jsx index 9aee42609e..5c44bd1d14 100644 --- a/client/modules/User/components/Collection.jsx +++ b/client/modules/User/components/Collection.jsx @@ -378,7 +378,7 @@ class Collection extends React.Component { _renderFieldHeader(fieldName, displayName) { const { field, direction } = this.props.sorting; const headerClass = classNames({ - "arrowDown": true, + arrowDown: true, 'sketches-table__header--selected': field === fieldName }); const buttonLabel = this._getButtonLabel(fieldName, displayName); @@ -392,20 +392,20 @@ class Collection extends React.Component { <span className={headerClass}>{displayName}</span> {field === fieldName && direction === SortingActions.DIRECTION.ASC && ( - <ArrowUpIcon - role="img" - aria-label={this.props.t('Collection.DirectionAscendingARIA')} - focusable="false" - /> - )} + <ArrowUpIcon + role="img" + aria-label={this.props.t('Collection.DirectionAscendingARIA')} + focusable="false" + /> + )} {field === fieldName && direction === SortingActions.DIRECTION.DESC && ( - <ArrowDownIcon - role="img" - aria-label={this.props.t('Collection.DirectionDescendingARIA')} - focusable="false" - /> - )} + <ArrowDownIcon + role="img" + aria-label={this.props.t('Collection.DirectionDescendingARIA')} + focusable="false" + /> + )} </button> </th> ); diff --git a/client/modules/User/pages/AccountView.jsx b/client/modules/User/pages/AccountView.jsx index bb3ca46456..a388997af4 100644 --- a/client/modules/User/pages/AccountView.jsx +++ b/client/modules/User/pages/AccountView.jsx @@ -17,17 +17,16 @@ import Overlay from '../../App/components/Overlay'; import Toast from '../../IDE/components/Toast'; function SocialLoginPanel(props) { - const { user } = props; + const { user, t } = props; return ( <React.Fragment> <AccountForm /> - {/* eslint-disable-next-line react/prop-types */} <h2 className="form-container__divider"> - {props.t('AccountView.SocialLogin')} + {t('AccountView.SocialLogin')} </h2> <p className="account__social-text"> {/* eslint-disable-next-line react/prop-types */} - {props.t('AccountView.SocialLoginDescription')} + {t('AccountView.SocialLoginDescription')} </p> <div className="account__social-stack"> <SocialAuthButton @@ -49,7 +48,8 @@ SocialLoginPanel.propTypes = { user: PropTypes.shape({ github: PropTypes.string, google: PropTypes.string - }).isRequired + }).isRequired, + t: PropTypes.func.isRequired }; class AccountView extends React.Component { diff --git a/client/modules/User/pages/NewPasswordView.jsx b/client/modules/User/pages/NewPasswordView.jsx index ec8b2ca0e1..506471bf5c 100644 --- a/client/modules/User/pages/NewPasswordView.jsx +++ b/client/modules/User/pages/NewPasswordView.jsx @@ -24,7 +24,7 @@ function NewPasswordView(props) { 'new-password': true, 'new-password--invalid': resetPasswordInvalid, 'form-container': true, - "user": true + user: true }); return ( <div className="new-password-container"> diff --git a/client/modules/User/pages/ResetPasswordView.jsx b/client/modules/User/pages/ResetPasswordView.jsx index 7c5942373b..d3af6c125c 100644 --- a/client/modules/User/pages/ResetPasswordView.jsx +++ b/client/modules/User/pages/ResetPasswordView.jsx @@ -16,7 +16,7 @@ function ResetPasswordView() { 'reset-password': true, 'reset-password--submitted': resetPasswordInitiate, 'form-container': true, - "user": true + user: true }); return ( <div className="reset-password-container"> diff --git a/server/config/passport.js b/server/config/passport.js index 2d23628466..1042cfcc6d 100644 --- a/server/config/passport.js +++ b/server/config/passport.js @@ -35,15 +35,16 @@ passport.use( new LocalStrategy({ usernameField: 'email' }, (email, password, done) => { User.findByEmailOrUsername(email) .then((user) => { - // eslint-disable-line consistent-return if (!user) { - return done(null, false, { msg: `Email ${email} not found.` }); + done(null, false, { msg: `Email ${email} not found.` }); + return; } user.comparePassword(password, (innerErr, isMatch) => { if (isMatch) { - return done(null, user); + done(null, user); + return; } - return done(null, false, { msg: 'Invalid email or password.' }); + done(null, false, { msg: 'Invalid email or password.' }); }); }) .catch((err) => done(null, false, { msg: err })); @@ -56,20 +57,22 @@ passport.use( passport.use( new BasicStrategy((userid, key, done) => { User.findByUsername(userid, (err, user) => { - // eslint-disable-line consistent-return if (err) { - return done(err); + done(err); + return; } if (!user) { - return done(null, false); + done(null, false); + return; } user.findMatchingKey(key, (innerErr, isMatch, keyDocument) => { if (isMatch) { keyDocument.lastUsedAt = Date.now(); user.save(); - return done(null, user); + done(null, user); + return; } - return done(null, false, { msg: 'Invalid username or API key' }); + done(null, false, { msg: 'Invalid username or API key' }); }); }); }) diff --git a/server/controllers/session.controller.js b/server/controllers/session.controller.js index 55f116cb00..c08c8cc124 100644 --- a/server/controllers/session.controller.js +++ b/server/controllers/session.controller.js @@ -4,19 +4,21 @@ import { userResponse } from './user.controller'; export function createSession(req, res, next) { passport.authenticate('local', (err, user) => { - // eslint-disable-line consistent-return if (err) { - return next(err); + next(err); + return; } if (!user) { - return res.status(401).json({ message: 'Invalid username or password.' }); + res.status(401).json({ message: 'Invalid username or password.' }); + return; } req.logIn(user, (innerErr) => { if (innerErr) { - return next(innerErr); + next(innerErr); + return; } - return res.json(userResponse(req.user)); + res.json(userResponse(req.user)); }); })(req, res, next); } diff --git a/server/domain-objects/__test__/Project.test.js b/server/domain-objects/__test__/Project.test.js index 1fbb7ff3b5..f0f0c43369 100644 --- a/server/domain-objects/__test__/Project.test.js +++ b/server/domain-objects/__test__/Project.test.js @@ -330,7 +330,7 @@ describe('transformFiles', () => { 'index.html': { content: 'some contents' }, - "data": { + data: { files: { 'index.html': { content: 'different file' diff --git a/server/models/user.js b/server/models/user.js index a582ebf942..57549c40ee 100644 --- a/server/models/user.js +++ b/server/models/user.js @@ -86,22 +86,23 @@ const userSchema = new Schema( * Password hash middleware. */ userSchema.pre('save', function checkPassword(next) { - // eslint-disable-line consistent-return const user = this; if (!user.isModified('password')) { - return next(); + next(); + return; } bcrypt.genSalt(10, (err, salt) => { - // eslint-disable-line consistent-return if (err) { - return next(err); + next(err); + return; } bcrypt.hash(user.password, salt, null, (innerErr, hash) => { if (innerErr) { - return next(innerErr); + next(innerErr); + return; } user.password = hash; - return next(); + next(); }); }); }); @@ -113,7 +114,8 @@ userSchema.pre('save', function checkApiKey(next) { // eslint-disable-line consistent-return const user = this; if (!user.isModified('apiKeys')) { - return next(); + next(); + return; } let hasNew = false; user.apiKeys.forEach((k) => { @@ -122,19 +124,21 @@ userSchema.pre('save', function checkApiKey(next) { bcrypt.genSalt(10, (err, salt) => { // eslint-disable-line consistent-return if (err) { - return next(err); + next(err); + return; } bcrypt.hash(k.hashedKey, salt, null, (innerErr, hash) => { if (innerErr) { - return next(innerErr); + next(innerErr); + return; } k.hashedKey = hash; - return next(); + next(); }); }); } }); - if (!hasNew) return next(); + if (!hasNew) next(); }); userSchema.virtual('id').get(function idToString() { From 75e0ab92426a00d0e2d859939f4e9071f73a38d1 Mon Sep 17 00:00:00 2001 From: Cassie Tarakajian <ctarakajian@gmail.com> Date: Wed, 27 Jan 2021 14:22:25 -0500 Subject: [PATCH 4/4] [#861] Add instructions to install prettier to code editor --- developer_docs/installation.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/developer_docs/installation.md b/developer_docs/installation.md index 01f53f3ad3..cee864c84e 100644 --- a/developer_docs/installation.md +++ b/developer_docs/installation.md @@ -27,11 +27,11 @@ _Note_: The installation steps assume you are using a Unix-like shell. If you ar 7. `$ cp .env.example .env` 8. (Optional) Update `.env` with necessary keys to enable certain app behaviors, i.e. add Github ID and Github Secret if you want to be able to log in with Github. 9. Run `$ npm run fetch-examples` to download the example sketches into a user called 'p5'. Note that you need to configure your GitHub Credentials, which you can do by following the [Github API Configuration](#github-api-configuration) section. - -10. `$ npm start` -11. Navigate to [http://localhost:8000](http://localhost:8000) in your browser -12. Install the [React Developer Tools](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi?hl=en) -13. Open and close the Redux DevTools using `ctrl+h`, and move them with `ctrl+w` +10. Enable Prettier in your text editor by following [this guide](https://prettier.io/docs/en/editors.html). +11. `$ npm start` +12. Navigate to [http://localhost:8000](http://localhost:8000) in your browser +13. Install the [React Developer Tools](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi?hl=en) +14. Open and close the Redux DevTools using `ctrl+h`, and move them with `ctrl+w` ## Docker Installation @@ -49,20 +49,20 @@ Note that this takes up a significant amount of space on your machine. Make sure 4. `$ cp .env.example .env` 5. (Optional) Update `.env` with necessary keys to enable certain app behavoirs, i.e. add Github ID and Github Secret if you want to be able to log in with Github. 6. `$ docker-compose -f docker-compose-development.yml run --rm app npm run fetch-examples` - note that you need to configure your GitHub Credentials, which you can do by following the [Github API Configuration](#github-api-configuration) section. - +7. Enable Prettier in your text editor by following [this guide](https://prettier.io/docs/en/editors.html). Now, anytime you wish to start the server with its dependencies, you can run: -7. `$ docker-compose -f docker-compose-development.yml up` -8. Navigate to [http://localhost:8000](http://localhost:8000) in your browser +8. `$ docker-compose -f docker-compose-development.yml up` +9. Navigate to [http://localhost:8000](http://localhost:8000) in your browser To open a terminal/shell in the running Docker server (i.e. after `docker-compose up` has been run): -9. `$ docker-compose -f docker-compose-development.yml exec app bash -l` +10. `$ docker-compose -f docker-compose-development.yml exec app bash -l` If you don't have the full server environment running, you can launch a one-off container instance (and have it automatically deleted after you're done using it): -10. `$ docker-compose -f docker-compose-development.yml run app --rm bash -l` +11. `$ docker-compose -f docker-compose-development.yml run app --rm bash -l` ## S3 Bucket Configuration