From 728d705b1ac6768c47c89a8297cf77c838f7f977 Mon Sep 17 00:00:00 2001 From: Christopher Willis-Ford <7019101+cwillisf@users.noreply.github.com> Date: Tue, 26 Jan 2021 14:17:22 -0800 Subject: [PATCH] fix merge & rearrange components to satisfy lint --- src/renderer/ScratchDesktopAppStateHOC.jsx | 53 +++++ src/renderer/ScratchDesktopGUIHOC.jsx | 175 +++++++++++++++++ src/renderer/app.jsx | 215 +-------------------- 3 files changed, 230 insertions(+), 213 deletions(-) create mode 100644 src/renderer/ScratchDesktopAppStateHOC.jsx create mode 100644 src/renderer/ScratchDesktopGUIHOC.jsx diff --git a/src/renderer/ScratchDesktopAppStateHOC.jsx b/src/renderer/ScratchDesktopAppStateHOC.jsx new file mode 100644 index 0000000..13a7905 --- /dev/null +++ b/src/renderer/ScratchDesktopAppStateHOC.jsx @@ -0,0 +1,53 @@ +import {ipcRenderer} from 'electron'; +import bindAll from 'lodash.bindall'; +import React from 'react'; + +/** + * Higher-order component to add desktop logic to AppStateHOC. + * @param {Component} WrappedComponent - an AppStateHOC-like component to wrap. + * @returns {Component} - a component similar to AppStateHOC with desktop-specific logic added. + */ +const ScratchDesktopAppStateHOC = function (WrappedComponent) { + class ScratchDesktopAppStateComponent extends React.Component { + constructor (props) { + super(props); + bindAll(this, [ + 'handleTelemetryModalOptIn', + 'handleTelemetryModalOptOut' + ]); + this.state = { + // use `sendSync` because this should be set before first render + telemetryDidOptIn: ipcRenderer.sendSync('getTelemetryDidOptIn') + }; + } + handleTelemetryModalOptIn () { + ipcRenderer.send('setTelemetryDidOptIn', true); + ipcRenderer.invoke('getTelemetryDidOptIn').then(telemetryDidOptIn => { + this.setState({telemetryDidOptIn}); + }); + } + handleTelemetryModalOptOut () { + ipcRenderer.send('setTelemetryDidOptIn', false); + ipcRenderer.invoke('getTelemetryDidOptIn').then(telemetryDidOptIn => { + this.setState({telemetryDidOptIn}); + }); + } + render () { + const shouldShowTelemetryModal = (typeof ipcRenderer.sendSync('getTelemetryDidOptIn') !== 'boolean'); + + return (); + } + } + + return ScratchDesktopAppStateComponent; +}; + +export default ScratchDesktopAppStateHOC; diff --git a/src/renderer/ScratchDesktopGUIHOC.jsx b/src/renderer/ScratchDesktopGUIHOC.jsx new file mode 100644 index 0000000..97e5c9e --- /dev/null +++ b/src/renderer/ScratchDesktopGUIHOC.jsx @@ -0,0 +1,175 @@ +import {ipcRenderer, remote} from 'electron'; +import bindAll from 'lodash.bindall'; +import omit from 'lodash.omit'; +import PropTypes from 'prop-types'; +import React from 'react'; +import {connect} from 'react-redux'; +import GUIComponent from 'scratch-gui/src/components/gui/gui.jsx'; + +import { + LoadingStates, + onFetchedProjectData, + onLoadedProject, + defaultProjectId, + requestNewProject, + requestProjectUpload, + setProjectId +} from 'scratch-gui/src/reducers/project-state'; +import { + openLoadingProject, + closeLoadingProject, + openTelemetryModal +} from 'scratch-gui/src/reducers/modals'; + +import ElectronStorageHelper from '../common/ElectronStorageHelper'; + +import showPrivacyPolicy from './showPrivacyPolicy'; + +/** + * Higher-order component to add desktop logic to the GUI. + * @param {Component} WrappedComponent - a GUI-like component to wrap. + * @returns {Component} - a component similar to GUI with desktop-specific logic added. + */ +const ScratchDesktopGUIHOC = function (WrappedComponent) { + class ScratchDesktopGUIComponent extends React.Component { + constructor (props) { + super(props); + bindAll(this, [ + 'handleProjectTelemetryEvent', + 'handleSetTitleFromSave', + 'handleStorageInit', + 'handleUpdateProjectTitle' + ]); + this.props.onLoadingStarted(); + ipcRenderer.invoke('get-initial-project-data').then(initialProjectData => { + const hasInitialProject = initialProjectData && (initialProjectData.length > 0); + this.props.onHasInitialProject(hasInitialProject, this.props.loadingState); + if (!hasInitialProject) { + this.props.onLoadingCompleted(); + return; + } + this.props.vm.loadProject(initialProjectData).then( + () => { + this.props.onLoadingCompleted(); + this.props.onLoadedProject(this.props.loadingState, true); + }, + e => { + this.props.onLoadingCompleted(); + this.props.onLoadedProject(this.props.loadingState, false); + remote.dialog.showMessageBox(remote.getCurrentWindow(), { + type: 'error', + title: 'Failed to load project', + message: 'Invalid or corrupt project file.', + detail: e.message + }); + + // this effectively sets the default project ID + // TODO: maybe setting the default project ID should be implicit in `requestNewProject` + this.props.onHasInitialProject(false, this.props.loadingState); + + // restart as if we didn't have an initial project to load + this.props.onRequestNewProject(); + } + ); + }); + } + componentDidMount () { + ipcRenderer.on('setTitleFromSave', this.handleSetTitleFromSave); + } + componentWillUnmount () { + ipcRenderer.removeListener('setTitleFromSave', this.handleSetTitleFromSave); + } + handleClickAbout () { + ipcRenderer.send('open-about-window'); + } + handleProjectTelemetryEvent (event, metadata) { + ipcRenderer.send(event, metadata); + } + handleSetTitleFromSave (event, args) { + this.handleUpdateProjectTitle(args.title); + } + handleStorageInit (storageInstance) { + storageInstance.addHelper(new ElectronStorageHelper(storageInstance)); + } + handleUpdateProjectTitle (newTitle) { + this.setState({projectTitle: newTitle}); + } + render () { + const childProps = omit(this.props, Object.keys(ScratchDesktopGUIComponent.propTypes)); + + return ( this.handleClickAbout() + }, + { + title: 'Privacy Policy', + onClick: () => showPrivacyPolicy() + }, + { + title: 'Data Settings', + onClick: () => this.props.onTelemetrySettingsClicked() + } + ]} + onProjectTelemetryEvent={this.handleProjectTelemetryEvent} + onShowPrivacyPolicy={showPrivacyPolicy} + onStorageInit={this.handleStorageInit} + onUpdateProjectTitle={this.handleUpdateProjectTitle} + + // allow passed-in props to override any of the above + {...childProps} + />); + } + } + + ScratchDesktopGUIComponent.propTypes = { + loadingState: PropTypes.oneOf(LoadingStates), + onFetchedInitialProjectData: PropTypes.func, + onHasInitialProject: PropTypes.func, + onLoadedProject: PropTypes.func, + onLoadingCompleted: PropTypes.func, + onLoadingStarted: PropTypes.func, + onRequestNewProject: PropTypes.func, + onTelemetrySettingsClicked: PropTypes.func, + // using PropTypes.instanceOf(VM) here will cause prop type warnings due to VM mismatch + vm: GUIComponent.WrappedComponent.propTypes.vm + }; + const mapStateToProps = state => { + const loadingState = state.scratchGui.projectState.loadingState; + return { + loadingState: loadingState, + vm: state.scratchGui.vm + }; + }; + const mapDispatchToProps = dispatch => ({ + onLoadingStarted: () => dispatch(openLoadingProject()), + onLoadingCompleted: () => dispatch(closeLoadingProject()), + onHasInitialProject: (hasInitialProject, loadingState) => { + if (hasInitialProject) { + // emulate sb-file-uploader + return dispatch(requestProjectUpload(loadingState)); + } + + // `createProject()` might seem more appropriate but it's not a valid state transition here + // setting the default project ID is a valid transition from NOT_LOADED and acts like "create new" + return dispatch(setProjectId(defaultProjectId)); + }, + onFetchedInitialProjectData: (projectData, loadingState) => + dispatch(onFetchedProjectData(projectData, loadingState)), + onLoadedProject: (loadingState, loadSuccess) => { + const canSaveToServer = false; + return dispatch(onLoadedProject(loadingState, canSaveToServer, loadSuccess)); + }, + onRequestNewProject: () => dispatch(requestNewProject(false)), + onTelemetrySettingsClicked: () => dispatch(openTelemetryModal()) + }); + + return connect(mapStateToProps, mapDispatchToProps)(ScratchDesktopGUIComponent); +}; + +export default ScratchDesktopGUIHOC; diff --git a/src/renderer/app.jsx b/src/renderer/app.jsx index 7810fc5..d437381 100644 --- a/src/renderer/app.jsx +++ b/src/renderer/app.jsx @@ -1,32 +1,11 @@ -import {ipcRenderer, remote} from 'electron'; -import bindAll from 'lodash.bindall'; -import omit from 'lodash.omit'; -import PropTypes from 'prop-types'; import React from 'react'; -import {connect} from 'react-redux'; import {compose} from 'redux'; import GUI from 'scratch-gui/src/index'; -import GUIComponent from 'scratch-gui/src/components/gui/gui.jsx'; import AppStateHOC from 'scratch-gui/src/lib/app-state-hoc.jsx'; -import { - LoadingStates, - onFetchedProjectData, - onLoadedProject, - defaultProjectId, - requestNewProject, - requestProjectUpload, - setProjectId -} from 'scratch-gui/src/reducers/project-state'; -import { - openLoadingProject, - closeLoadingProject, - openTelemetryModal -} from 'scratch-gui/src/reducers/modals'; -import ElectronStorageHelper from '../common/ElectronStorageHelper'; - -import showPrivacyPolicy from './showPrivacyPolicy'; +import ScratchDesktopAppStateHOC from './ScratchDesktopAppStateHOC.jsx'; +import ScratchDesktopGUIHOC from './ScratchDesktopGUIHOC.jsx'; import styles from './app.css'; const appTarget = document.getElementById('app'); @@ -34,196 +13,6 @@ appTarget.className = styles.app || 'app'; GUI.setAppElement(appTarget); -/** - * Higher-order component to add desktop logic to AppStateHOC. - * @param {Component} WrappedComponent - an AppStateHOC-like component to wrap. - * @returns {Component} - a component similar to AppStateHOC with desktop-specific logic added. - */ -const ScratchDesktopAppStateHOC = function (WrappedComponent) { - const ScratchDesktopAppStateComponent = function (props) { - const shouldShowTelemetryModal = (typeof ipcRenderer.sendSync('getTelemetryDidOptIn') !== 'boolean'); - - return (); - }; - - return ScratchDesktopAppStateComponent; -}; - -/** - * Higher-order component to add desktop logic to the GUI. - * @param {Component} WrappedComponent - a GUI-like component to wrap. - * @returns {Component} - a component similar to GUI with desktop-specific logic added. - */ -const ScratchDesktopGUIHOC = function (WrappedComponent) { - class ScratchDesktopGUIComponent extends React.Component { - constructor (props) { - super(props); - bindAll(this, [ - 'handleProjectTelemetryEvent', - 'handleSetTitleFromSave', - 'handleStorageInit', - 'handleTelemetryModalOptIn', - 'handleTelemetryModalOptOut', - 'handleUpdateProjectTitle' - ]); - this.state = { - // use `sendSync` because this should be set before first render - telemetryDidOptIn: ipcRenderer.sendSync('getTelemetryDidOptIn') - }; - this.props.onLoadingStarted(); - ipcRenderer.invoke('get-initial-project-data').then(initialProjectData => { - const hasInitialProject = initialProjectData && (initialProjectData.length > 0); - this.props.onHasInitialProject(hasInitialProject, this.props.loadingState); - if (!hasInitialProject) { - this.props.onLoadingCompleted(); - return; - } - this.props.vm.loadProject(initialProjectData).then( - () => { - this.props.onLoadingCompleted(); - this.props.onLoadedProject(this.props.loadingState, true); - }, - e => { - this.props.onLoadingCompleted(); - this.props.onLoadedProject(this.props.loadingState, false); - remote.dialog.showMessageBox(remote.getCurrentWindow(), { - type: 'error', - title: 'Failed to load project', - message: 'Invalid or corrupt project file.', - detail: e.message - }); - - // this effectively sets the default project ID - // TODO: maybe setting the default project ID should be implicit in `requestNewProject` - this.props.onHasInitialProject(false, this.props.loadingState); - - // restart as if we didn't have an initial project to load - this.props.onRequestNewProject(); - } - ); - }); - } - componentDidMount () { - ipcRenderer.on('setTitleFromSave', this.handleSetTitleFromSave); - } - componentWillUnmount () { - ipcRenderer.removeListener('setTitleFromSave', this.handleSetTitleFromSave); - } - handleClickAbout () { - ipcRenderer.send('open-about-window'); - } - handleProjectTelemetryEvent (event, metadata) { - ipcRenderer.send(event, metadata); - } - handleSetTitleFromSave (event, args) { - this.handleUpdateProjectTitle(args.title); - } - handleStorageInit (storageInstance) { - storageInstance.addHelper(new ElectronStorageHelper(storageInstance)); - } - handleTelemetryModalOptIn () { - ipcRenderer.send('setTelemetryDidOptIn', true); - ipcRenderer.invoke('getTelemetryDidOptIn').then(telemetryDidOptIn => { - this.setState({telemetryDidOptIn}); - }); - } - handleTelemetryModalOptOut () { - ipcRenderer.send('setTelemetryDidOptIn', false); - ipcRenderer.invoke('getTelemetryDidOptIn').then(telemetryDidOptIn => { - this.setState({telemetryDidOptIn}); - }); - } - handleUpdateProjectTitle (newTitle) { - this.setState({projectTitle: newTitle}); - } - render () { - const shouldShowTelemetryModal = (typeof this.state.telemetryDidOptIn !== 'boolean'); - - const childProps = omit(this.props, Object.keys(ScratchDesktopGUIComponent.propTypes)); - - return ( this.handleClickAbout() - }, - { - title: 'Privacy Policy', - onClick: () => showPrivacyPolicy() - }, - { - title: 'Data Settings', - onClick: () => this.props.onTelemetrySettingsClicked() - } - ]} - onProjectTelemetryEvent={this.handleProjectTelemetryEvent} - onShowPrivacyPolicy={showPrivacyPolicy} - onStorageInit={this.handleStorageInit} - onTelemetryModalOptIn={this.handleTelemetryModalOptIn} - onTelemetryModalOptOut={this.handleTelemetryModalOptOut} - onUpdateProjectTitle={this.handleUpdateProjectTitle} - - // allow passed-in props to override any of the above - {...childProps} - />); - } - } - - ScratchDesktopGUIComponent.propTypes = { - loadingState: PropTypes.oneOf(LoadingStates), - onFetchedInitialProjectData: PropTypes.func, - onHasInitialProject: PropTypes.func, - onLoadedProject: PropTypes.func, - onLoadingCompleted: PropTypes.func, - onLoadingStarted: PropTypes.func, - onRequestNewProject: PropTypes.func, - onTelemetrySettingsClicked: PropTypes.func, - // using PropTypes.instanceOf(VM) here will cause prop type warnings due to VM mismatch - vm: GUIComponent.WrappedComponent.propTypes.vm - }; - const mapStateToProps = state => { - const loadingState = state.scratchGui.projectState.loadingState; - return { - loadingState: loadingState, - vm: state.scratchGui.vm - }; - }; - const mapDispatchToProps = dispatch => ({ - onLoadingStarted: () => dispatch(openLoadingProject()), - onLoadingCompleted: () => dispatch(closeLoadingProject()), - onHasInitialProject: (hasInitialProject, loadingState) => { - if (hasInitialProject) { - // emulate sb-file-uploader - return dispatch(requestProjectUpload(loadingState)); - } - - // `createProject()` might seem more appropriate but it's not a valid state transition here - // setting the default project ID is a valid transition from NOT_LOADED and acts like "create new" - return dispatch(setProjectId(defaultProjectId)); - }, - onFetchedInitialProjectData: (projectData, loadingState) => - dispatch(onFetchedProjectData(projectData, loadingState)), - onLoadedProject: (loadingState, loadSuccess) => { - const canSaveToServer = false; - return dispatch(onLoadedProject(loadingState, canSaveToServer, loadSuccess)); - }, - onRequestNewProject: () => dispatch(requestNewProject(false)), - onTelemetrySettingsClicked: () => dispatch(openTelemetryModal()) - }); - - return connect(mapStateToProps, mapDispatchToProps)(ScratchDesktopGUIComponent); -}; // note that redux's 'compose' function is just being used as a general utility to make // the hierarchy of HOC constructor calls clearer here; it has nothing to do with redux's