diff --git a/.babelrc b/.babelrc index 5f3130eafe..7ef51374cd 100644 --- a/.babelrc +++ b/.babelrc @@ -51,7 +51,16 @@ "development": { "plugins": [ "babel-plugin-styled-components", - "react-hot-loader/babel" + "react-refresh/babel" + ], + "presets": [ + [ + "@babel/preset-react", + { + "development": true, + "runtime": "automatic" + } + ] ] } }, diff --git a/.eslintrc b/.eslintrc index 02de296973..14122f0c17 100644 --- a/.eslintrc +++ b/.eslintrc @@ -15,12 +15,22 @@ "import/namespace": 0, "import/no-unresolved": 0, "import/no-named-as-default": 2, + "import/no-cycle": 0, + "import/no-import-module-exports": 1, + "import/no-useless-path-segments": 1, "comma-dangle": 0, // not sure why airbnb turned this on. gross! + "default-param-last": 0, + "no-else-return": 0, "indent": 0, "no-console": 0, "no-alert": 0, + "no-import-assign": 2, + "no-promise-executor-return": 1, + "no-restricted-exports": 1, "no-underscore-dangle": 0, + "prefer-object-spread": 0, "max-len": [1, 120, 2, {"ignoreComments": true, "ignoreTemplateLiterals": true}], + "max-classes-per-file": 0, "quote-props": [1, "as-needed"], "no-unused-vars": [1, {"vars": "local", "args": "none"}], "consistent-return": ["error", { "treatUndefinedAsUnspecified": true }], @@ -30,11 +40,26 @@ "html": false }], "newline-per-chained-call": 0, + "lines-between-class-members": 0, "react/prefer-stateless-function": [2, { "ignorePureComponents": true }], "class-methods-use-this": 0, - "react/jsx-no-bind": [2, {"allowBind": true, "allowArrowFunctions": true}], + "react/button-has-type": 0, + "react/destructuring-assignment": 0, + "react/function-component-definition": 0, + "react/jsx-curly-newline": 0, + "react/jsx-fragments": 0, + "react/jsx-no-bind": 0, + "react/jsx-no-useless-fragment": 1, + "react/jsx-one-expression-per-line": 0, + "react/jsx-props-no-spreading": 0, + "react/jsx-wrap-multilines": 0, + "react/no-access-state-in-setstate": 1, + "react/no-deprecated": 1, + "react/no-unused-class-component-methods": 1, + "react/sort-comp": 0, + "react/static-property-placement": 1, "no-return-assign": [2, "except-parens"], "jsx-a11y/anchor-is-valid": [ "error", @@ -61,6 +86,8 @@ "allowChildren": false } ], + "jsx-a11y/control-has-associated-label": 1, + "jsx-a11y/label-has-associated-control": 1, "prettier/prettier": [ "error" ] diff --git a/.prettierrc b/.prettierrc index e2da8bb25a..2228d55dc4 100644 --- a/.prettierrc +++ b/.prettierrc @@ -15,5 +15,5 @@ "trailingComma": "none", "useTabs": false, "quoteProps": "as-needed", - "endOfLine":"auto" -} \ No newline at end of file + "endOfLine": "auto" +} diff --git a/client/common/Button.jsx b/client/common/Button.jsx index b7f3c3bddd..d6dd18491e 100644 --- a/client/common/Button.jsx +++ b/client/common/Button.jsx @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import styled from 'styled-components'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; import { remSize, prop } from '../theme'; diff --git a/client/common/ButtonOrLink.jsx b/client/common/ButtonOrLink.jsx index f2c31e1c6e..924f108024 100644 --- a/client/common/ButtonOrLink.jsx +++ b/client/common/ButtonOrLink.jsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; import PropTypes from 'prop-types'; /** diff --git a/client/common/FinalFormField.jsx b/client/common/FinalFormField.jsx new file mode 100644 index 0000000000..74914cc292 --- /dev/null +++ b/client/common/FinalFormField.jsx @@ -0,0 +1,51 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { Field } from 'react-final-form'; +import FormField from './FormField'; + +function FinalFormField({ + name, + validate, + validateFields, + initialValue, + ...rest +}) { + return ( + + {(field) => ( + + )} + + ); +} + +FinalFormField.propTypes = { + id: PropTypes.string.isRequired, + label: PropTypes.string.isRequired, + ariaLabel: PropTypes.string.isRequired, + type: PropTypes.string.isRequired, + autoComplete: PropTypes.string, + name: PropTypes.string.isRequired, + validate: PropTypes.func, + validateFields: PropTypes.arrayOf(PropTypes.string), + initialValue: PropTypes.string +}; + +FinalFormField.defaultProps = { + autoComplete: undefined, + validate: undefined, + validateFields: undefined, + initialValue: '' +}; + +export default FinalFormField; diff --git a/client/common/FormField.jsx b/client/common/FormField.jsx new file mode 100644 index 0000000000..c0f5d89767 --- /dev/null +++ b/client/common/FormField.jsx @@ -0,0 +1,32 @@ +import PropTypes from 'prop-types'; +import React from 'react'; + +function FormField({ id, label, ariaLabel, hasError, error, ...rest }) { + return ( +

+ + + {hasError && {error}} +

+ ); +} + +FormField.propTypes = { + id: PropTypes.string.isRequired, + label: PropTypes.string.isRequired, + ariaLabel: PropTypes.string.isRequired, + type: PropTypes.string.isRequired, + autoComplete: PropTypes.string, + hasError: PropTypes.bool, + error: PropTypes.string +}; + +FormField.defaultProps = { + autoComplete: null, + hasError: false, + error: null +}; + +export default FormField; diff --git a/client/common/icons.jsx b/client/common/icons.jsx index ff526c63f4..71d746b150 100644 --- a/client/common/icons.jsx +++ b/client/common/icons.jsx @@ -13,6 +13,8 @@ import DropdownArrow from '../images/down-filled-triangle.svg'; import Preferences from '../images/preferences.svg'; import Play from '../images/triangle-arrow-right.svg'; import More from '../images/more.svg'; +import Editor from '../images/editor.svg'; +import Account from '../images/account.svg'; import Code from '../images/code.svg'; import Save from '../images/save.svg'; import Terminal from '../images/terminal.svg'; @@ -87,12 +89,12 @@ export const DropdownArrowIcon = withLabel(DropdownArrow); export const PreferencesIcon = withLabel(Preferences); export const PlayIcon = withLabel(Play); export const MoreIcon = withLabel(More); +export const EditorIcon = withLabel(Editor); export const TerminalIcon = withLabel(Terminal); export const CodeIcon = withLabel(Code); export const SaveIcon = withLabel(Save); - export const FolderIcon = withLabel(Folder); - +export const AccountIcon = withLabel(Account); export const CircleTerminalIcon = withLabel(CircleTerminal); export const CircleFolderIcon = withLabel(CircleFolder); export const CircleInfoIcon = withLabel(CircleInfo); diff --git a/client/components/Nav.jsx b/client/components/Nav.jsx deleted file mode 100644 index 2320111bce..0000000000 --- a/client/components/Nav.jsx +++ /dev/null @@ -1,420 +0,0 @@ -import { sortBy } from 'lodash'; -import PropTypes from 'prop-types'; -import React from 'react'; -import { withTranslation } from 'react-i18next'; -import { connect } from 'react-redux'; -import { Link, withRouter } from 'react-router'; -import { availableLanguages, 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 { logoutUser } from '../modules/User/actions'; - -import getConfig from '../utils/getConfig'; -import { metaKeyName, metaKey } from '../utils/metaKey'; -import { getIsUserOwner } from '../modules/IDE/selectors/users'; -import { selectSketchPath } from '../modules/IDE/selectors/project'; - -import CaretLeftIcon from '../images/left-arrow.svg'; -import LogoIcon from '../images/p5js-logo-small.svg'; -import NavDropdownMenu from './Nav/NavDropdownMenu'; -import NavMenuItem from './Nav/NavMenuItem'; -import NavBar from './Nav/NavBar'; - -class Nav extends React.PureComponent { - constructor(props) { - super(props); - this.handleSave = this.handleSave.bind(this); - this.handleNew = this.handleNew.bind(this); - this.handleShare = this.handleShare.bind(this); - this.handleDownload = this.handleDownload.bind(this); - this.handleLangSelection = this.handleLangSelection.bind(this); - } - - handleNew() { - const { unsavedChanges, warnIfUnsavedChanges } = this.props; - if (!unsavedChanges) { - this.props.showToast(1500); - this.props.setToastText('Toast.OpenedNewSketch'); - this.props.newProject(); - } else if (warnIfUnsavedChanges && warnIfUnsavedChanges()) { - this.props.showToast(1500); - this.props.setToastText('Toast.OpenedNewSketch'); - this.props.newProject(); - } - } - - handleSave() { - if (this.props.user.authenticated) { - this.props.saveProject(this.props.cmController.getContent()); - } else { - this.props.showErrorModal('forceAuthentication'); - } - } - - handleLangSelection(event) { - this.props.setLanguage(event.target.value); - this.props.showToast(1500); - this.props.setToastText('Toast.LangChange'); - } - - handleDownload() { - this.props.autosaveProject(); - projectActions.exportProjectAsZip(this.props.project.id); - } - - handleShare() { - const { username } = this.props.params; - this.props.showShareModal( - this.props.project.id, - this.props.project.name, - username - ); - } - - renderDashboardMenu() { - return ( - - ); - } - - renderProjectMenu() { - const replaceCommand = - metaKey === 'Ctrl' ? `${metaKeyName}+H` : `${metaKeyName}+⌥+F`; - return ( - - ); - } - - renderLanguageMenu() { - return ( - - {sortBy(availableLanguages).map((key) => ( - - {languageKeyToLabel(key)} - - ))} - - ); - } - - renderUnauthenticatedUserMenu() { - return ( - - ); - } - - renderAuthenticatedUserMenu() { - return ( - - ); - } - - renderUserMenu() { - const isLoginEnabled = getConfig('LOGIN_ENABLED'); - const isAuthenticated = this.props.user.authenticated; - - if (isLoginEnabled && isAuthenticated) { - return this.renderAuthenticatedUserMenu(); - } else if (isLoginEnabled && !isAuthenticated) { - return this.renderUnauthenticatedUserMenu(); - } - - return null; - } - - renderLeftLayout() { - switch (this.props.layout) { - case 'dashboard': - return this.renderDashboardMenu(); - case 'project': - default: - return this.renderProjectMenu(); - } - } - - render() { - return ( - - {this.renderLeftLayout()} - {this.renderUserMenu()} - - ); - } -} - -Nav.propTypes = { - newProject: PropTypes.func.isRequired, - showToast: PropTypes.func.isRequired, - setToastText: PropTypes.func.isRequired, - saveProject: PropTypes.func.isRequired, - autosaveProject: PropTypes.func.isRequired, - cloneProject: PropTypes.func.isRequired, - user: PropTypes.shape({ - authenticated: PropTypes.bool.isRequired, - username: PropTypes.string, - id: PropTypes.string - }).isRequired, - project: PropTypes.shape({ - id: PropTypes.string, - name: PropTypes.string, - owner: PropTypes.shape({ - id: PropTypes.string - }) - }), - logoutUser: PropTypes.func.isRequired, - showShareModal: PropTypes.func.isRequired, - showErrorModal: PropTypes.func.isRequired, - unsavedChanges: PropTypes.bool.isRequired, - warnIfUnsavedChanges: PropTypes.func, - showKeyboardShortcutModal: PropTypes.func.isRequired, - cmController: PropTypes.shape({ - tidyCode: PropTypes.func, - showFind: PropTypes.func, - showReplace: PropTypes.func, - getContent: PropTypes.func - }), - startSketch: PropTypes.func.isRequired, - stopSketch: PropTypes.func.isRequired, - newFile: PropTypes.func.isRequired, - newFolder: PropTypes.func.isRequired, - layout: PropTypes.oneOf(['dashboard', 'project']), - rootFile: PropTypes.shape({ - id: PropTypes.string.isRequired - }).isRequired, - params: PropTypes.shape({ - username: PropTypes.string - }), - t: PropTypes.func.isRequired, - setLanguage: PropTypes.func.isRequired, - language: PropTypes.string.isRequired, - isUserOwner: PropTypes.bool.isRequired, - editorLink: PropTypes.string -}; - -Nav.defaultProps = { - project: { - id: undefined, - owner: undefined - }, - cmController: {}, - layout: 'project', - warnIfUnsavedChanges: undefined, - params: { - username: undefined - }, - editorLink: '/' -}; - -function mapStateToProps(state) { - return { - project: state.project, - user: state.user, - unsavedChanges: state.ide.unsavedChanges, - rootFile: state.files.filter((file) => file.name === 'root')[0], - language: state.preferences.language, - isUserOwner: getIsUserOwner(state), - editorLink: selectSketchPath(state) - }; -} - -const mapDispatchToProps = { - ...IDEActions, - ...projectActions, - ...toastActions, - logoutUser, - setAllAccessibleOutput, - setLanguage -}; - -export default withTranslation()( - withRouter(connect(mapStateToProps, mapDispatchToProps)(Nav)) -); -export { Nav as NavComponent }; diff --git a/client/components/Nav.unit.test.jsx b/client/components/Nav.unit.test.jsx index ba78b96c73..50a313ec64 100644 --- a/client/components/Nav.unit.test.jsx +++ b/client/components/Nav.unit.test.jsx @@ -1,5 +1,5 @@ import React from 'react'; -import { render, reduxRender } from '../test-utils'; +import { render, reduxRender } from '../../../test-utils'; import Nav, { NavComponent } from './Nav'; @@ -42,7 +42,6 @@ describe('Nav', () => { stopSketch: jest.fn(), setAllAccessibleOutput: jest.fn(), showToast: jest.fn(), - setToastText: jest.fn(), rootFile: { id: 'root-file' }, diff --git a/client/components/Nav/NavBar.jsx b/client/components/Nav/NavBar.jsx index d5e33ada23..4333e97014 100644 --- a/client/components/Nav/NavBar.jsx +++ b/client/components/Nav/NavBar.jsx @@ -6,6 +6,7 @@ import React, { useRef, useState } from 'react'; +import useKeyDownHandlers from '../../modules/IDE/hooks/useKeyDownHandlers'; import { MenuOpenContext, NavBarContext } from './contexts'; function NavBar({ children }) { @@ -31,18 +32,9 @@ function NavBar({ children }) { }; }, [nodeRef, setDropdownOpen]); - // TODO: replace with `useKeyDownHandlers` after #2052 is merged - useEffect(() => { - function handleKeyDown(e) { - if (e.keyCode === 27) { - setDropdownOpen('none'); - } - } - document.addEventListener('keydown', handleKeyDown, false); - return () => { - document.removeEventListener('keydown', handleKeyDown, false); - }; - }, [setDropdownOpen]); + useKeyDownHandlers({ + Esc: () => setDropdownOpen('none') + }); const clearHideTimeout = useCallback(() => { if (timerRef.current) { diff --git a/client/components/Nav/NavDropdownMenu.jsx b/client/components/Nav/NavDropdownMenu.jsx index 4757baf7bb..c2b3660048 100644 --- a/client/components/Nav/NavDropdownMenu.jsx +++ b/client/components/Nav/NavDropdownMenu.jsx @@ -11,10 +11,10 @@ function NavDropdownMenu({ id, title, children }) { const { createDropdownHandlers } = useContext(NavBarContext); - const handlers = useMemo(() => createDropdownHandlers(id), [ - createDropdownHandlers, - id - ]); + const handlers = useMemo( + () => createDropdownHandlers(id), + [createDropdownHandlers, id] + ); return (
  • diff --git a/client/components/Nav/NavMenuItem.jsx b/client/components/Nav/NavMenuItem.jsx index 91b42ee44c..0f04afe0cc 100644 --- a/client/components/Nav/NavMenuItem.jsx +++ b/client/components/Nav/NavMenuItem.jsx @@ -8,10 +8,10 @@ function NavMenuItem({ hideIf, ...rest }) { const { createMenuItemHandlers } = useContext(NavBarContext); - const handlers = useMemo(() => createMenuItemHandlers(parent), [ - createMenuItemHandlers, - parent - ]); + const handlers = useMemo( + () => createMenuItemHandlers(parent), + [createMenuItemHandlers, parent] + ); if (hideIf) { return null; diff --git a/client/components/PreviewNav.jsx b/client/components/PreviewNav.jsx index 7bb14b0550..fa42a89427 100644 --- a/client/components/PreviewNav.jsx +++ b/client/components/PreviewNav.jsx @@ -1,6 +1,6 @@ import PropTypes from 'prop-types'; import React from 'react'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import LogoIcon from '../images/p5js-logo-small.svg'; diff --git a/client/components/RootPage.jsx b/client/components/RootPage.jsx index ca6a4723cd..55029bf51f 100644 --- a/client/components/RootPage.jsx +++ b/client/components/RootPage.jsx @@ -1,13 +1,20 @@ +import PropTypes from 'prop-types'; import styled from 'styled-components'; import { prop } from '../theme'; const RootPage = styled.div` - min-height: 100%; + height: 100vh; display: flex; flex-direction: column; color: ${prop('primaryTextColor')}; + overflow: hidden; background-color: ${prop('backgroundColor')}; - height: ${({ fixedHeight }) => fixedHeight || 'initial'}; + /* height: ${({ fixedHeight }) => fixedHeight || 'initial'}; */ `; +RootPage.propTypes = { + fixedHeight: PropTypes.string, + children: PropTypes.node.isRequired +}; + export default RootPage; diff --git a/client/components/createRedirectWithUsername.jsx b/client/components/createRedirectWithUsername.jsx index 760cd4fcd3..5e6e23bb5b 100644 --- a/client/components/createRedirectWithUsername.jsx +++ b/client/components/createRedirectWithUsername.jsx @@ -1,29 +1,23 @@ +import PropTypes from 'prop-types'; import React from 'react'; -import { connect } from 'react-redux'; -import { browserHistory } from 'react-router'; +import { useSelector } from 'react-redux'; +import { Navigate } from 'react-router-dom'; -const RedirectToUser = ({ username, url = '/:username/sketches' }) => { - React.useEffect(() => { - if (username == null) { - return; - } - - browserHistory.replace(url.replace(':username', username)); - }, [username]); - - return null; +/** + * Sets the current username to the `:username` template in the provided URL, + * eg. `/:username/sketches` => `/p5/sketches`. + */ +const RedirectToUser = ({ url = '/:username/sketches' }) => { + const username = useSelector((state) => + state.user ? state.user.username : null + ); + return username ? ( + + ) : null; }; -function mapStateToProps(state) { - return { - username: state.user ? state.user.username : null - }; -} - -const ConnectedRedirectToUser = connect(mapStateToProps)(RedirectToUser); - -const createRedirectWithUsername = (url) => (props) => ( - -); +RedirectToUser.propTypes = { + url: PropTypes.string.isRequired +}; -export default createRedirectWithUsername; +export default RedirectToUser; diff --git a/client/components/mobile/ActionStrip.jsx b/client/components/mobile/ActionStrip.jsx index cd46b9fec4..3e792fec8b 100644 --- a/client/components/mobile/ActionStrip.jsx +++ b/client/components/mobile/ActionStrip.jsx @@ -45,7 +45,7 @@ const ActionStrip = ({ actions }) => ( ActionStrip.propTypes = { actions: PropTypes.arrayOf( PropTypes.shape({ - icon: PropTypes.component, + icon: PropTypes.elementType, aria: PropTypes.string.isRequired, action: PropTypes.func.isRequired, inverted: PropTypes.bool diff --git a/client/components/mobile/Explorer.jsx b/client/components/mobile/Explorer.jsx index 0d798e471d..6277e8aa66 100644 --- a/client/components/mobile/Explorer.jsx +++ b/client/components/mobile/Explorer.jsx @@ -1,15 +1,18 @@ +import PropTypes from 'prop-types'; import React from 'react'; import { useTranslation } from 'react-i18next'; -import PropTypes from 'prop-types'; -import Sidebar from './Sidebar'; +import { useSelector } from 'react-redux'; import ConnectedFileNode from '../../modules/IDE/components/FileNode'; +import { selectRootFile } from '../../modules/IDE/selectors/files'; +import Sidebar from './Sidebar'; -const Explorer = ({ id, canEdit, onPressClose }) => { +const Explorer = ({ canEdit, onPressClose }) => { const { t } = useTranslation(); + const root = useSelector(selectRootFile); return ( onPressClose()} /> @@ -18,7 +21,6 @@ const Explorer = ({ id, canEdit, onPressClose }) => { }; Explorer.propTypes = { - id: PropTypes.number.isRequired, onPressClose: PropTypes.func, canEdit: PropTypes.bool }; diff --git a/client/components/mobile/FloatingNav.jsx b/client/components/mobile/FloatingNav.jsx deleted file mode 100644 index 6983ba6c72..0000000000 --- a/client/components/mobile/FloatingNav.jsx +++ /dev/null @@ -1,44 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import styled from 'styled-components'; -import { remSize, prop } from '../../theme'; -import IconButton from './IconButton'; - -const FloatingContainer = styled.div` - position: fixed; - right: ${remSize(16)}; - top: ${remSize(80)}; - - text-align: right; - z-index: 3; - - svg { - width: ${remSize(32)}; - } - svg > path { - fill: ${prop('Button.primary.default.background')} !important; - } -`; - -const FloatingNav = ({ items }) => ( - - {items.map(({ icon, onPress }) => ( - - ))} - -); - -FloatingNav.propTypes = { - items: PropTypes.arrayOf( - PropTypes.shape({ - icon: PropTypes.element, - onPress: PropTypes.func - }) - ) -}; - -FloatingNav.defaultProps = { - items: [] -}; - -export default FloatingNav; diff --git a/client/components/mobile/Header.jsx b/client/components/mobile/MobileHeader.jsx similarity index 100% rename from client/components/mobile/Header.jsx rename to client/components/mobile/MobileHeader.jsx diff --git a/client/components/mobile/MobileScreen.jsx b/client/components/mobile/MobileScreen.jsx index 6177d00d3e..c658e28fb8 100644 --- a/client/components/mobile/MobileScreen.jsx +++ b/client/components/mobile/MobileScreen.jsx @@ -1,45 +1,18 @@ -import React from 'react'; -import PropTypes from 'prop-types'; import styled from 'styled-components'; -import { remSize, prop } from '../../theme'; +import { remSize } from '../../theme'; +import RootPage from '../RootPage'; -const ScreenWrapper = styled.div` +const Screen = styled(RootPage)` .toast { font-size: ${remSize(12)}; padding: ${remSize(8)}; - border-radius: ${remSize(4)}; width: 92%; top: unset; min-width: unset; bottom: ${remSize(64)}; } - ${({ fullscreen }) => - fullscreen && - ` - display: flex; - width: 100%; - height: 100%; - flex-flow: column; - background-color: ${prop('backgroundColor')} - `} + height: 100vh; `; -const Screen = ({ children, fullscreen, slimheader }) => ( - - {children} - -); - -Screen.defaultProps = { - fullscreen: false, - slimheader: false -}; - -Screen.propTypes = { - children: PropTypes.node.isRequired, - fullscreen: PropTypes.bool, - slimheader: PropTypes.bool -}; - export default Screen; diff --git a/client/components/mobile/PreferencePicker.jsx b/client/components/mobile/PreferencePicker.jsx deleted file mode 100644 index 0a6746d65c..0000000000 --- a/client/components/mobile/PreferencePicker.jsx +++ /dev/null @@ -1,73 +0,0 @@ -import React from 'react'; -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' -}))` - color: ${prop('primaryTextColor')}; -`; - -const Preference = styled.div.attrs((props) => ({ - ...props, - className: 'preference' -}))` - flex-wrap: nowrap !important; - align-items: baseline !important; - justify-items: space-between; -`; - -const OptionLabel = styled.label.attrs({ className: 'preference__option' })` - font-size: ${remSize(14)} !important; -`; - -const PreferencePicker = ({ title, value, onSelect, options }) => ( - - {title} -
    - {options.map((option) => ( - - onSelect(option.value)} - aria-label={option.ariaLabel} - name={option.name} - key={`${option.name}-${option.id}-input`} - id={option.id} - className="preference__radio-button" - value={option.value} - checked={value === option.value} - /> - - {option.label} - - - ))} -
    -
    -); - -PreferencePicker.defaultProps = { - options: [] -}; - -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 -}; - -export default PreferencePicker; diff --git a/client/components/mobile/Sidebar.jsx b/client/components/mobile/Sidebar.jsx index 270da1b0ed..28b3bf8aed 100644 --- a/client/components/mobile/Sidebar.jsx +++ b/client/components/mobile/Sidebar.jsx @@ -2,7 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import styled from 'styled-components'; import { remSize, prop } from '../../theme'; -import Header from './Header'; +import MobileHeader from './MobileHeader'; import IconButton from './IconButton'; import { ExitIcon } from '../../common/icons'; @@ -21,13 +21,13 @@ const SidebarWrapper = styled.div` const Sidebar = ({ title, onPressClose, children }) => ( {title && ( -
    + -
    + )} {children}
    diff --git a/client/components/mobile/Tab.jsx b/client/components/mobile/Tab.jsx index 23741f82ec..bd064b3f36 100644 --- a/client/components/mobile/Tab.jsx +++ b/client/components/mobile/Tab.jsx @@ -1,5 +1,5 @@ import styled from 'styled-components'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; import { prop, remSize } from '../../theme'; export default styled(Link)` diff --git a/client/constants.js b/client/constants.js index ec0e4107ac..c391694f46 100644 --- a/client/constants.js +++ b/client/constants.js @@ -1,16 +1,7 @@ // TODO Organize this file by reducer type, to break this apart into // multiple files export const UPDATE_FILE_CONTENT = 'UPDATE_FILE_CONTENT'; -export const TOGGLE_SKETCH = 'TOGGLE_SKETCH'; -export const START_SKETCH = 'START_SKETCH'; -export const STOP_SKETCH = 'STOP_SKETCH'; - -export const START_ACCESSIBLE_OUTPUT = 'START_ACCESSIBLE_OUTPUT'; -export const STOP_ACCESSIBLE_OUTPUT = 'STOP_ACCESSIBLE_OUTPUT'; - -export const OPEN_PREFERENCES = 'OPEN_PREFERENCES'; -export const CLOSE_PREFERENCES = 'CLOSE_PREFERENCES'; export const SET_FONT_SIZE = 'SET_FONT_SIZE'; export const SET_LINE_NUMBERS = 'SET_LINE_NUMBERS'; @@ -47,18 +38,11 @@ export const EDIT_COLLECTION = 'EDIT_COLLECTION'; export const DELETE_PROJECT = 'DELETE_PROJECT'; export const SET_SELECTED_FILE = 'SET_SELECTED_FILE'; -export const SHOW_MODAL = 'SHOW_MODAL'; -export const HIDE_MODAL = 'HIDE_MODAL'; export const CREATE_FILE = 'CREATE_FILE'; export const SET_BLOB_URL = 'SET_BLOB_URL'; -export const EXPAND_SIDEBAR = 'EXPAND_SIDEBAR'; -export const COLLAPSE_SIDEBAR = 'COLLAPSE_SIDEBAR'; - export const CONSOLE_EVENT = 'CONSOLE_EVENT'; export const CLEAR_CONSOLE = 'CLEAR_CONSOLE'; -export const EXPAND_CONSOLE = 'EXPAND_CONSOLE'; -export const COLLAPSE_CONSOLE = 'COLLAPSE_CONSOLE'; export const UPDATE_LINT_MESSAGE = 'UPDATE_LINT_MESSAGE'; export const CLEAR_LINT_MESSAGE = 'CLEAR_LINT_MESSAGE'; @@ -77,34 +61,16 @@ export const SET_SOUND_OUTPUT = 'SET_SOUND_OUTPUT'; export const SET_AUTOCLOSE_BRACKETS_QUOTES = 'SET_AUTOCLOSE_BRACKETS_QUOTES'; export const SET_AUTOCOMPLETE_HINTER = 'SET_AUTOCOMPLETE_HINTER'; -export const OPEN_PROJECT_OPTIONS = 'OPEN_PROJECT_OPTIONS'; -export const CLOSE_PROJECT_OPTIONS = 'CLOSE_PROJECT_OPTIONS'; -export const SHOW_NEW_FOLDER_MODAL = 'SHOW_NEW_FOLDER_MODAL'; -export const CLOSE_NEW_FOLDER_MODAL = 'CLOSE_NEW_FOLDER_MODAL'; export const SHOW_FOLDER_CHILDREN = 'SHOW_FOLDER_CHILDREN'; export const HIDE_FOLDER_CHILDREN = 'HIDE_FOLDER_CHILDREN'; -export const OPEN_UPLOAD_FILE_MODAL = 'OPEN_UPLOAD_FILE_MODAL'; -export const CLOSE_UPLOAD_FILE_MODAL = 'CLOSE_UPLOAD_FILE_MODAL'; - -export const SHOW_SHARE_MODAL = 'SHOW_SHARE_MODAL'; -export const CLOSE_SHARE_MODAL = 'CLOSE_SHARE_MODAL'; -export const SHOW_EDITOR_OPTIONS = 'SHOW_EDITOR_OPTIONS'; -export const CLOSE_EDITOR_OPTIONS = 'CLOSE_EDITOR_OPTIONS'; -export const SHOW_KEYBOARD_SHORTCUT_MODAL = 'SHOW_KEYBOARD_SHORTCUT_MODAL'; -export const CLOSE_KEYBOARD_SHORTCUT_MODAL = 'CLOSE_KEYBOARD_SHORTCUT_MODAL'; + export const SHOW_TOAST = 'SHOW_TOAST'; export const HIDE_TOAST = 'HIDE_TOAST'; export const SET_TOAST_TEXT = 'SET_TOAST_TEXT'; export const SET_THEME = 'SET_THEME'; export const SET_LANGUAGE = 'SET_LANGUAGE'; -export const SET_UNSAVED_CHANGES = 'SET_UNSAVED_CHANGES'; export const SET_AUTOREFRESH = 'SET_AUTOREFRESH'; -export const START_SKETCH_REFRESH = 'START_SKETCH_REFRESH'; -export const END_SKETCH_REFRESH = 'END_SKETCH_REFRESH'; - -export const DETECT_INFINITE_LOOPS = 'DETECT_INFINITE_LOOPS'; -export const RESET_INFINITE_LOOPS = 'RESET_INFINITE_LOOPS'; export const RESET_PASSWORD_INITIATE = 'RESET_PASSWORD_INITIATE'; export const RESET_PASSWORD_RESET = 'RESET_PASSWORD_RESET'; @@ -118,20 +84,11 @@ export const EMAIL_VERIFICATION_INVALID = 'EMAIL_VERIFICATION_INVALID'; // eventually, handle errors more specifically and better export const ERROR = 'ERROR'; -export const JUST_OPENED_PROJECT = 'JUST_OPENED_PROJECT'; -export const RESET_JUST_OPENED_PROJECT = 'RESET_JUST_OPENED_PROJECT'; - export const SET_PROJECT_SAVED_TIME = 'SET_PROJECT_SAVED_TIME'; -export const RESET_PROJECT_SAVED_TIME = 'RESET_PROJECT_SAVED_TIME'; -export const SET_PREVIOUS_PATH = 'SET_PREVIOUS_PATH'; -export const SHOW_ERROR_MODAL = 'SHOW_ERROR_MODAL'; -export const HIDE_ERROR_MODAL = 'HIDE_ERROR_MODAL'; export const PERSIST_STATE = 'PERSIST_STATE'; export const CLEAR_PERSISTED_STATE = 'CLEAR_PERSISTED_STATE'; -export const HIDE_RUNTIME_ERROR_WARNING = 'HIDE_RUNTIME_ERROR_WARNING'; -export const SHOW_RUNTIME_ERROR_WARNING = 'SHOW_RUNTIME_ERROR_WARNING'; export const SET_ASSETS = 'SET_ASSETS'; export const DELETE_ASSET = 'DELETE_ASSET'; diff --git a/client/images/account.svg b/client/images/account.svg new file mode 100644 index 0000000000..c01ab67e66 --- /dev/null +++ b/client/images/account.svg @@ -0,0 +1,3 @@ + + + diff --git a/client/images/down-arrow-white.svg b/client/images/down-arrow-white.svg index 34efd84bd1..c246dfd749 100644 --- a/client/images/down-arrow-white.svg +++ b/client/images/down-arrow-white.svg @@ -1,9 +1,5 @@ - - arrow-shape-copy-2 - Created with Sketch. - @@ -15,4 +11,4 @@ - \ No newline at end of file + diff --git a/client/images/down-arrow.svg b/client/images/down-arrow.svg index a1dc41d5d1..874a1d11a4 100644 --- a/client/images/down-arrow.svg +++ b/client/images/down-arrow.svg @@ -1,8 +1,5 @@ - - - diff --git a/client/images/down-filled-triangle.svg b/client/images/down-filled-triangle.svg index b673aafe2d..c4ce39aebc 100644 --- a/client/images/down-filled-triangle.svg +++ b/client/images/down-filled-triangle.svg @@ -1,10 +1,6 @@ - - Triangle - Created with Sketch. - - - + + - \ No newline at end of file + diff --git a/client/images/editor.svg b/client/images/editor.svg new file mode 100644 index 0000000000..4960b2577a --- /dev/null +++ b/client/images/editor.svg @@ -0,0 +1,4 @@ + + + + diff --git a/client/images/exit.svg b/client/images/exit.svg index b6c0cc7463..98cc301bf7 100644 --- a/client/images/exit.svg +++ b/client/images/exit.svg @@ -1,12 +1,6 @@ - - Exit - - - - - - + + diff --git a/client/images/file.svg b/client/images/file.svg index aa19bd0c93..9e8dbf53ee 100644 --- a/client/images/file.svg +++ b/client/images/file.svg @@ -1,9 +1,5 @@ - - "K" file icon Copy - Created with Sketch. - @@ -11,4 +7,4 @@ - \ No newline at end of file + diff --git a/client/images/folder.svg b/client/images/folder.svg index 47e3e4302b..0f00a9767b 100644 --- a/client/images/folder.svg +++ b/client/images/folder.svg @@ -1,12 +1,8 @@ - - "." project folder - Created with Sketch. - - \ No newline at end of file + diff --git a/client/images/minus.svg b/client/images/minus.svg index c6a4b1bad0..80611c826f 100644 --- a/client/images/minus.svg +++ b/client/images/minus.svg @@ -1,12 +1,6 @@ - - Decrease Value - - - - - - + + diff --git a/client/images/more.svg b/client/images/more.svg index ee41de95dd..9b6dd50ee3 100644 --- a/client/images/more.svg +++ b/client/images/more.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/client/images/p5js-logo.svg b/client/images/p5js-logo.svg index 57573167c1..5484a2df6f 100644 --- a/client/images/p5js-logo.svg +++ b/client/images/p5js-logo.svg @@ -1,9 +1,5 @@ - - p5js-rect - - diff --git a/client/images/play.svg b/client/images/play.svg index aff6d46199..b28a6d330a 100644 --- a/client/images/play.svg +++ b/client/images/play.svg @@ -1,20 +1,6 @@ - - - play - - - - - - - - - - - - - - + + + - \ No newline at end of file + diff --git a/client/images/plus-icon.svg b/client/images/plus-icon.svg index 502e28587a..da91915a19 100644 --- a/client/images/plus-icon.svg +++ b/client/images/plus-icon.svg @@ -1,9 +1,5 @@ - - close shape - - @@ -13,4 +9,4 @@ - \ No newline at end of file + diff --git a/client/images/plus.svg b/client/images/plus.svg index 5629954a23..c8ce01dffc 100644 --- a/client/images/plus.svg +++ b/client/images/plus.svg @@ -1,12 +1,6 @@ - - Increase Value - - - - - - + + diff --git a/client/images/preferences.svg b/client/images/preferences.svg index 7d0393daba..a2962ae16e 100644 --- a/client/images/preferences.svg +++ b/client/images/preferences.svg @@ -1,19 +1,6 @@ - - preferences - - + - - - - - - - - - - diff --git a/client/images/right-arrow-white.svg b/client/images/right-arrow-white.svg index b789f93d25..83bf58dc82 100644 --- a/client/images/right-arrow-white.svg +++ b/client/images/right-arrow-white.svg @@ -1,9 +1,5 @@ - - arrow-shape-copy - Created with Sketch. - @@ -13,4 +9,4 @@ - \ No newline at end of file + diff --git a/client/images/stop.svg b/client/images/stop.svg index c97f4b8a1d..00479aec72 100644 --- a/client/images/stop.svg +++ b/client/images/stop.svg @@ -1,20 +1,6 @@ - - stop - - - - - - - - - - - - - - + + - \ No newline at end of file + diff --git a/client/images/triangle-arrow-down-white.svg b/client/images/triangle-arrow-down-white.svg index 75fd556753..66af8030e9 100644 --- a/client/images/triangle-arrow-down-white.svg +++ b/client/images/triangle-arrow-down-white.svg @@ -1,12 +1,5 @@ - Down Arrow - Created with Sketch. - - - - - - - + + - \ No newline at end of file + diff --git a/client/images/triangle-arrow-down.svg b/client/images/triangle-arrow-down.svg index 47ab5d101d..248edaa5db 100644 --- a/client/images/triangle-arrow-down.svg +++ b/client/images/triangle-arrow-down.svg @@ -1,12 +1,5 @@ - Down Arrow - Created with Sketch. - - - - - - - + + - \ No newline at end of file + diff --git a/client/images/triangle-arrow-left.svg b/client/images/triangle-arrow-left.svg index dcc159dfb6..7dc2944171 100644 --- a/client/images/triangle-arrow-left.svg +++ b/client/images/triangle-arrow-left.svg @@ -1,14 +1,7 @@ - Left Arrow - Created with Sketch. - - - - - - - + + diff --git a/client/images/triangle-arrow-right-white.svg b/client/images/triangle-arrow-right-white.svg index ff4a6b67aa..112d9aa4b5 100644 --- a/client/images/triangle-arrow-right-white.svg +++ b/client/images/triangle-arrow-right-white.svg @@ -1,12 +1,5 @@ - Right Arrow - Created with Sketch. - - - - - - - + + - \ No newline at end of file + diff --git a/client/images/triangle-arrow-right.svg b/client/images/triangle-arrow-right.svg index 222020a8a3..db6f48c8a3 100644 --- a/client/images/triangle-arrow-right.svg +++ b/client/images/triangle-arrow-right.svg @@ -1,12 +1,5 @@ - Right Arrow - Created with Sketch. - - - - - - - + + - \ No newline at end of file + diff --git a/client/index.integration.test.jsx b/client/index.integration.test.jsx index c7a803089d..5c9a245706 100644 --- a/client/index.integration.test.jsx +++ b/client/index.integration.test.jsx @@ -1,16 +1,15 @@ import { setupServer } from 'msw/node'; import { rest } from 'msw'; import React from 'react'; -import { Router, browserHistory } from 'react-router'; +import { RouterProvider } from 'react-router-dom'; +import router from './router'; import { reduxRender, act, waitFor, screen, within } from './test-utils'; import configureStore from './store'; -import routes from './routes'; import * as Actions from './modules/User/actions'; import { userResponse } from './testData/testServerResponses'; // setup for the app -const history = browserHistory; const initialState = window.__INITIAL_STATE__; const store = configureStore(initialState); @@ -57,7 +56,7 @@ document.createRange = () => { describe('index.jsx integration', () => { // the subject under test const subject = () => - reduxRender(, { store }); + reduxRender(, { store }); // spy on this function and wait for it to be called before making assertions const spy = jest.spyOn(Actions, 'getUser'); diff --git a/client/index.jsx b/client/index.jsx index be714fc0d5..b33a50a18a 100644 --- a/client/index.jsx +++ b/client/index.jsx @@ -1,11 +1,10 @@ import React, { Suspense } from 'react'; import { render } from 'react-dom'; -import { hot } from 'react-hot-loader/root'; import { Provider } from 'react-redux'; -import { Router, browserHistory } from 'react-router'; +import { RouterProvider } from 'react-router-dom'; import configureStore from './store'; -import routes from './routes'; +import router from './router'; import ThemeProvider from './modules/App/components/ThemeProvider'; import Loader from './modules/App/components/loader'; import './i18n'; @@ -15,7 +14,6 @@ require('./styles/main.scss'); // Load the p5 png logo, so that webpack will use it require('./images/p5js-square-logo.png'); -const history = browserHistory; const initialState = window.__INITIAL_STATE__; const store = configureStore(initialState); @@ -23,16 +21,14 @@ const store = configureStore(initialState); const App = () => ( - + ); -const HotApp = hot(App); - render( }> - + , document.getElementById('root') ); diff --git a/client/middleware.js b/client/middleware.js new file mode 100644 index 0000000000..0b4230ec9d --- /dev/null +++ b/client/middleware.js @@ -0,0 +1,5 @@ +import { createListenerMiddleware } from '@reduxjs/toolkit'; + +const listenerMiddleware = createListenerMiddleware(); + +export default listenerMiddleware; diff --git a/client/modules/App/App.jsx b/client/modules/App/App.jsx index 1f41446a79..10286ac09a 100644 --- a/client/modules/App/App.jsx +++ b/client/modules/App/App.jsx @@ -1,7 +1,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import { connect } from 'react-redux'; -import getConfig from '../../utils/getConfig'; +import { showReduxDevTools } from '../../store'; import DevTools from './components/DevTools'; import { setPreviousPath } from '../IDE/actions/ide'; import { setLanguage } from '../IDE/actions/preferences'; @@ -51,9 +51,7 @@ class App extends React.Component { return (
    - {this.state.isMounted && - !window.devToolsExtension && - getConfig('NODE_ENV') === 'development' && } + {this.state.isMounted && showReduxDevTools() && } {this.props.children}
    ); diff --git a/client/modules/App/components/Overlay.jsx b/client/modules/App/components/Overlay.jsx index 39f492fc6f..ef55699833 100644 --- a/client/modules/App/components/Overlay.jsx +++ b/client/modules/App/components/Overlay.jsx @@ -1,9 +1,10 @@ import PropTypes from 'prop-types'; import React from 'react'; -import { browserHistory } from 'react-router'; import { withTranslation } from 'react-i18next'; import ExitIcon from '../../../images/exit.svg'; +import { DocumentKeyDown } from '../../IDE/hooks/useKeyDownHandlers'; +import { withRouter } from '../../../utils/router-compatibilty'; class Overlay extends React.Component { constructor(props) { @@ -11,12 +12,10 @@ class Overlay extends React.Component { this.close = this.close.bind(this); this.handleClick = this.handleClick.bind(this); this.handleClickOutside = this.handleClickOutside.bind(this); - this.keyPressHandle = this.keyPressHandle.bind(this); } componentWillMount() { document.addEventListener('mousedown', this.handleClick, false); - document.addEventListener('keydown', this.keyPressHandle); } componentDidMount() { @@ -25,7 +24,6 @@ class Overlay extends React.Component { componentWillUnmount() { document.removeEventListener('mousedown', this.handleClick, false); - document.removeEventListener('keydown', this.keyPressHandle); } handleClick(e) { @@ -40,14 +38,6 @@ class Overlay extends React.Component { this.close(); } - keyPressHandle(e) { - // escape key code = 27. - // So here we are checking if the key pressed was Escape key. - if (e.keyCode === 27) { - this.close(); - } - } - close() { // Only close if it is the last (and therefore the topmost overlay) const overlays = document.getElementsByClassName('overlay'); @@ -55,7 +45,7 @@ class Overlay extends React.Component { return; if (!this.props.closeOverlay) { - browserHistory.push(this.props.previousPath); + this.props.navigate(this.props.previousPath); } else { this.props.closeOverlay(); } @@ -90,6 +80,7 @@ class Overlay extends React.Component { {children} + this.close() }} /> @@ -105,7 +96,8 @@ Overlay.propTypes = { ariaLabel: PropTypes.string, previousPath: PropTypes.string, isFixedHeight: PropTypes.bool, - t: PropTypes.func.isRequired + t: PropTypes.func.isRequired, + navigate: PropTypes.func.isRequired }; Overlay.defaultProps = { @@ -118,4 +110,4 @@ Overlay.defaultProps = { isFixedHeight: false }; -export default withTranslation()(Overlay); +export default withRouter(withTranslation()(Overlay)); diff --git a/client/modules/IDE/actions/collections.js b/client/modules/IDE/actions/collections.js index 69da0f23ba..e8626f5bc6 100644 --- a/client/modules/IDE/actions/collections.js +++ b/client/modules/IDE/actions/collections.js @@ -1,10 +1,8 @@ -import { browserHistory } from 'react-router'; +import { navigate } from '../../../router'; import apiClient from '../../../utils/apiClient'; import * as ActionTypes from '../../../constants'; import { startLoader, stopLoader } from './loader'; -import { setToastText, showToast } from './toast'; - -const TOAST_DISPLAY_TIME_MS = 1500; +import { showToast } from './toast'; // eslint-disable-next-line export function getCollections(username) { @@ -50,13 +48,12 @@ export function createCollection(collection) { dispatch(stopLoader()); const newCollection = response.data; - dispatch(setToastText(`Created "${newCollection.name}"`)); - dispatch(showToast(TOAST_DISPLAY_TIME_MS)); + dispatch(showToast(`Created "${newCollection.name}"`)); const pathname = `/${newCollection.owner.username}/collections/${newCollection.id}`; const location = { pathname, state: { skipSavingPath: true } }; - browserHistory.push(location); + navigate(location); }) .catch((error) => { const { response } = error; @@ -85,8 +82,7 @@ export function addToCollection(collectionId, projectId) { const collectionName = response.data.name; - dispatch(setToastText(`Added to "${collectionName}`)); - dispatch(showToast(TOAST_DISPLAY_TIME_MS)); + dispatch(showToast(`Added to "${collectionName}`)); return response.data; }) @@ -118,8 +114,7 @@ export function removeFromCollection(collectionId, projectId) { const collectionName = response.data.name; - dispatch(setToastText(`Removed from "${collectionName}`)); - dispatch(showToast(TOAST_DISPLAY_TIME_MS)); + dispatch(showToast(`Removed from "${collectionName}`)); return response.data; }) diff --git a/client/modules/IDE/actions/ide.js b/client/modules/IDE/actions/ide.js index 80a43443bc..c1b3072c3e 100644 --- a/client/modules/IDE/actions/ide.js +++ b/client/modules/IDE/actions/ide.js @@ -2,49 +2,50 @@ import * as ActionTypes from '../../../constants'; import { clearConsole } from './console'; import { dispatchMessage, MessageTypes } from '../../../utils/dispatcher'; -export function startVisualSketch() { - return { - type: ActionTypes.START_SKETCH - }; -} - -export function stopVisualSketch() { - return { - type: ActionTypes.STOP_SKETCH - }; -} - -export function startRefreshSketch() { - return { - type: ActionTypes.START_SKETCH_REFRESH - }; -} - -export function startSketchAndRefresh() { - return (dispatch) => { - dispatch(startVisualSketch()); - dispatch(startRefreshSketch()); - }; -} - -export function endSketchRefresh() { - return { - type: ActionTypes.END_SKETCH_REFRESH - }; -} - -export function startAccessibleOutput() { - return { - type: ActionTypes.START_ACCESSIBLE_OUTPUT - }; -} - -export function stopAccessibleOutput() { - return { - type: ActionTypes.STOP_ACCESSIBLE_OUTPUT - }; -} - +import { ideActions } from '../reducers/ide'; + +// TODO: refactor actions which are only used internally by other actions. +const { + startVisualSketch, + stopVisualSketch, + startRefreshSketch, + startAccessibleOutput, + stopAccessibleOutput, + showShareModal: showShareModalInternal +} = ideActions; + +export const { + openUploadFileModal, + closeUploadFileModal, + hideRuntimeErrorWarning, + showRuntimeErrorWarning, + setUnsavedChanges, + endSketchRefresh, // TODO: export is not actually needed + newFile, + closeNewFileModal, + newFolder, + closeNewFolderModal, + expandSidebar, + collapseSidebar, + toggleSidebar, + expandConsole, + collapseConsole, + toggleConsole, + openPreferences, + closePreferences, + openProjectOptions, + closeProjectOptions, + closeShareModal, + showKeyboardShortcutModal, + closeKeyboardShortcutModal, + showErrorModal, + hideErrorModal, + setPreviousPath, + justOpenedProject, + resetJustOpenedProject +} = ideActions; + +// TODO: move to /files export function setSelectedFile(fileId) { return { type: ActionTypes.SET_SELECTED_FILE, @@ -52,199 +53,27 @@ export function setSelectedFile(fileId) { }; } +// TODO: move to /files export function resetSelectedFile(previousId) { return (dispatch, getState) => { const state = getState(); const newId = state.files.find( (file) => file.name !== 'root' && file.id !== previousId ).id; - dispatch({ - type: ActionTypes.SET_SELECTED_FILE, - selectedFile: newId - }); - }; -} - -export function newFile(parentId) { - return { - type: ActionTypes.SHOW_MODAL, - parentId - }; -} - -export function closeNewFileModal() { - return { - type: ActionTypes.HIDE_MODAL - }; -} - -export function openUploadFileModal(parentId) { - return { - type: ActionTypes.OPEN_UPLOAD_FILE_MODAL, - parentId - }; -} - -export function closeUploadFileModal() { - return { - type: ActionTypes.CLOSE_UPLOAD_FILE_MODAL - }; -} - -export function expandSidebar() { - return { - type: ActionTypes.EXPAND_SIDEBAR - }; -} - -export function collapseSidebar() { - return { - type: ActionTypes.COLLAPSE_SIDEBAR - }; -} - -export function expandConsole() { - return { - type: ActionTypes.EXPAND_CONSOLE - }; -} - -export function collapseConsole() { - return { - type: ActionTypes.COLLAPSE_CONSOLE - }; -} - -export function openPreferences() { - return { - type: ActionTypes.OPEN_PREFERENCES - }; -} - -export function closePreferences() { - return { - type: ActionTypes.CLOSE_PREFERENCES - }; -} - -export function openProjectOptions() { - return { - type: ActionTypes.OPEN_PROJECT_OPTIONS - }; -} - -export function closeProjectOptions() { - return { - type: ActionTypes.CLOSE_PROJECT_OPTIONS - }; -} - -export function newFolder(parentId) { - return { - type: ActionTypes.SHOW_NEW_FOLDER_MODAL, - parentId - }; -} - -export function closeNewFolderModal() { - return { - type: ActionTypes.CLOSE_NEW_FOLDER_MODAL + dispatch(setSelectedFile(newId)); }; } export function showShareModal(projectId, projectName, ownerUsername) { return (dispatch, getState) => { const { project, user } = getState(); - dispatch({ - type: ActionTypes.SHOW_SHARE_MODAL, - payload: { + dispatch( + showShareModalInternal({ shareModalProjectId: projectId || project.id, shareModalProjectName: projectName || project.name, shareModalProjectUsername: ownerUsername || user.username - } - }); - }; -} - -export function closeShareModal() { - return { - type: ActionTypes.CLOSE_SHARE_MODAL - }; -} - -export function showKeyboardShortcutModal() { - return { - type: ActionTypes.SHOW_KEYBOARD_SHORTCUT_MODAL - }; -} - -export function closeKeyboardShortcutModal() { - return { - type: ActionTypes.CLOSE_KEYBOARD_SHORTCUT_MODAL - }; -} - -export function setUnsavedChanges(value) { - return { - type: ActionTypes.SET_UNSAVED_CHANGES, - value - }; -} - -export function detectInfiniteLoops(message) { - return { - type: ActionTypes.DETECT_INFINITE_LOOPS, - message - }; -} - -export function resetInfiniteLoops() { - return { - type: ActionTypes.RESET_INFINITE_LOOPS - }; -} - -export function justOpenedProject() { - return { - type: ActionTypes.JUST_OPENED_PROJECT - }; -} - -export function resetJustOpenedProject() { - return { - type: ActionTypes.RESET_JUST_OPENED_PROJECT - }; -} - -export function setPreviousPath(path) { - return { - type: ActionTypes.SET_PREVIOUS_PATH, - path - }; -} - -export function showErrorModal(modalType) { - return { - type: ActionTypes.SHOW_ERROR_MODAL, - modalType - }; -} - -export function hideErrorModal() { - return { - type: ActionTypes.HIDE_ERROR_MODAL - }; -} - -export function hideRuntimeErrorWarning() { - return { - type: ActionTypes.HIDE_RUNTIME_ERROR_WARNING - }; -} - -export function showRuntimeErrorWarning() { - return { - type: ActionTypes.SHOW_RUNTIME_ERROR_WARNING + }) + ); }; } @@ -269,11 +98,13 @@ export function startSketch() { }; } +// TODO: does this need to call dispatchMessage like in startSketch? Should it call startSketch internally? export function startAccessibleSketch() { return (dispatch) => { dispatch(clearConsole()); dispatch(startAccessibleOutput()); - dispatch(startSketchAndRefresh()); + dispatch(startVisualSketch()); + dispatch(startRefreshSketch()); }; } @@ -287,6 +118,7 @@ export function stopSketch() { }; } +// TODO: move to /files export function createError(error) { return { type: ActionTypes.ERROR, diff --git a/client/modules/IDE/actions/project.js b/client/modules/IDE/actions/project.js index 9a528a34f6..1def701ed9 100644 --- a/client/modules/IDE/actions/project.js +++ b/client/modules/IDE/actions/project.js @@ -1,11 +1,11 @@ -import { browserHistory } from 'react-router'; import objectID from 'bson-objectid'; import each from 'async/each'; import isEqual from 'lodash/isEqual'; +import { navigate } from '../../../router'; import apiClient from '../../../utils/apiClient'; import getConfig from '../../../utils/getConfig'; import * as ActionTypes from '../../../constants'; -import { showToast, setToastText } from './toast'; +import { showToast } from './toast'; import { setUnsavedChanges, justOpenedProject, @@ -175,24 +175,21 @@ export function saveProject( dispatch(projectSaveSuccess()); if (!autosave) { if (state.ide.justOpenedProject && state.preferences.autosave) { - dispatch(showToast(5500)); - dispatch(setToastText('Toast.SketchSaved')); + dispatch(showToast('Toast.SketchSaved', 5500)); setTimeout( - () => dispatch(setToastText('Toast.AutosaveEnabled')), + () => dispatch(showToast('Toast.AutosaveEnabled', 5500)), 1500 ); dispatch(resetJustOpenedProject()); } else { - dispatch(showToast(1500)); - dispatch(setToastText('Toast.SketchSaved')); + dispatch(showToast('Toast.SketchSaved')); } } }) .catch((error) => { const { response } = error; dispatch(endSavingProject()); - dispatch(setToastText('Toast.SketchFailedSave')); - dispatch(showToast(1500)); + dispatch(showToast('Toast.SketchFailedSave')); if (response.status === 403) { dispatch(showErrorModal('staleSession')); } else if (response.status === 409) { @@ -214,7 +211,7 @@ export function saveProject( dispatch(setNewProject(synchedProject)); dispatch(setUnsavedChanges(false)); - browserHistory.push( + navigate( `/${response.data.user.username}/sketches/${response.data.id}` ); @@ -225,24 +222,21 @@ export function saveProject( dispatch(projectSaveSuccess()); if (!autosave) { if (state.preferences.autosave) { - dispatch(showToast(5500)); - dispatch(setToastText('Toast.SketchSaved')); + dispatch(showToast('Toast.SketchSaved', 5500)); setTimeout( - () => dispatch(setToastText('Toast.AutosaveEnabled')), + () => dispatch(showToast('Toast.AutosaveEnabled')), 1500 ); dispatch(resetJustOpenedProject()); } else { - dispatch(showToast(1500)); - dispatch(setToastText('Toast.SketchSaved')); + dispatch(showToast('Toast.SketchSaved')); } } }) .catch((error) => { const { response } = error; dispatch(endSavingProject()); - dispatch(setToastText('Toast.SketchFailedSave')); - dispatch(showToast(1500)); + dispatch(showToast('Toast.SketchFailedSave')); if (response.status === 403) { dispatch(showErrorModal('staleSession')); } else { @@ -271,7 +265,7 @@ export function resetProject() { export function newProject() { setTimeout(() => { - browserHistory.push('/'); + navigate('/'); }, 0); return resetProject(); } @@ -334,7 +328,7 @@ export function cloneProject(project) { apiClient .post('/projects', formParams) .then((response) => { - browserHistory.push( + navigate( `/${response.data.user.username}/sketches/${response.data.id}` ); dispatch(setNewProject(response.data)); diff --git a/client/modules/IDE/actions/toast.js b/client/modules/IDE/actions/toast.js index 25f472e14b..e47e0b6629 100644 --- a/client/modules/IDE/actions/toast.js +++ b/client/modules/IDE/actions/toast.js @@ -1,23 +1,12 @@ -import * as ActionTypes from '../../../constants'; +import { setToast, hideToast } from '../reducers/toast'; -export function hideToast() { - return { - type: ActionTypes.HIDE_TOAST - }; -} +export { hideToast } from '../reducers/toast'; -export function showToast(time) { - return (dispatch) => { - dispatch({ - type: ActionTypes.SHOW_TOAST - }); - setTimeout(() => dispatch(hideToast()), time); - }; -} +export const TOAST_DISPLAY_TIME_MS = 1500; -export function setToastText(text) { - return { - type: ActionTypes.SET_TOAST_TEXT, - text +export const showToast = + (text, timeout = TOAST_DISPLAY_TIME_MS) => + (dispatch) => { + dispatch(setToast(text)); + setTimeout(() => dispatch(hideToast()), timeout); }; -} diff --git a/client/modules/IDE/components/About.jsx b/client/modules/IDE/components/About.jsx index 6a68828a65..6b9dd16889 100644 --- a/client/modules/IDE/components/About.jsx +++ b/client/modules/IDE/components/About.jsx @@ -2,13 +2,13 @@ import React from 'react'; import { useSelector } from 'react-redux'; import { Helmet } from 'react-helmet'; import { useTranslation } from 'react-i18next'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; import SquareLogoIcon from '../../../images/p5js-square-logo.svg'; // import PlayIcon from '../../../images/play.svg'; import AsteriskIcon from '../../../images/p5-asterisk.svg'; import packageData from '../../../../package.json'; -function About(props) { +function About() { const { t } = useTranslation(); const p5version = useSelector((state) => { const index = state.files.find((file) => file.name === 'index.html'); diff --git a/client/modules/IDE/components/AddToCollectionList.jsx b/client/modules/IDE/components/AddToCollectionList.jsx index 26addfaa34..12bd2fbb80 100644 --- a/client/modules/IDE/components/AddToCollectionList.jsx +++ b/client/modules/IDE/components/AddToCollectionList.jsx @@ -8,7 +8,6 @@ import { withTranslation } from 'react-i18next'; import * as ProjectActions from '../actions/project'; import * as ProjectsActions from '../actions/projects'; import * as CollectionsActions from '../actions/collections'; -import * as ToastActions from '../actions/toast'; import * as SortingActions from '../actions/sorting'; import getSortedCollections from '../selectors/collections'; import Loader from '../../App/components/loader'; @@ -171,7 +170,6 @@ function mapDispatchToProps(dispatch) { CollectionsActions, ProjectsActions, ProjectActions, - ToastActions, SortingActions ), dispatch diff --git a/client/modules/IDE/components/AddToCollectionSketchList.jsx b/client/modules/IDE/components/AddToCollectionSketchList.jsx index e42aff8d60..c3d116e14f 100644 --- a/client/modules/IDE/components/AddToCollectionSketchList.jsx +++ b/client/modules/IDE/components/AddToCollectionSketchList.jsx @@ -7,7 +7,6 @@ import { withTranslation } from 'react-i18next'; // import find from 'lodash/find'; import * as ProjectsActions from '../actions/projects'; import * as CollectionsActions from '../actions/collections'; -import * as ToastActions from '../actions/toast'; import * as SortingActions from '../actions/sorting'; import getSortedSketches from '../selectors/projects'; import Loader from '../../App/components/loader'; @@ -145,13 +144,7 @@ function mapStateToProps(state) { function mapDispatchToProps(dispatch) { return bindActionCreators( - Object.assign( - {}, - ProjectsActions, - CollectionsActions, - ToastActions, - SortingActions - ), + Object.assign({}, ProjectsActions, CollectionsActions, SortingActions), dispatch ); } diff --git a/client/modules/IDE/components/AssetList.jsx b/client/modules/IDE/components/AssetList.jsx index a77a0d6d66..559f60c580 100644 --- a/client/modules/IDE/components/AssetList.jsx +++ b/client/modules/IDE/components/AssetList.jsx @@ -2,7 +2,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; import { Helmet } from 'react-helmet'; import prettyBytes from 'pretty-bytes'; import { withTranslation } from 'react-i18next'; diff --git a/client/modules/IDE/components/AssetPreview.jsx b/client/modules/IDE/components/AssetPreview.jsx new file mode 100644 index 0000000000..a8ace64b54 --- /dev/null +++ b/client/modules/IDE/components/AssetPreview.jsx @@ -0,0 +1,48 @@ +import mime from 'mime'; +import PropTypes from 'prop-types'; +import React from 'react'; +import styled from 'styled-components'; + +const Audio = styled.audio` + width: 90%; + margin: 30px 5%; +`; + +const Image = styled.img` + max-width: 100%; + height: auto; +`; + +const Video = styled.video` + max-width: 100%; + height: auto; +`; + +function AssetPreview({ url, name }) { + const contentType = mime.getType(url); + const type = contentType?.split('/')[0]; + + switch (type) { + case 'image': + return {`Preview; + case 'audio': + // eslint-disable-next-line jsx-a11y/media-has-caption + return
  • - + @@ -394,114 +188,32 @@ const MobileIDEView = (props) => { )} - + dispatch(toggleConsole()), + inverted: true + }, + { + icon: SaveIcon, + aria: t('Common.Save'), // TODO: translation for 'Save project'? + action: () => + dispatch(saveProject(cmController.getContent(), false, true)) + }, + { + icon: FolderIcon, + aria: t('Editor.OpenSketchARIA'), + action: toggleExplorer + } + ]} + /> ); }; -const handleGlobalKeydownProps = { - expandConsole: PropTypes.func.isRequired, - collapseConsole: PropTypes.func.isRequired, - expandSidebar: PropTypes.func.isRequired, - collapseSidebar: PropTypes.func.isRequired, - - setAllAccessibleOutput: PropTypes.func.isRequired, - saveProject: PropTypes.func.isRequired, - cloneProject: PropTypes.func.isRequired, - showErrorModal: PropTypes.func.isRequired, - - closeNewFolderModal: PropTypes.func.isRequired, - closeUploadFileModal: PropTypes.func.isRequired, - closeNewFileModal: PropTypes.func.isRequired, - isUserOwner: PropTypes.bool.isRequired -}; - -MobileIDEView.propTypes = { - ide: PropTypes.shape({ - consoleIsExpanded: PropTypes.bool.isRequired - }).isRequired, - - preferences: PropTypes.shape({}).isRequired, - - project: PropTypes.shape({ - id: PropTypes.string, - name: PropTypes.string.isRequired, - owner: PropTypes.shape({ - username: PropTypes.string, - id: PropTypes.string - }) - }).isRequired, - - selectedFile: PropTypes.shape({ - id: PropTypes.string.isRequired, - content: 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, - - toggleForceDesktop: PropTypes.func.isRequired, - - user: PropTypes.shape({ - authenticated: PropTypes.bool.isRequired, - id: PropTypes.string, - username: PropTypes.string - }).isRequired, - - logoutUser: PropTypes.func.isRequired, - - getProject: PropTypes.func.isRequired, - clearPersistedState: PropTypes.func.isRequired, - params: PropTypes.shape({ - project_id: PropTypes.string, - username: PropTypes.string - }).isRequired, - - startSketch: PropTypes.func.isRequired, - - unsavedChanges: PropTypes.bool.isRequired, - 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'), - ide: state.ide, - files: state.files, - unsavedChanges: state.ide.unsavedChanges, - preferences: state.preferences, - user: state.user, - project: state.project, - console: state.console, - isUserOwner: getIsUserOwner(state) - }; -} - -const mapDispatchToProps = (dispatch) => - bindActionCreators( - { - ...ProjectActions, - ...IDEActions, - ...ConsoleActions, - ...PreferencesActions, - ...EditorAccessibilityActions - }, - dispatch - ); - -export default withRouter( - connect(mapStateToProps, mapDispatchToProps)(MobileIDEView) -); +export default MobileIDEView; diff --git a/client/modules/IDE/reducers/ide.js b/client/modules/IDE/reducers/ide.js index 8d45b8b50a..7d8fc85fdc 100644 --- a/client/modules/IDE/reducers/ide.js +++ b/client/modules/IDE/reducers/ide.js @@ -1,8 +1,10 @@ -import * as ActionTypes from '../../../constants'; +import { createSlice } from '@reduxjs/toolkit'; const initialState = { isPlaying: false, + // TODO: this doesn't do anything. isAccessibleOutputPlaying: false, + // TODO: rename ambiguous property modalIsVisible: false, sidebarIsExpanded: false, consoleIsExpanded: true, @@ -10,12 +12,14 @@ const initialState = { projectOptionsVisible: false, newFolderModalVisible: false, uploadFileModalVisible: false, + // TODO: nested properties instead of all at top-level shareModalVisible: false, shareModalProjectId: 'abcd', shareModalProjectName: 'My Cute Sketch', shareModalProjectUsername: 'p5_user', keyboardShortcutVisible: false, unsavedChanges: false, + // TODO: remove dead code, see: PR #849 and issue #698 infiniteLoop: false, previewIsRefreshing: false, infiniteLoopMessage: '', @@ -26,105 +30,139 @@ const initialState = { parentId: undefined }; -const ide = (state = initialState, action) => { - switch (action.type) { - case ActionTypes.START_SKETCH: - return Object.assign({}, state, { isPlaying: true }); - case ActionTypes.STOP_SKETCH: - return Object.assign({}, state, { isPlaying: false }); - case ActionTypes.START_ACCESSIBLE_OUTPUT: - return Object.assign({}, state, { isAccessibleOutputPlaying: true }); - case ActionTypes.STOP_ACCESSIBLE_OUTPUT: - return Object.assign({}, state, { isAccessibleOutputPlaying: false }); - case ActionTypes.CONSOLE_EVENT: - return Object.assign({}, state, { consoleEvent: action.event }); - case ActionTypes.SHOW_MODAL: - return Object.assign({}, state, { - modalIsVisible: true, - parentId: action.parentId, - newFolderModalVisible: false - }); - case ActionTypes.HIDE_MODAL: - return Object.assign({}, state, { modalIsVisible: false }); - case ActionTypes.COLLAPSE_SIDEBAR: - return Object.assign({}, state, { sidebarIsExpanded: false }); - case ActionTypes.EXPAND_SIDEBAR: - return Object.assign({}, state, { sidebarIsExpanded: true }); - case ActionTypes.COLLAPSE_CONSOLE: - return Object.assign({}, state, { consoleIsExpanded: false }); - case ActionTypes.EXPAND_CONSOLE: - return Object.assign({}, state, { consoleIsExpanded: true }); - case ActionTypes.OPEN_PREFERENCES: - return Object.assign({}, state, { preferencesIsVisible: true }); - case ActionTypes.CLOSE_PREFERENCES: - return Object.assign({}, state, { preferencesIsVisible: false }); - case ActionTypes.RESET_PROJECT: - return initialState; - case ActionTypes.OPEN_PROJECT_OPTIONS: - return Object.assign({}, state, { projectOptionsVisible: true }); - case ActionTypes.CLOSE_PROJECT_OPTIONS: - return Object.assign({}, state, { projectOptionsVisible: false }); - case ActionTypes.SHOW_NEW_FOLDER_MODAL: - return Object.assign({}, state, { - newFolderModalVisible: true, - parentId: action.parentId, - modalIsVisible: false - }); - case ActionTypes.CLOSE_NEW_FOLDER_MODAL: - return Object.assign({}, state, { newFolderModalVisible: false }); - case ActionTypes.SHOW_SHARE_MODAL: - return Object.assign({}, state, { - shareModalVisible: true, - shareModalProjectId: action.payload.shareModalProjectId, - shareModalProjectName: action.payload.shareModalProjectName, - shareModalProjectUsername: action.payload.shareModalProjectUsername - }); - case ActionTypes.CLOSE_SHARE_MODAL: - return Object.assign({}, state, { shareModalVisible: false }); - case ActionTypes.SHOW_KEYBOARD_SHORTCUT_MODAL: - return Object.assign({}, state, { keyboardShortcutVisible: true }); - case ActionTypes.CLOSE_KEYBOARD_SHORTCUT_MODAL: - return Object.assign({}, state, { keyboardShortcutVisible: false }); - 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 - }); - case ActionTypes.RESET_INFINITE_LOOPS: - return Object.assign({}, state, { - infiniteLoop: false, - infiniteLoopMessage: '' - }); - case ActionTypes.START_SKETCH_REFRESH: - return Object.assign({}, state, { previewIsRefreshing: true }); - case ActionTypes.END_SKETCH_REFRESH: - return Object.assign({}, state, { previewIsRefreshing: false }); - case ActionTypes.JUST_OPENED_PROJECT: - return Object.assign({}, state, { justOpenedProject: true }); - case ActionTypes.RESET_JUST_OPENED_PROJECT: - return Object.assign({}, state, { justOpenedProject: false }); - case ActionTypes.SET_PREVIOUS_PATH: - return Object.assign({}, state, { previousPath: action.path }); - case ActionTypes.SHOW_ERROR_MODAL: - return Object.assign({}, state, { errorType: action.modalType }); - case ActionTypes.HIDE_ERROR_MODAL: - return Object.assign({}, state, { errorType: undefined }); - case ActionTypes.HIDE_RUNTIME_ERROR_WARNING: - return Object.assign({}, state, { runtimeErrorWarningVisible: false }); - 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 - }); - case ActionTypes.CLOSE_UPLOAD_FILE_MODAL: - return Object.assign({}, state, { uploadFileModalVisible: false }); - default: - return state; +const ideSlice = createSlice({ + name: 'ide', + initialState, + reducers: { + startVisualSketch: (state) => { + state.isPlaying = true; + }, + stopVisualSketch: (state) => { + state.isPlaying = false; + }, + startAccessibleOutput: (state) => { + state.isAccessibleOutputPlaying = true; + }, + stopAccessibleOutput: (state) => { + state.isAccessibleOutputPlaying = false; + }, + consoleEvent: (state, action) => { + state.consoleEvent = action.payload; + }, + collapseSidebar: (state) => { + state.sidebarIsExpanded = false; + }, + expandSidebar: (state) => { + state.sidebarIsExpanded = true; + }, + toggleSidebar: (state) => { + state.sidebarIsExpanded = !state.sidebarIsExpanded; + }, + collapseConsole: (state) => { + state.consoleIsExpanded = false; + }, + expandConsole: (state) => { + state.consoleIsExpanded = true; + }, + toggleConsole: (state) => { + state.consoleIsExpanded = !state.consoleIsExpanded; + }, + openPreferences: (state) => { + state.preferencesIsVisible = true; + }, + closePreferences: (state) => { + state.preferencesIsVisible = false; + }, + openProjectOptions: (state) => { + state.projectOptionsVisible = true; + }, + closeProjectOptions: (state) => { + state.projectOptionsVisible = false; + }, + resetProject: () => initialState, + // TODO: rename to openNewFileModal or showNewFileModal + newFile: (state, action) => { + state.modalIsVisible = true; + // TODO: nested properties + state.parentId = action.payload; + state.newFolderModalVisible = false; + }, + closeNewFileModal: (state) => { + state.modalIsVisible = false; + }, + // TODO: rename to openNewFolderModal or showNewFolderModal + newFolder: (state, action) => { + state.newFolderModalVisible = true; + state.parentId = action.payload; + state.modalIsVisible = false; + }, + closeNewFolderModal: (state) => { + state.newFolderModalVisible = false; + }, + openUploadFileModal: (state, action) => { + state.uploadFileModalVisible = true; + state.parentId = action.payload; + }, + closeUploadFileModal: (state) => { + state.uploadFileModalVisible = false; + }, + showShareModal: (state, action) => { + state.shareModalVisible = true; + state.shareModalProjectId = action.payload.shareModalProjectId; + state.shareModalProjectName = action.payload.shareModalProjectName; + state.shareModalProjectUsername = + action.payload.shareModalProjectUsername; + }, + closeShareModal: (state) => { + state.shareModalVisible = false; + }, + showKeyboardShortcutModal: (state) => { + state.keyboardShortcutVisible = true; + }, + closeKeyboardShortcutModal: (state) => { + state.keyboardShortcutVisible = false; + }, + showErrorModal: (state, action) => { + state.errorType = action.payload; + }, + hideErrorModal: (state) => { + state.errorType = undefined; + }, + setUnsavedChanges: (state, action) => { + state.unsavedChanges = action.payload; + }, + detectInfiniteLoops: (state, action) => { + state.infiniteLoop = true; + state.infiniteLoopMessage = action.payload; + }, + resetInfiniteLoops: (state) => { + state.infiniteLoop = false; + state.infiniteLoopMessage = ''; + }, + startRefreshSketch: (state) => { + state.previewIsRefreshing = true; + }, + endSketchRefresh: (state) => { + state.previewIsRefreshing = false; + }, + justOpenedProject: (state) => { + state.justOpenedProject = true; + }, + resetJustOpenedProject: (state) => { + state.justOpenedProject = false; + }, + setPreviousPath: (state, action) => { + state.previousPath = action.payload; + }, + showRuntimeErrorWarning: (state) => { + state.runtimeErrorWarningVisible = true; + }, + hideRuntimeErrorWarning: (state) => { + state.runtimeErrorWarningVisible = false; + } } -}; +}); + +export const ideActions = ideSlice.actions; -export default ide; +export default ideSlice.reducer; diff --git a/client/modules/IDE/reducers/toast.js b/client/modules/IDE/reducers/toast.js index b680c7cfef..9c092862a6 100644 --- a/client/modules/IDE/reducers/toast.js +++ b/client/modules/IDE/reducers/toast.js @@ -1,21 +1,24 @@ -import * as ActionTypes from '../../../constants'; +import { createSlice } from '@reduxjs/toolkit'; const initialState = { isVisible: false, text: '' }; -const toast = (state = initialState, action) => { - switch (action.type) { - case ActionTypes.SHOW_TOAST: - return Object.assign({}, state, { isVisible: true }); - case ActionTypes.HIDE_TOAST: - return Object.assign({}, state, { isVisible: false }); - case ActionTypes.SET_TOAST_TEXT: - return Object.assign({}, state, { text: action.text }); - default: - return state; +const toastSlice = createSlice({ + name: 'toast', + initialState, + reducers: { + setToast: (state, action) => { + state.isVisible = true; + state.text = action.payload; + }, + hideToast: (state) => { + state.isVisible = false; + } } -}; +}); + +export const { setToast, hideToast } = toastSlice.actions; -export default toast; +export default toastSlice.reducer; diff --git a/client/modules/IDE/selectors/collections.js b/client/modules/IDE/selectors/collections.js index 207dce39a9..136a0a7920 100644 --- a/client/modules/IDE/selectors/collections.js +++ b/client/modules/IDE/selectors/collections.js @@ -1,4 +1,4 @@ -import { createSelector } from 'reselect'; +import { createSelector } from '@reduxjs/toolkit'; import differenceInMilliseconds from 'date-fns/differenceInMilliseconds'; import find from 'lodash/find'; import orderBy from 'lodash/orderBy'; diff --git a/client/modules/IDE/selectors/files.js b/client/modules/IDE/selectors/files.js new file mode 100644 index 0000000000..2e2699740e --- /dev/null +++ b/client/modules/IDE/selectors/files.js @@ -0,0 +1,15 @@ +import { createSelector } from '@reduxjs/toolkit'; + +const selectFiles = (state) => state.files; + +export const selectRootFile = createSelector(selectFiles, (files) => + files.find((file) => file.name === 'root') +); + +export const selectActiveFile = createSelector( + selectFiles, + (files) => + files.find((file) => file.isSelectedFile) || + files.find((file) => file.name === 'sketch.js') || + files.find((file) => file.name !== 'root') +); diff --git a/client/modules/IDE/selectors/project.js b/client/modules/IDE/selectors/project.js index 26197f3b00..cdf7d61826 100644 --- a/client/modules/IDE/selectors/project.js +++ b/client/modules/IDE/selectors/project.js @@ -1,4 +1,4 @@ -import { createSelector } from 'reselect'; +import { createSelector } from '@reduxjs/toolkit'; export const selectProjectOwner = (state) => state.project.owner; export const selectProjectId = (state) => state.project.id; diff --git a/client/modules/IDE/selectors/projects.js b/client/modules/IDE/selectors/projects.js index 08701d211c..d5ba29a268 100644 --- a/client/modules/IDE/selectors/projects.js +++ b/client/modules/IDE/selectors/projects.js @@ -1,4 +1,4 @@ -import { createSelector } from 'reselect'; +import { createSelector } from '@reduxjs/toolkit'; import differenceInMilliseconds from 'date-fns/differenceInMilliseconds'; import orderBy from 'lodash/orderBy'; import { DIRECTION } from '../actions/sorting'; diff --git a/client/modules/IDE/selectors/users.js b/client/modules/IDE/selectors/users.js index 4086348041..4923c78e68 100644 --- a/client/modules/IDE/selectors/users.js +++ b/client/modules/IDE/selectors/users.js @@ -1,11 +1,12 @@ -import { createSelector } from 'reselect'; +import { createSelector } from '@reduxjs/toolkit'; import getConfig from '../../../utils/getConfig'; -const getAuthenticated = (state) => state.user.authenticated; +export const getAuthenticated = (state) => state.user.authenticated; const getTotalSize = (state) => state.user.totalSize; const getAssetsTotalSize = (state) => state.assets.totalSize; -const getSketchOwner = (state) => state.project.owner; +export const getSketchOwner = (state) => state.project.owner; const getUserId = (state) => state.user.id; +export const selectUsername = (state) => state.user.username; const limit = getConfig('UPLOAD_LIMIT') || 250000000; export const getCanUploadMedia = createSelector( @@ -39,3 +40,9 @@ export const getIsUserOwner = createSelector( return sketchOwner.id === userId; } ); + +export const selectCanEditSketch = createSelector( + getSketchOwner, + getIsUserOwner, + (sketchOwner, isOwner) => !sketchOwner || isOwner +); diff --git a/client/modules/Mobile/MobileDashboardView.jsx b/client/modules/Mobile/MobileDashboardView.jsx index f5a6beb770..c484f6a0ac 100644 --- a/client/modules/Mobile/MobileDashboardView.jsx +++ b/client/modules/Mobile/MobileDashboardView.jsx @@ -1,12 +1,11 @@ import React from 'react'; -import PropTypes from 'prop-types'; import styled from 'styled-components'; import { useSelector } from 'react-redux'; -import { withRouter } from 'react-router'; +import { useLocation, useParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import Screen from '../../components/mobile/MobileScreen'; -import Header from '../../components/mobile/Header'; +import MobileHeader from '../../components/mobile/MobileHeader'; import IconButton from '../../components/mobile/IconButton'; import { ExitIcon, MoreIcon } from '../../common/icons'; import Footer from '../../components/mobile/Footer'; @@ -178,8 +177,10 @@ const NavItem = styled.li` const renderPanel = (name, props) => ((Component) => Component && )(Panels[name]); -const MobileDashboard = ({ params, location }) => { +const MobileDashboard = () => { const user = useSelector((state) => state.user); + const location = useLocation(); + const params = useParams(); const { username: paramsUsername } = params; const { pathname } = location; const { t } = useTranslation(); @@ -198,8 +199,8 @@ const MobileDashboard = ({ params, location }) => { ); return ( - -
    + { -
    + { ); }; -MobileDashboard.propTypes = { - location: PropTypes.shape({ - pathname: PropTypes.string.isRequired - }).isRequired, - params: PropTypes.shape({ - username: PropTypes.string.isRequired - }) -}; -MobileDashboard.defaultProps = { params: {} }; - -export default withRouter(MobileDashboard); +export default MobileDashboard; diff --git a/client/modules/Mobile/MobilePreferences.jsx b/client/modules/Mobile/MobilePreferences.jsx index 5dcb115092..87e18da22b 100644 --- a/client/modules/Mobile/MobilePreferences.jsx +++ b/client/modules/Mobile/MobilePreferences.jsx @@ -1,165 +1,36 @@ import React from 'react'; -import { bindActionCreators } from 'redux'; -import { useSelector, useDispatch } from 'react-redux'; -import { withRouter } from 'react-router'; import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; - -import * as PreferencesActions from '../IDE/actions/preferences'; -import * as IdeActions from '../IDE/actions/ide'; +import { ExitIcon } from '../../common/icons'; +import MobileHeader from '../../components/mobile/MobileHeader'; import IconButton from '../../components/mobile/IconButton'; import Screen from '../../components/mobile/MobileScreen'; -import Header from '../../components/mobile/Header'; -import PreferencePicker from '../../components/mobile/PreferencePicker'; -import { ExitIcon } from '../../common/icons'; -import { remSize, prop } from '../../theme'; -import { - optionsPickOne, - preferenceOnOff -} from '../IDE/components/Preferences/PreferenceCreators'; - -const Content = styled.div` - z-index: 0; - margin-top: ${remSize(68)}; -`; - -const SectionHeader = styled.h2` - color: ${prop('primaryTextColor')}; - padding-top: ${remSize(32)}; -`; - -const SectionSubeader = styled.h3` - color: ${prop('primaryTextColor')}; +import { remSize } from '../../theme'; +import Preferences from '../IDE/components/Preferences'; + +const PreferencesContent = styled(Preferences)` + margin-left: auto; + margin-right: auto; + padding-top: ${remSize(84)}; + height: 100vh; + max-height: 100vh; `; const MobilePreferences = () => { - // Props - const { - theme, - autosave, - linewrap, - autocompleteHinter, - textOutput, - gridOutput, - lineNumbers, - lintWarning - } = useSelector((state) => state.preferences); - - // Actions - const { - setTheme, - setAutosave, - setLinewrap, - setAutocompleteHinter, - setTextOutput, - setGridOutput, - setLineNumbers, - setLintWarning - } = bindActionCreators( - { ...PreferencesActions, ...IdeActions }, - useDispatch() - ); - const { t } = useTranslation(); - const generalSettings = [ - { - title: t('MobilePreferences.Theme'), - value: theme, - options: optionsPickOne( - t('MobilePreferences.Theme'), - t('MobilePreferences.LightTheme'), - t('MobilePreferences.DarkTheme'), - t('MobilePreferences.HighContrastTheme') - ), - onSelect: (x) => setTheme(x) // setTheme - }, - preferenceOnOff( - t('MobilePreferences.Autosave'), - autosave, - setAutosave, - 'autosave' - ), - preferenceOnOff( - t('MobilePreferences.AutocompleteHinter'), - autocompleteHinter, - setAutocompleteHinter, - 'autocompleteHinter' - ), - 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' - ) - ]; - - const accessibilitySettings = [ - preferenceOnOff( - t('MobilePreferences.LineNumbers'), - lineNumbers, - setLineNumbers - ), - preferenceOnOff( - t('MobilePreferences.LintWarningSound'), - lintWarning, - setLintWarning - ) - ]; - return ( - +
    -
    - -
    -
    - - - {t('MobilePreferences.GeneralSettings')} - - {generalSettings.map((option) => ( - - ))} - - - {t('MobilePreferences.Accessibility')} - - {accessibilitySettings.map((option) => ( - - ))} - - - {t('MobilePreferences.AccessibleOutput')} - - - {t('MobilePreferences.UsedScreenReader')} - - {outputSettings.map((option) => ( - - ))} - -
    + + + +
    ); }; -export default withRouter(MobilePreferences); +export default MobilePreferences; diff --git a/client/modules/Mobile/MobileSketchView.jsx b/client/modules/Mobile/MobileSketchView.jsx index dc69550f6f..27832a8ddd 100644 --- a/client/modules/Mobile/MobileSketchView.jsx +++ b/client/modules/Mobile/MobileSketchView.jsx @@ -1,90 +1,39 @@ -import React from 'react'; -import { bindActionCreators } from 'redux'; -import { useSelector, useDispatch } from 'react-redux'; -import Header from '../../components/mobile/Header'; +import React, { useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useDispatch, useSelector } from 'react-redux'; +import { ExitIcon } from '../../common/icons'; +import Footer from '../../components/mobile/Footer'; +import MobileHeader from '../../components/mobile/MobileHeader'; import IconButton from '../../components/mobile/IconButton'; -import PreviewFrame from '../IDE/components/PreviewFrame'; import Screen from '../../components/mobile/MobileScreen'; +import { startSketch, stopSketch } from '../IDE/actions/ide'; import Console from '../IDE/components/Console'; -import * as ProjectActions from '../IDE/actions/project'; -import * as IDEActions from '../IDE/actions/ide'; -import * as PreferencesActions from '../IDE/actions/preferences'; -import * as ConsoleActions from '../IDE/actions/console'; -import * as FilesActions from '../IDE/actions/files'; - -import { getHTMLFile } from '../IDE/reducers/files'; - -import { ExitIcon } from '../../common/icons'; -import Footer from '../../components/mobile/Footer'; +import PreviewFrame from '../IDE/components/PreviewFrame'; import Content from './MobileViewContent'; const MobileSketchView = () => { - const { files, ide, preferences } = useSelector((state) => state); + const { t } = useTranslation(); + const dispatch = useDispatch(); - 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') - ); + useEffect(() => { + dispatch(startSketch()); + return () => { + dispatch(stopSketch()); + }; + }, [dispatch]); - const { - setTextOutput, - setGridOutput, - dispatchConsoleEvent, - endSketchRefresh, - stopSketch, - setBlobUrl, - expandConsole, - clearConsole - } = bindActionCreators( - { - ...ProjectActions, - ...IDEActions, - ...PreferencesActions, - ...ConsoleActions, - ...FilesActions - }, - useDispatch() - ); + const projectName = useSelector((state) => state.project.name); return ( - -
    + + } title={projectName} /> - - } - content={selectedFile.content} - isPlaying - isAccessibleOutputPlaying={ide.isAccessibleOutputPlaying} - previewIsRefreshing={ide.previewIsRefreshing} - textOutput={preferences.textOutput} - gridOutput={preferences.gridOutput} - autorefresh={preferences.autorefresh} - setTextOutput={setTextOutput} - setGridOutput={setGridOutput} - dispatchConsoleEvent={dispatchConsoleEvent} - endSketchRefresh={endSketchRefresh} - stopSketch={stopSketch} - setBlobUrl={setBlobUrl} - expandConsole={expandConsole} - clearConsole={clearConsole} - /> +