Implement Etcher.OS.Dialog module (#381)

Signed-off-by: Juan Cruz Viotti <jviottidc@gmail.com>
This commit is contained in:
Juan Cruz Viotti 2016-04-29 09:03:20 -04:00
parent d373d32472
commit ad758cf391
5 changed files with 121 additions and 144 deletions

View File

@ -22,8 +22,6 @@
var angular = require('angular');
const _ = require('lodash');
const electron = require('electron');
const dialog = electron.remote.require('./gui/dialog');
const app = angular.module('Etcher', [
require('angular-ui-router'),
@ -51,6 +49,7 @@ const app = angular.module('Etcher', [
require('./os/window-progress/window-progress'),
require('./os/open-external/open-external'),
require('./os/dropzone/dropzone'),
require('./os/dialog/dialog'),
// Utils
require('./utils/if-state/if-state'),
@ -76,7 +75,6 @@ app.config(function($stateProvider, $urlRouterProvider) {
});
app.controller('AppController', function(
$q,
$state,
$scope,
NotifierService,
@ -87,7 +85,8 @@ app.controller('AppController', function(
AnalyticsService,
DriveSelectorService,
OSWindowProgressService,
OSNotificationService
OSNotificationService,
OSDialogService
) {
let self = this;
this.selection = SelectionStateModel;
@ -117,7 +116,7 @@ app.controller('AppController', function(
OSWindowProgressService.set(state.progress);
});
this.scanner.start(2000).on('error', dialog.showError).on('scan', function(drives) {
this.scanner.start(2000).on('error', OSDialogService.showError).on('scan', function(drives) {
// Cover the case where you select a drive, but then eject it.
if (self.selection.hasDrive() && !_.find(drives, self.selection.isCurrentDrive)) {
@ -167,7 +166,7 @@ app.controller('AppController', function(
};
this.openImageSelector = function() {
return $q.when(dialog.selectImage()).then(function(image) {
return OSDialogService.selectImage().then(function(image) {
// Avoid analytics and selection state changes
// if no file was resolved from the dialog.
@ -260,7 +259,7 @@ app.controller('AppController', function(
}
self.writer.resetState();
dialog.showError(error);
OSDialogService.showError(error);
})
.finally(OSWindowProgressService.clear);
};

View File

@ -1,88 +0,0 @@
/*
* Copyright 2016 Resin.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';
const electron = require('electron');
const Bluebird = require('bluebird');
const zipImage = require('resin-zip-image');
const packageJSON = require('../../package.json');
/**
* @summary Open an image selection dialog
* @function
* @public
*
* @description
* Notice that by image, we mean *.img/*.iso/*.zip files.
*
* If the user selects an invalid zip image, an error alert
* is shown, and the promise resolves `undefined`.
*
* @fulfil {String} - selected image
* @returns {Promise};
*
* @example
* dialog.selectImage().then(function(image) {
* console.log('The selected image is', image);
* });
*/
exports.selectImage = function() {
return new Bluebird(function(resolve) {
electron.dialog.showOpenDialog({
properties: [ 'openFile' ],
filters: [
{
name: 'IMG/ISO/ZIP',
extensions: [
'zip',
'img',
'iso'
]
}
]
}, function(files) {
return resolve(files || []);
});
}).get(0).then(function(file) {
if (file && zipImage.isZip(file) && !zipImage.isValidZipImage(file)) {
electron.dialog.showErrorBox(
'Invalid zip image',
`${packageJSON.displayName} can only open Zip archives that contain exactly one image file inside.`
);
return;
}
return file;
});
};
/**
* @summary Show error dialog for an Error instance
* @function
* @public
*
* @param {Error} error - error
*
* @example
* dialog.showError(new Error('Foo Bar'));
*/
exports.showError = function(error) {
error = error || {};
electron.dialog.showErrorBox(error.message || 'An error ocurred!', error.stack || '');
throw error;
};

View File

@ -0,0 +1,31 @@
/*
* Copyright 2016 Resin.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';
/**
* @module Etcher.OS.Dialog
*
* The purpose of this module is to provide an easy way
* to interact with OS dialogs.
*/
const angular = require('angular');
const MODULE_NAME = 'Etcher.OS.Dialog';
const OSDialog = angular.module(MODULE_NAME, []);
OSDialog.service('OSDialogService', require('./services/dialog'));
module.exports = MODULE_NAME;

View File

@ -0,0 +1,84 @@
/*
* Copyright 2016 Resin.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';
const _ = require('lodash');
const electron = require('electron');
const imageStream = require('etcher-image-stream');
module.exports = function($q) {
/**
* @summary Open an image selection dialog
* @function
* @public
*
* @description
* Notice that by image, we mean *.img/*.iso/*.zip/etc files.
*
* @fulfil {String} - selected image
* @returns {Promise};
*
* @example
* OSDialogService.selectImage().then(function(image) {
* console.log('The selected image is', image);
* });
*/
this.selectImage = function() {
return $q(function(resolve) {
electron.remote.dialog.showOpenDialog({
properties: [
'openFile'
],
filters: [
{
name: 'OS Images',
extensions: imageStream.supportedFileTypes
}
]
}, function(files) {
// `_.first` is smart enough to not throw and return `undefined`
// if we pass it an `undefined` value (e.g: when the selection
// dialog was cancelled).
return resolve(_.first(files));
});
});
};
/**
* @summary Show error dialog for an Error instance
* @function
* @public
*
* @param {Error} error - error
*
* @example
* OSDialogService.showError(new Error('Foo Bar'));
*/
this.showError = function(error) {
error = error || {};
electron.remote.dialog.showErrorBox(error.message || 'An error ocurred!', error.stack || '');
// Also throw it so it gets displayed in DevTools
// and its reported by TrackJS.
throw error;
};
};

View File

@ -1,49 +0,0 @@
'use strict';
const m = require('mochainon');
const electron = require('electron');
const dialog = require('../../lib/gui/dialog');
describe('Dialog:', function() {
describe('.selectImage()', function() {
describe('given the user does not select anything', function() {
beforeEach(function() {
this.showOpenDialogStub = m.sinon.stub(electron.dialog, 'showOpenDialog');
this.showOpenDialogStub.yields(undefined);
});
afterEach(function() {
this.showOpenDialogStub.restore();
});
it('should eventually be undefined', function() {
const promise = dialog.selectImage();
m.chai.expect(promise).to.eventually.be.undefined;
});
});
describe('given the users performs a selection', function() {
beforeEach(function() {
this.showOpenDialogStub = m.sinon.stub(electron.dialog, 'showOpenDialog');
this.showOpenDialogStub.yields([ 'foo/bar' ]);
});
afterEach(function() {
this.showOpenDialogStub.restore();
});
it('should eventually equal the file', function() {
const promise = dialog.selectImage();
m.chai.expect(promise).to.eventually.equal('foo/bar');
});
});
});
});