From 00175f521d9624bf88ab16be8a1d2acfda3ad129 Mon Sep 17 00:00:00 2001 From: Christopher Willis-Ford Date: Wed, 17 Jul 2019 17:16:18 -0700 Subject: [PATCH] set project title from save file name --- package-lock.json | 6 ++ package.json | 1 + src/main/ScratchDesktopTelemetry.js | 16 ++++++ src/main/index.js | 9 +++ src/renderer/app.jsx | 89 ++++++++++++++++++++++------- 5 files changed, 99 insertions(+), 22 deletions(-) diff --git a/package-lock.json b/package-lock.json index 50dc847..0f62845 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7640,6 +7640,12 @@ "integrity": "sha512-DHb1ub+rMjjrxqlB3H56/6MXtm1lSksDp2rA2cNWjG8mlDUYFhUj3Di2Zn5IwSU87xLv8tNIQ7sSwE/YOX/D/Q==", "dev": true }, + "lodash.bindall": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.bindall/-/lodash.bindall-4.4.0.tgz", + "integrity": "sha1-p7/Ugro9LnBxad/NyZPrv2w9eZg=", + "dev": true + }, "lodash.defaultsdeep": { "version": "4.6.1", "resolved": "https://registry.npmjs.org/lodash.defaultsdeep/-/lodash.defaultsdeep-4.6.1.tgz", diff --git a/package.json b/package.json index 3b2315c..55b56f2 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "eslint-plugin-import": "^2.18.0", "eslint-plugin-react": "^7.14.2", "intl": "1.2.5", + "lodash.bindall": "^4.4.0", "lodash.defaultsdeep": "^4.6.1", "mkdirp": "^0.5.1", "nets": "^3.2.0", diff --git a/src/main/ScratchDesktopTelemetry.js b/src/main/ScratchDesktopTelemetry.js index 60f8e09..a2352d2 100644 --- a/src/main/ScratchDesktopTelemetry.js +++ b/src/main/ScratchDesktopTelemetry.js @@ -49,9 +49,25 @@ class ScratchDesktopTelemetry { } projectDidSave (metadata = {}) { + // Since the save dialog appears on the main process the GUI does not wait for the actual save to complete. + // That means the GUI sends this event before we know the file name used for the save, which is where the new + // project title comes from. Instead, just hold on to this metadata pending a `projectSaveCompleted` event + // from the save code on the main process. If the user cancels the save this data will be cleared. + this._pendingProjectSave = metadata; + } + + projectSaveCompleted (newProjectTitle) { + const metadata = this._pendingProjectSave; + this._pendingProjectSave = null; + + metadata.projectName = newProjectTitle; this._telemetryClient.addEvent('project::save', this._buildMetadata(metadata)); } + projectSaveCanceled () { + this._pendingProjectSave = null; + } + projectWasCreated (metadata = {}) { this._telemetryClient.addEvent('project::create', this._buildMetadata(metadata)); } diff --git a/src/main/index.js b/src/main/index.js index a00c2c7..4195197 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -62,13 +62,22 @@ const createMainWindow = () => { if (extName) { const extNameNoDot = extName.replace(/^\./, ''); const options = { + defaultPath: baseName, filters: [getFilterForExtension(extNameNoDot)] }; const userChosenPath = dialog.showSaveDialog(window, options); if (userChosenPath) { item.setSavePath(userChosenPath); + const newProjectTitle = path.basename(userChosenPath, extName); + webContents.send('setTitleFromSave', {title: newProjectTitle}); + + // "setTitleFromSave" will set the project title but GUI has already reported the telemetry event + // using the old title. This call lets the telemetry client know that the save was actually completed + // and the event should be committed to the event queue with this new title. + telemetry.projectSaveCompleted(newProjectTitle); } else { item.cancel(); + telemetry.projectSaveCanceled(); } } }); diff --git a/src/renderer/app.jsx b/src/renderer/app.jsx index 257315b..8f57506 100644 --- a/src/renderer/app.jsx +++ b/src/renderer/app.jsx @@ -1,7 +1,10 @@ import {ipcRenderer, shell} from 'electron'; +import bindAll from 'lodash.bindall'; +import PropTypes from 'prop-types'; import React from 'react'; import ReactDOM from 'react-dom'; -import GUI, {AppStateHOC} from 'scratch-gui'; +import {compose} from 'redux'; +import GUI, {AppStateHOC, TitledHOC} from 'scratch-gui'; import ElectronStorageHelper from '../common/ElectronStorageHelper'; @@ -23,27 +26,69 @@ appTarget.className = styles.app || 'app'; // TODO document.body.appendChild(appTarget); GUI.setAppElement(appTarget); -const WrappedGui = AppStateHOC(GUI); -const onStorageInit = storageInstance => { - storageInstance.addHelper(new ElectronStorageHelper(storageInstance)); - // storageInstance.addOfficialScratchWebStores(); // TODO: do we want this? -}; - -const guiProps = { - onStorageInit, - isScratchDesktop: true, - projectId: defaultProjectId, - showTelemetryModal: (typeof ipcRenderer.sendSync('getTelemetryDidOptIn')) !== 'boolean', - onTelemetryModalOptIn: () => { - ipcRenderer.send('setTelemetryDidOptIn', true); - }, - onTelemetryModalOptOut: () => { - ipcRenderer.send('setTelemetryDidOptIn', false); - }, - onProjectTelemetryEvent: (event, metadata) => { - ipcRenderer.send(event, metadata); +const ScratchDesktopHOC = function (WrappedComponent) { + class ScratchDesktopComponent extends React.Component { + constructor (props) { + super(props); + bindAll(this, [ + 'handleProjectTelemetryEvent', + 'handleSetTitleFromSave', + 'handleStorageInit', + 'handleTelemetryModalOptIn', + 'handleTelemetryModalOptOut' + ]); + } + componentDidMount () { + ipcRenderer.on('setTitleFromSave', this.handleSetTitleFromSave); + } + componentWillUnmount () { + ipcRenderer.removeListener('setTitleFromSave', this.handleSetTitleFromSave); + } + handleProjectTelemetryEvent (event, metadata) { + ipcRenderer.send(event, metadata); + } + handleSetTitleFromSave (event, args) { + this.props.onUpdateProjectTitle(args.title); + } + handleStorageInit (storageInstance) { + storageInstance.addHelper(new ElectronStorageHelper(storageInstance)); + } + handleTelemetryModalOptIn () { + ipcRenderer.send('setTelemetryDidOptIn', true); + } + handleTelemetryModalOptOut () { + ipcRenderer.send('setTelemetryDidOptIn', false); + } + render () { + const shouldShowTelemetryModal = (typeof ipcRenderer.sendSync('getTelemetryDidOptIn') !== 'boolean'); + return (); + } } + + ScratchDesktopComponent.propTypes = { + onUpdateProjectTitle: PropTypes.func + }; + + return ScratchDesktopComponent; }; -const wrappedGui = React.createElement(WrappedGui, guiProps); -ReactDOM.render(wrappedGui, appTarget); + +// 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 +// ability to compose reducers. +const WrappedGui = compose( + AppStateHOC, + TitledHOC, + ScratchDesktopHOC // must come after `TitledHOC` so it has access to `onUpdateProjectTitle` +)(GUI); + +ReactDOM.render(, appTarget);