set project title from save file name

This commit is contained in:
Christopher Willis-Ford 2019-07-17 17:16:18 -07:00
parent 5b028a1d3d
commit 00175f521d
5 changed files with 99 additions and 22 deletions

6
package-lock.json generated
View file

@ -7640,6 +7640,12 @@
"integrity": "sha512-DHb1ub+rMjjrxqlB3H56/6MXtm1lSksDp2rA2cNWjG8mlDUYFhUj3Di2Zn5IwSU87xLv8tNIQ7sSwE/YOX/D/Q==", "integrity": "sha512-DHb1ub+rMjjrxqlB3H56/6MXtm1lSksDp2rA2cNWjG8mlDUYFhUj3Di2Zn5IwSU87xLv8tNIQ7sSwE/YOX/D/Q==",
"dev": true "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": { "lodash.defaultsdeep": {
"version": "4.6.1", "version": "4.6.1",
"resolved": "https://registry.npmjs.org/lodash.defaultsdeep/-/lodash.defaultsdeep-4.6.1.tgz", "resolved": "https://registry.npmjs.org/lodash.defaultsdeep/-/lodash.defaultsdeep-4.6.1.tgz",

View file

@ -45,6 +45,7 @@
"eslint-plugin-import": "^2.18.0", "eslint-plugin-import": "^2.18.0",
"eslint-plugin-react": "^7.14.2", "eslint-plugin-react": "^7.14.2",
"intl": "1.2.5", "intl": "1.2.5",
"lodash.bindall": "^4.4.0",
"lodash.defaultsdeep": "^4.6.1", "lodash.defaultsdeep": "^4.6.1",
"mkdirp": "^0.5.1", "mkdirp": "^0.5.1",
"nets": "^3.2.0", "nets": "^3.2.0",

View file

@ -49,9 +49,25 @@ class ScratchDesktopTelemetry {
} }
projectDidSave (metadata = {}) { 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)); this._telemetryClient.addEvent('project::save', this._buildMetadata(metadata));
} }
projectSaveCanceled () {
this._pendingProjectSave = null;
}
projectWasCreated (metadata = {}) { projectWasCreated (metadata = {}) {
this._telemetryClient.addEvent('project::create', this._buildMetadata(metadata)); this._telemetryClient.addEvent('project::create', this._buildMetadata(metadata));
} }

View file

@ -62,13 +62,22 @@ const createMainWindow = () => {
if (extName) { if (extName) {
const extNameNoDot = extName.replace(/^\./, ''); const extNameNoDot = extName.replace(/^\./, '');
const options = { const options = {
defaultPath: baseName,
filters: [getFilterForExtension(extNameNoDot)] filters: [getFilterForExtension(extNameNoDot)]
}; };
const userChosenPath = dialog.showSaveDialog(window, options); const userChosenPath = dialog.showSaveDialog(window, options);
if (userChosenPath) { if (userChosenPath) {
item.setSavePath(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 { } else {
item.cancel(); item.cancel();
telemetry.projectSaveCanceled();
} }
} }
}); });

View file

@ -1,7 +1,10 @@
import {ipcRenderer, shell} from 'electron'; import {ipcRenderer, shell} from 'electron';
import bindAll from 'lodash.bindall';
import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; 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'; import ElectronStorageHelper from '../common/ElectronStorageHelper';
@ -23,27 +26,69 @@ appTarget.className = styles.app || 'app'; // TODO
document.body.appendChild(appTarget); document.body.appendChild(appTarget);
GUI.setAppElement(appTarget); GUI.setAppElement(appTarget);
const WrappedGui = AppStateHOC(GUI);
const onStorageInit = storageInstance => { const ScratchDesktopHOC = function (WrappedComponent) {
storageInstance.addHelper(new ElectronStorageHelper(storageInstance)); class ScratchDesktopComponent extends React.Component {
// storageInstance.addOfficialScratchWebStores(); // TODO: do we want this? constructor (props) {
}; super(props);
bindAll(this, [
const guiProps = { 'handleProjectTelemetryEvent',
onStorageInit, 'handleSetTitleFromSave',
isScratchDesktop: true, 'handleStorageInit',
projectId: defaultProjectId, 'handleTelemetryModalOptIn',
showTelemetryModal: (typeof ipcRenderer.sendSync('getTelemetryDidOptIn')) !== 'boolean', 'handleTelemetryModalOptOut'
onTelemetryModalOptIn: () => { ]);
ipcRenderer.send('setTelemetryDidOptIn', true); }
}, componentDidMount () {
onTelemetryModalOptOut: () => { ipcRenderer.on('setTitleFromSave', this.handleSetTitleFromSave);
ipcRenderer.send('setTelemetryDidOptIn', false); }
}, componentWillUnmount () {
onProjectTelemetryEvent: (event, metadata) => { ipcRenderer.removeListener('setTitleFromSave', this.handleSetTitleFromSave);
ipcRenderer.send(event, metadata); }
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 (<WrappedComponent
isScratchDesktop
projectId={defaultProjectId}
showTelemetryModal={shouldShowTelemetryModal}
onProjectTelemetryEvent={this.handleProjectTelemetryEvent}
onStorageInit={this.handleStorageInit}
onTelemetryModalOptIn={this.handleTelemetryModalOptIn}
onTelemetryModalOptOut={this.handleTelemetryModalOptOut}
{...this.props}
/>);
}
} }
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(<WrappedGui />, appTarget);