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);