From 5cd3c5fcc086d619a35ae6a4930412a60569e20c Mon Sep 17 00:00:00 2001 From: Lucian Date: Mon, 2 Dec 2019 15:53:47 +0000 Subject: [PATCH] Refactor image-selection Change-type: patch Changelog-entry: Use React instead of Angular for image selection Signed-off-by: Lucian --- lib/gui/app/app.js | 1 - .../image-selector/image-selector.jsx | 371 +++++++++++++++--- .../app/components/image-selector/index.js | 14 +- .../app/os/dropzone/directives/dropzone.js | 74 ---- lib/gui/app/os/dropzone/dropzone.js | 28 -- .../pages/main/controllers/image-selection.js | 265 ------------- lib/gui/app/pages/main/controllers/main.js | 10 +- lib/gui/app/pages/main/main.js | 2 - .../app/pages/main/templates/main.tpl.html | 30 +- npm-shrinkwrap.json | 37 +- package.json | 1 + tests/gui/os/dropzone.spec.js | 86 ---- tests/gui/pages/main.spec.js | 71 +--- 13 files changed, 392 insertions(+), 598 deletions(-) delete mode 100644 lib/gui/app/os/dropzone/directives/dropzone.js delete mode 100644 lib/gui/app/os/dropzone/dropzone.js delete mode 100644 lib/gui/app/pages/main/controllers/image-selection.js delete mode 100644 tests/gui/os/dropzone.spec.js diff --git a/lib/gui/app/app.js b/lib/gui/app/app.js index c3ee1ddc..b03f6a18 100644 --- a/lib/gui/app/app.js +++ b/lib/gui/app/app.js @@ -98,7 +98,6 @@ const app = angular.module('Etcher', [ // OS require('./os/open-external/open-external'), - require('./os/dropzone/dropzone'), // Utils require('./utils/manifest-bind/manifest-bind') diff --git a/lib/gui/app/components/image-selector/image-selector.jsx b/lib/gui/app/components/image-selector/image-selector.jsx index d19692cf..aac1ee6c 100644 --- a/lib/gui/app/components/image-selector/image-selector.jsx +++ b/lib/gui/app/components/image-selector/image-selector.jsx @@ -16,13 +16,24 @@ 'use strict' -/* eslint-disable no-unused-vars */ -const React = require('react') +const Bluebird = require('bluebird') +const sdk = require('etcher-sdk') +const _ = require('lodash') +const path = require('path') const propTypes = require('prop-types') - -const middleEllipsis = require('./../../utils/middle-ellipsis') - -const shared = require('./../../../../shared/units') +const React = require('react') +const Dropzone = require('react-dropzone').default +const errors = require('../../../../shared/errors') +const messages = require('../../../../shared/messages') +const supportedFormats = require('../../../../shared/supported-formats') +const shared = require('../../../../shared/units') +const selectionState = require('../../models/selection-state') +const settings = require('../../models/settings') +const store = require('../../models/store') +const analytics = require('../../modules/analytics') +const exceptionReporter = require('../../modules/exception-reporter') +const osDialog = require('../../os/dialog') +const { replaceWindowsNetworkDriveLetter } = require('../../os/windows-network-drives') const { StepButton, StepNameButton, @@ -32,67 +43,309 @@ const { DetailsText, ChangeButton, ThemedProvider -} = require('./../../styled-components') +} = require('../../styled-components') +const middleEllipsis = require('../../utils/middle-ellipsis') +const SVGIcon = require('../svg-icon/svg-icon.jsx') + +/** + * @summary Main supported extensions + * @constant + * @type {String[]} + * @public + */ +const mainSupportedExtensions = _.intersection([ + 'img', + 'iso', + 'zip' +], supportedFormats.getAllExtensions()) + +/** + * @summary Extra supported extensions + * @constant + * @type {String[]} + * @public + */ +const extraSupportedExtensions = _.difference( + supportedFormats.getAllExtensions(), + mainSupportedExtensions +).sort() + +const getState = () => { + return { + hasImage: selectionState.hasImage(), + imageName: selectionState.getImageName(), + imageSize: selectionState.getImageSize() + } +} + +class ImageSelector extends React.Component { + constructor (props) { + super(props) + + this.state = getState() + + this.openImageSelector = this.openImageSelector.bind(this) + this.reselectImage = this.reselectImage.bind(this) + this.handleOnDrop = this.handleOnDrop.bind(this) + } + + componentDidMount () { + this.unsubscribe = store.observe(() => { + this.setState(getState()) + }) + } + + componentWillUnmount () { + this.unsubscribe() + } + + reselectImage () { + analytics.logEvent('Reselect image', { + previousImage: selectionState.getImage(), + applicationSessionUuid: store.getState().toJS().applicationSessionUuid, + flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid + }) + + this.openImageSelector() + } + + selectImage (image) { + const { + WarningModalService + } = this.props + + if (!supportedFormats.isSupportedImage(image.path)) { + const invalidImageError = errors.createUserError({ + title: 'Invalid image', + description: messages.error.invalidImage(image) + }) + + osDialog.showError(invalidImageError) + analytics.logEvent('Invalid image', _.merge({ + applicationSessionUuid: store.getState().toJS().applicationSessionUuid, + flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid + }, image)) + return + } + + Bluebird.try(() => { + let message = null + + if (supportedFormats.looksLikeWindowsImage(image.path)) { + analytics.logEvent('Possibly Windows image', { + image, + applicationSessionUuid: store.getState().toJS().applicationSessionUuid, + flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid + }) + message = messages.warning.looksLikeWindowsImage() + } else if (!image.hasMBR) { + analytics.logEvent('Missing partition table', { + image, + applicationSessionUuid: store.getState().toJS().applicationSessionUuid, + flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid + }) + message = messages.warning.missingPartitionTable() + } + + if (message) { + // TODO: `Continue` should be on a red background (dangerous action) instead of `Change`. + // We want `X` to act as `Continue`, that's why `Continue` is the `rejectionLabel` + return WarningModalService.display({ + confirmationLabel: 'Change', + rejectionLabel: 'Continue', + description: message + }) + } + + return false + }).then((shouldChange) => { + if (shouldChange) { + return this.reselectImage() + } + + selectionState.selectImage(image) + + // An easy way so we can quickly identify if we're making use of + // certain features without printing pages of text to DevTools. + image.logo = Boolean(image.logo) + image.blockMap = Boolean(image.blockMap) + + return analytics.logEvent('Select image', { + image, + applicationSessionUuid: store.getState().toJS().applicationSessionUuid, + flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid + }) + }).catch(exceptionReporter.report) + } + + async selectImageByPath (imagePath) { + try { + // eslint-disable-next-line no-param-reassign + imagePath = await replaceWindowsNetworkDriveLetter(imagePath) + } catch (error) { + analytics.logException(error) + } + if (!supportedFormats.isSupportedImage(imagePath)) { + const invalidImageError = errors.createUserError({ + title: 'Invalid image', + description: messages.error.invalidImage(imagePath) + }) + + osDialog.showError(invalidImageError) + analytics.logEvent('Invalid image', { path: imagePath }) + return + } + + const source = new sdk.sourceDestination.File(imagePath, sdk.sourceDestination.File.OpenFlags.Read) + try { + const innerSource = await source.getInnerSource() + const metadata = await innerSource.getMetadata() + const partitionTable = await innerSource.getPartitionTable() + if (partitionTable) { + metadata.hasMBR = true + metadata.partitions = partitionTable.partitions + } + metadata.path = imagePath + // eslint-disable-next-line no-magic-numbers + metadata.extension = path.extname(imagePath).slice(1) + this.selectImage(metadata) + } catch (error) { + const imageError = errors.createUserError({ + title: 'Error opening image', + description: messages.error.openImage(path.basename(imagePath), error.message) + }) + osDialog.showError(imageError) + analytics.logException(error) + } finally { + try { + await source.close() + } catch (error) { + // Noop + } + } + } + + /** + * @summary Open image selector + * @function + * @public + * + * @example + * ImageSelectionController.openImageSelector(); + */ + openImageSelector () { + analytics.logEvent('Open image selector', { + applicationSessionUuid: store.getState().toJS().applicationSessionUuid, + flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid + }) + + if (settings.get('experimentalFilePicker')) { + const { + FileSelectorService + } = this.props + + FileSelectorService.open() + } else { + osDialog.selectImage().then((imagePath) => { + // Avoid analytics and selection state changes + // if no file was resolved from the dialog. + if (!imagePath) { + analytics.logEvent('Image selector closed', { + applicationSessionUuid: store.getState().toJS().applicationSessionUuid, + flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid + }) + return + } + + this.selectImageByPath(imagePath) + }).catch(exceptionReporter.report) + } + } + + handleOnDrop (acceptedFiles) { + const [ file ] = acceptedFiles + + if (file) { + this.selectImageByPath(file.path) + } + } + + // TODO add a visual change when dragging a file over the selector + render () { + const { + flashing, + showSelectedImageDetails + } = this.props + + const hasImage = selectionState.hasImage() + + const imageBasename = hasImage ? path.basename(selectionState.getImagePath()) : '' + const imageName = selectionState.getImageName() + const imageSize = selectionState.getImageSize() -const SelectImageButton = (props) => { - if (props.hasImage) { return ( - - {/* eslint-disable no-magic-numbers */} - { middleEllipsis(props.imageName || props.imageBasename, 20) } - - { !props.flashing && - - Change - - } - - {shared.bytesToClosestUnit(props.imageSize)} - + + {({ getRootProps, getInputProps }) => ( +
+ +
+ +
+ +
+ {hasImage ? ( + + + {/* eslint-disable no-magic-numbers */} + { middleEllipsis(imageName || imageBasename, 20) } + + { !flashing && + + Change + + } + + {shared.bytesToClosestUnit(imageSize)} + + + ) : ( + + + Select image + +
+ { mainSupportedExtensions.join(', ') }, and{' '} + + many more + +
+
+ )} +
+
+ )} +
) } - return ( - - - - Select image - -
- { props.mainSupportedExtensions.join(', ') }, and{' '} - - many more - -
-
-
- ) } -SelectImageButton.propTypes = { - openImageSelector: propTypes.func, - mainSupportedExtensions: propTypes.array, - extraSupportedExtensions: propTypes.array, - hasImage: propTypes.bool, - showSelectedImageDetails: propTypes.func, - imageName: propTypes.string, - imageBasename: propTypes.string, - reselectImage: propTypes.func, +ImageSelector.propTypes = { flashing: propTypes.bool, - imageSize: propTypes.number + showSelectedImageDetails: propTypes.func } -module.exports = SelectImageButton +module.exports = ImageSelector diff --git a/lib/gui/app/components/image-selector/index.js b/lib/gui/app/components/image-selector/index.js index e8e3f25b..699085aa 100644 --- a/lib/gui/app/components/image-selector/index.js +++ b/lib/gui/app/components/image-selector/index.js @@ -16,6 +16,8 @@ 'use strict' +/* eslint-disable jsdoc/require-example */ + /** * @module Etcher.Components.ImageSelector */ @@ -24,11 +26,15 @@ const angular = require('angular') const { react2angular } = require('react2angular') const MODULE_NAME = 'Etcher.Components.ImageSelector' -const SelectImageButton = angular.module(MODULE_NAME, []) +const ImageSelector = angular.module(MODULE_NAME, []) -SelectImageButton.component( +ImageSelector.component( 'imageSelector', - react2angular(require('./image-selector.jsx')) + react2angular(require('./image-selector.jsx')), + [], + [ + 'FileSelectorService', + 'WarningModalService' + ] ) - module.exports = MODULE_NAME diff --git a/lib/gui/app/os/dropzone/directives/dropzone.js b/lib/gui/app/os/dropzone/directives/dropzone.js deleted file mode 100644 index 4799089b..00000000 --- a/lib/gui/app/os/dropzone/directives/dropzone.js +++ /dev/null @@ -1,74 +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 _ = require('lodash') - -/** - * @summary Dropzone directive - * @function - * @public - * - * @description - * This directive provides an attribute to detect a file - * being dropped into the element. - * - * @param {Object} $timeout - Angular's timeout wrapper - * @returns {Object} directive - * - * @example - *
Drag a file here
- */ -module.exports = ($timeout) => { - return { - restrict: 'A', - scope: { - osDropzone: '&' - }, - link: (scope, $element) => { - const domElement = _.first($element) - - // See https://github.com/electron/electron/blob/master/docs/api/file-object.md - - // We're not interested in these events - domElement.ondragover = _.constant(false) - domElement.ondragleave = _.constant(false) - domElement.ondragend = _.constant(false) - - domElement.ondrop = (event) => { - event.preventDefault() - - if (event.dataTransfer.files.length) { - const filename = _.first(event.dataTransfer.files).path - - // Safely bring this to the world of Angular - $timeout(() => { - scope.osDropzone({ - - // Pass the filename as a named - // parameter called `$file` - $file: filename - - }) - }) - } - - return false - } - } - } -} diff --git a/lib/gui/app/os/dropzone/dropzone.js b/lib/gui/app/os/dropzone/dropzone.js deleted file mode 100644 index 17c7e98f..00000000 --- a/lib/gui/app/os/dropzone/dropzone.js +++ /dev/null @@ -1,28 +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' - -/** - * @module Etcher.OS.Dropzone - */ - -const angular = require('angular') -const MODULE_NAME = 'Etcher.OS.Dropzone' -const OSDropzone = angular.module(MODULE_NAME, []) -OSDropzone.directive('osDropzone', require('./directives/dropzone')) - -module.exports = MODULE_NAME diff --git a/lib/gui/app/pages/main/controllers/image-selection.js b/lib/gui/app/pages/main/controllers/image-selection.js deleted file mode 100644 index 3060c713..00000000 --- a/lib/gui/app/pages/main/controllers/image-selection.js +++ /dev/null @@ -1,265 +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 _ = require('lodash') -const Bluebird = require('bluebird') -const path = require('path') -const sdk = require('etcher-sdk') - -const store = require('../../../models/store') -const messages = require('../../../../../shared/messages') -const errors = require('../../../../../shared/errors') -const supportedFormats = require('../../../../../shared/supported-formats') -const analytics = require('../../../modules/analytics') -const settings = require('../../../models/settings') -const selectionState = require('../../../models/selection-state') -const osDialog = require('../../../os/dialog') -const { replaceWindowsNetworkDriveLetter } = require('../../../os/windows-network-drives') -const exceptionReporter = require('../../../modules/exception-reporter') - -module.exports = function ( - $timeout, - FileSelectorService, - WarningModalService -) { - /** - * @summary Main supported extensions - * @constant - * @type {String[]} - * @public - */ - this.mainSupportedExtensions = _.intersection([ - 'img', - 'iso', - 'zip' - ], supportedFormats.getAllExtensions()) - - /** - * @summary Extra supported extensions - * @constant - * @type {String[]} - * @public - */ - this.extraSupportedExtensions = _.difference( - supportedFormats.getAllExtensions(), - this.mainSupportedExtensions - ).sort() - - /** - * @summary Select image - * @function - * @public - * - * @param {Object} image - image - * - * @example - * osDialogService.selectImage() - * .then(ImageSelectionController.selectImage); - */ - this.selectImage = (image) => { - if (!supportedFormats.isSupportedImage(image.path)) { - const invalidImageError = errors.createUserError({ - title: 'Invalid image', - description: messages.error.invalidImage(image) - }) - - osDialog.showError(invalidImageError) - analytics.logEvent('Invalid image', _.merge({ - applicationSessionUuid: store.getState().toJS().applicationSessionUuid, - flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid - }, image)) - return - } - - Bluebird.try(() => { - let message = null - - if (supportedFormats.looksLikeWindowsImage(image.path)) { - analytics.logEvent('Possibly Windows image', { - image, - applicationSessionUuid: store.getState().toJS().applicationSessionUuid, - flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid - }) - message = messages.warning.looksLikeWindowsImage() - } else if (!image.hasMBR) { - analytics.logEvent('Missing partition table', { - image, - applicationSessionUuid: store.getState().toJS().applicationSessionUuid, - flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid - }) - message = messages.warning.missingPartitionTable() - } - - if (message) { - // TODO: `Continue` should be on a red background (dangerous action) instead of `Change`. - // We want `X` to act as `Continue`, that's why `Continue` is the `rejectionLabel` - return WarningModalService.display({ - confirmationLabel: 'Change', - rejectionLabel: 'Continue', - description: message - }) - } - - return false - }).then((shouldChange) => { - if (shouldChange) { - return this.reselectImage() - } - - selectionState.selectImage(image) - - // An easy way so we can quickly identify if we're making use of - // certain features without printing pages of text to DevTools. - image.logo = Boolean(image.logo) - image.blockMap = Boolean(image.blockMap) - - return analytics.logEvent('Select image', { - image, - applicationSessionUuid: store.getState().toJS().applicationSessionUuid, - flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid - }) - }).catch(exceptionReporter.report) - } - - /** - * @summary Select an image by path - * @function - * @public - * - * @param {String} imagePath - image path - * - * @example - * ImageSelectionController.selectImageByPath('path/to/image.img'); - */ - this.selectImageByPath = async (imagePath) => { - try { - // eslint-disable-next-line no-param-reassign - imagePath = await replaceWindowsNetworkDriveLetter(imagePath) - } catch (error) { - analytics.logException(error) - } - if (!supportedFormats.isSupportedImage(imagePath)) { - const invalidImageError = errors.createUserError({ - title: 'Invalid image', - description: messages.error.invalidImage(imagePath) - }) - - osDialog.showError(invalidImageError) - analytics.logEvent('Invalid image', { path: imagePath }) - return - } - - const source = new sdk.sourceDestination.File(imagePath, sdk.sourceDestination.File.OpenFlags.Read) - try { - const innerSource = await source.getInnerSource() - const metadata = await innerSource.getMetadata() - const partitionTable = await innerSource.getPartitionTable() - if (partitionTable) { - metadata.hasMBR = true - metadata.partitions = partitionTable.partitions - } - metadata.path = imagePath - // eslint-disable-next-line no-magic-numbers - metadata.extension = path.extname(imagePath).slice(1) - this.selectImage(metadata) - $timeout() - } catch (error) { - const imageError = errors.createUserError({ - title: 'Error opening image', - description: messages.error.openImage(path.basename(imagePath), error.message) - }) - osDialog.showError(imageError) - analytics.logException(error) - } finally { - try { - await source.close() - } catch (error) { - // Noop - } - } - } - - /** - * @summary Open image selector - * @function - * @public - * - * @example - * ImageSelectionController.openImageSelector(); - */ - this.openImageSelector = () => { - analytics.logEvent('Open image selector', { - applicationSessionUuid: store.getState().toJS().applicationSessionUuid, - flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid - }) - - if (settings.get('experimentalFilePicker')) { - FileSelectorService.open() - } else { - osDialog.selectImage().then((imagePath) => { - // Avoid analytics and selection state changes - // if no file was resolved from the dialog. - if (!imagePath) { - analytics.logEvent('Image selector closed', { - applicationSessionUuid: store.getState().toJS().applicationSessionUuid, - flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid - }) - return - } - - this.selectImageByPath(imagePath) - }).catch(exceptionReporter.report) - } - } - - /** - * @summary Reselect image - * @function - * @public - * - * @example - * ImageSelectionController.reselectImage(); - */ - this.reselectImage = () => { - analytics.logEvent('Reselect image', { - previousImage: selectionState.getImage(), - applicationSessionUuid: store.getState().toJS().applicationSessionUuid, - flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid - }) - - this.openImageSelector() - } - - /** - * @summary Get the basename of the selected image - * @function - * @public - * - * @returns {String} basename of the selected image - * - * @example - * const imageBasename = ImageSelectionController.getImageBasename(); - */ - this.getImageBasename = () => { - if (!selectionState.hasImage()) { - return '' - } - - return path.basename(selectionState.getImagePath()) - } -} diff --git a/lib/gui/app/pages/main/controllers/main.js b/lib/gui/app/pages/main/controllers/main.js index 60d52d0a..c995e7bc 100644 --- a/lib/gui/app/pages/main/controllers/main.js +++ b/lib/gui/app/pages/main/controllers/main.js @@ -31,7 +31,8 @@ const prettyBytes = require('pretty-bytes') module.exports = function ( TooltipModalService, OSOpenExternalService, - $filter + $filter, + $scope ) { // Expose several modules to the template for convenience this.selection = selectionState @@ -43,6 +44,13 @@ module.exports = function ( this.progressMessage = messages.progress this.isWebviewShowing = Boolean(store.getState().toJS().isWebviewShowing) + // Trigger an update if the store changes + store.observe(() => { + if (!$scope.$$phase) { + $scope.$apply() + } + }) + /** * @summary Determine if the drive step should be disabled * @function diff --git a/lib/gui/app/pages/main/main.js b/lib/gui/app/pages/main/main.js index 7838ba04..71f54dc9 100644 --- a/lib/gui/app/pages/main/main.js +++ b/lib/gui/app/pages/main/main.js @@ -47,14 +47,12 @@ const MainPage = angular.module(MODULE_NAME, [ require('../../components/drive-selector'), require('../../os/open-external/open-external'), - require('../../os/dropzone/dropzone'), require('../../utils/byte-size/byte-size'), require('../../utils/middle-ellipsis/filter') ]) MainPage.controller('MainController', require('./controllers/main')) -MainPage.controller('ImageSelectionController', require('./controllers/image-selection')) MainPage.controller('DriveSelectionController', require('./controllers/drive-selection')) MainPage.controller('FlashController', require('./controllers/flash')) diff --git a/lib/gui/app/pages/main/templates/main.tpl.html b/lib/gui/app/pages/main/templates/main.tpl.html index fdf1b71d..d1c76a1c 100644 --- a/lib/gui/app/pages/main/templates/main.tpl.html +++ b/lib/gui/app/pages/main/templates/main.tpl.html @@ -1,28 +1,10 @@
-
-
- -
- -
- -
- - -
- -
+
+ +
diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 0572e1ae..addd9d5f 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -2135,6 +2135,11 @@ "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==" }, + "attr-accept": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.0.0.tgz", + "integrity": "sha512-I9SDP4Wvh2ItYYoafEg8hFpsBe96pfQ+eabceShXt3sw2fbIP96+Aoj9zZE0vkZNAkXXzHJATVRuWz+h9FxJxQ==" + }, "aws-sign2": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", @@ -6400,6 +6405,14 @@ "object-assign": "^4.0.1" } }, + "file-selector": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.1.12.tgz", + "integrity": "sha512-Kx7RTzxyQipHuiqyZGf+Nz4vY9R1XGxuQl/hLoJwq+J4avk/9wxxgZyHKtbyIPJmbD4A66DWGYfyykWNpcYutQ==", + "requires": { + "tslib": "^1.9.0" + } + }, "file-type": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/file-type/-/file-type-8.1.0.tgz", @@ -10762,6 +10775,28 @@ "scheduler": "^0.17.0" } }, + "react-dropzone": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-10.2.1.tgz", + "integrity": "sha512-Me5nOu8hK9/Xyg5easpdfJ6SajwUquqYR/2YTdMotsCUgJ1pHIIwNsv0n+qcIno0tWR2V2rVQtj2r/hXYs2TnQ==", + "requires": { + "attr-accept": "^2.0.0", + "file-selector": "^0.1.12", + "prop-types": "^15.7.2" + }, + "dependencies": { + "prop-types": { + "version": "15.7.2", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", + "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", + "requires": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.8.1" + } + } + } + }, "react-google-recaptcha": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/react-google-recaptcha/-/react-google-recaptcha-2.0.1.tgz", @@ -14834,4 +14869,4 @@ } } } -} +} \ No newline at end of file diff --git a/package.json b/package.json index 9d1e9d49..fa033e80 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ "prop-types": "^15.5.9", "react": "^16.8.5", "react-dom": "^16.8.5", + "react-dropzone": "^10.2.1", "react2angular": "^4.0.2", "redux": "^3.5.2", "rendition": "^11.24.0", diff --git a/tests/gui/os/dropzone.spec.js b/tests/gui/os/dropzone.spec.js deleted file mode 100644 index 06871ef3..00000000 --- a/tests/gui/os/dropzone.spec.js +++ /dev/null @@ -1,86 +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 m = require('mochainon') -const angular = require('angular') - -describe('Browser: OSDropzone', function () { - beforeEach(angular.mock.module( - require('../../../lib/gui/app/os/dropzone/dropzone') - )) - - describe('osDropzone', function () { - let $compile - let $rootScope - let $timeout - - beforeEach(angular.mock.inject(function (_$compile_, _$rootScope_, _$timeout_) { - $compile = _$compile_ - $rootScope = _$rootScope_ - $timeout = _$timeout_ - })) - - it('should pass the file back to the callback as $file', function (done) { - $rootScope.onDropZone = function (file) { - m.chai.expect(file).to.deep.equal('/foo/bar') - done() - } - - const element = $compile('
Drop a file here
')($rootScope) - $rootScope.$digest() - - element[0].ondrop({ - preventDefault: angular.noop, - dataTransfer: { - files: [ - { - path: '/foo/bar' - } - ] - } - }) - - $rootScope.$digest() - $timeout.flush() - }) - - it('should pass undefined to the callback if not passing $file', function (done) { - $rootScope.onDropZone = function (file) { - m.chai.expect(file).to.be.undefined - done() - } - - const element = $compile('
Drop a file here
')($rootScope) - $rootScope.$digest() - - element[0].ondrop({ - preventDefault: angular.noop, - dataTransfer: { - files: [ - { - path: '/foo/bar' - } - ] - } - }) - - $rootScope.$digest() - $timeout.flush() - }) - }) -}) diff --git a/tests/gui/pages/main.spec.js b/tests/gui/pages/main.spec.js index 3e51f06d..415d7b6e 100644 --- a/tests/gui/pages/main.spec.js +++ b/tests/gui/pages/main.spec.js @@ -19,8 +19,6 @@ const m = require('mochainon') const _ = require('lodash') const fs = require('fs') -const path = require('path') -const supportedFormats = require('../../../lib/shared/supported-formats') const angular = require('angular') const flashState = require('../../../lib/gui/app/models/flash-state') const availableDrives = require('../../../lib/gui/app/models/available-drives') @@ -53,7 +51,9 @@ describe('Browser: MainPage', function () { describe('.shouldDriveStepBeDisabled()', function () { it('should return true if there is no drive', function () { const controller = $controller('MainController', { - $scope: {} + $scope: { + $apply: _.noop + } }) selectionState.clear() @@ -63,7 +63,9 @@ describe('Browser: MainPage', function () { it('should return false if there is a drive', function () { const controller = $controller('MainController', { - $scope: {} + $scope: { + $apply: _.noop + } }) selectionState.selectImage({ @@ -80,7 +82,9 @@ describe('Browser: MainPage', function () { describe('.shouldFlashStepBeDisabled()', function () { it('should return true if there is no selected drive nor image', function () { const controller = $controller('MainController', { - $scope: {} + $scope: { + $apply: _.noop + } }) selectionState.clear() @@ -90,7 +94,9 @@ describe('Browser: MainPage', function () { it('should return true if there is a selected image but no drive', function () { const controller = $controller('MainController', { - $scope: {} + $scope: { + $apply: _.noop + } }) selectionState.clear() @@ -106,7 +112,9 @@ describe('Browser: MainPage', function () { it('should return true if there is a selected drive but no image', function () { const controller = $controller('MainController', { - $scope: {} + $scope: { + $apply: _.noop + } }) availableDrives.setDrives([ @@ -127,7 +135,9 @@ describe('Browser: MainPage', function () { it('should return false if there is a selected drive and a selected image', function () { const controller = $controller('MainController', { - $scope: {} + $scope: { + $apply: _.noop + } }) availableDrives.setDrives([ @@ -155,51 +165,6 @@ describe('Browser: MainPage', function () { }) }) - describe('ImageSelectionController', function () { - let $controller - - beforeEach(angular.mock.inject(function (_$controller_) { - $controller = _$controller_ - })) - - it('should contain all available extensions in mainSupportedExtensions and extraSupportedExtensions', function () { - const $scope = {} - const controller = $controller('ImageSelectionController', { - $scope - }) - - const extensions = controller.mainSupportedExtensions.concat(controller.extraSupportedExtensions) - m.chai.expect(_.sortBy(extensions)).to.deep.equal(_.sortBy(supportedFormats.getAllExtensions())) - }) - - describe('.getImageBasename()', function () { - it('should return the basename of the selected image', function () { - const controller = $controller('ImageSelectionController', { - $scope: {} - }) - - selectionState.selectImage({ - path: path.join(__dirname, 'foo', 'bar.img'), - extension: 'img', - size: 999999999, - isSizeEstimated: false - }) - - m.chai.expect(controller.getImageBasename()).to.equal('bar.img') - selectionState.deselectImage() - }) - - it('should return an empty string if no selected image', function () { - const controller = $controller('ImageSelectionController', { - $scope: {} - }) - - selectionState.deselectImage() - m.chai.expect(controller.getImageBasename()).to.equal('') - }) - }) - }) - describe('FlashController', function () { let $controller