Refactor image-selection

Change-type: patch
Changelog-entry: Use React instead of Angular for image selection
Signed-off-by: Lucian <lucian.buzzo@gmail.com>
This commit is contained in:
Lucian 2019-12-02 15:53:47 +00:00 committed by Alexis Svinartchouk
parent 5e568d7dd8
commit 5cd3c5fcc0
13 changed files with 392 additions and 598 deletions

View File

@ -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')

View File

@ -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 (
<ThemedProvider>
<StepNameButton
plain
onClick={props.showSelectedImageDetails}
tooltip={props.imageBasename}
>
{/* eslint-disable no-magic-numbers */}
{ middleEllipsis(props.imageName || props.imageBasename, 20) }
</StepNameButton>
{ !props.flashing &&
<ChangeButton
plain
mb={14}
onClick={props.reselectImage}
>
Change
</ChangeButton>
}
<DetailsText>
{shared.bytesToClosestUnit(props.imageSize)}
</DetailsText>
<Dropzone multiple={false} noClick onDrop={this.handleOnDrop}>
{({ getRootProps, getInputProps }) => (
<div className="box text-center relative" {...getRootProps()}>
<input {...getInputProps()} />
<div className="center-block">
<SVGIcon contents={selectionState.getImageLogo()} paths={[ '../../assets/image.svg' ]} />
</div>
<div className="space-vertical-large">
{hasImage ? (
<React.Fragment>
<StepNameButton
plain
onClick={showSelectedImageDetails}
tooltip={imageBasename}
>
{/* eslint-disable no-magic-numbers */}
{ middleEllipsis(imageName || imageBasename, 20) }
</StepNameButton>
{ !flashing &&
<ChangeButton
plain
mb={14}
onClick={this.reselectImage}
>
Change
</ChangeButton>
}
<DetailsText>
{shared.bytesToClosestUnit(imageSize)}
</DetailsText>
</React.Fragment>
) : (
<StepSelection>
<StepButton
onClick={this.openImageSelector}
>
Select image
</StepButton>
<Footer>
{ mainSupportedExtensions.join(', ') }, and{' '}
<Underline
tooltip={ extraSupportedExtensions.join(', ') }
>
many more
</Underline>
</Footer>
</StepSelection>
)}
</div>
</div>
)}
</Dropzone>
</ThemedProvider>
)
}
return (
<ThemedProvider>
<StepSelection>
<StepButton
onClick={props.openImageSelector}
>
Select image
</StepButton>
<Footer>
{ props.mainSupportedExtensions.join(', ') }, and{' '}
<Underline
tooltip={ props.extraSupportedExtensions.join(', ') }
>
many more
</Underline>
</Footer>
</StepSelection>
</ThemedProvider>
)
}
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

View File

@ -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

View File

@ -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
* <div os-dropzone="doSomething($file)">Drag a file here</div>
*/
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
}
}
}
}

View File

@ -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

View File

@ -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())
}
}

View File

@ -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

View File

@ -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'))

View File

@ -1,28 +1,10 @@
<div class="page-main row around-xs">
<div class="col-xs" ng-controller="ImageSelectionController as image">
<div class="box text-center relative" os-dropzone="image.selectImageByPath($file)">
<div class="center-block">
<svg-icon contents="[ main.selection.getImageLogo() ]" paths="[ '../../assets/image.svg' ]"></svg-icon>
</div>
<div class="space-vertical-large">
<image-selector
has-image="main.selection.hasImage()"
open-image-selector="image.openImageSelector"
main-supported-extensions="image.mainSupportedExtensions"
extra-supported-extensions="image.extraSupportedExtensions"
show-selected-image-details="main.showSelectedImageDetails"
image-name="main.selection.getImageName()"
image-basename="image.getImageBasename()"
reselect-image="image.reselectImage"
flashing="main.state.isFlashing()"
image-size="main.selection.getImageSize()"
>
</image-selector>
</div>
</div>
<div class="col-xs">
<image-selector
flashing="main.state.isFlashing()"
showSelectedImageDetails={main.showSelectedImageDetails}
>
</image-selector>
</div>
<div class="col-xs" ng-controller="DriveSelectionController as drive">

37
npm-shrinkwrap.json generated
View File

@ -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 @@
}
}
}
}
}

View File

@ -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",

View File

@ -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('<div os-dropzone="onDropZone($file)">Drop a file here</div>')($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('<div os-dropzone="onDropZone()">Drop a file here</div>')($rootScope)
$rootScope.$digest()
element[0].ondrop({
preventDefault: angular.noop,
dataTransfer: {
files: [
{
path: '/foo/bar'
}
]
}
})
$rootScope.$digest()
$timeout.flush()
})
})
})

View File

@ -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