mirror of
https://github.com/balena-io/etcher.git
synced 2025-07-25 12:16:37 +00:00
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:
parent
5e568d7dd8
commit
5cd3c5fcc0
@ -98,7 +98,6 @@ const app = angular.module('Etcher', [
|
|||||||
|
|
||||||
// OS
|
// OS
|
||||||
require('./os/open-external/open-external'),
|
require('./os/open-external/open-external'),
|
||||||
require('./os/dropzone/dropzone'),
|
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
require('./utils/manifest-bind/manifest-bind')
|
require('./utils/manifest-bind/manifest-bind')
|
||||||
|
@ -16,13 +16,24 @@
|
|||||||
|
|
||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
/* eslint-disable no-unused-vars */
|
const Bluebird = require('bluebird')
|
||||||
const React = require('react')
|
const sdk = require('etcher-sdk')
|
||||||
|
const _ = require('lodash')
|
||||||
|
const path = require('path')
|
||||||
const propTypes = require('prop-types')
|
const propTypes = require('prop-types')
|
||||||
|
const React = require('react')
|
||||||
const middleEllipsis = require('./../../utils/middle-ellipsis')
|
const Dropzone = require('react-dropzone').default
|
||||||
|
const errors = require('../../../../shared/errors')
|
||||||
const shared = require('./../../../../shared/units')
|
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 {
|
const {
|
||||||
StepButton,
|
StepButton,
|
||||||
StepNameButton,
|
StepNameButton,
|
||||||
@ -32,67 +43,309 @@ const {
|
|||||||
DetailsText,
|
DetailsText,
|
||||||
ChangeButton,
|
ChangeButton,
|
||||||
ThemedProvider
|
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 (
|
return (
|
||||||
<ThemedProvider>
|
<ThemedProvider>
|
||||||
<StepNameButton
|
<Dropzone multiple={false} noClick onDrop={this.handleOnDrop}>
|
||||||
plain
|
{({ getRootProps, getInputProps }) => (
|
||||||
onClick={props.showSelectedImageDetails}
|
<div className="box text-center relative" {...getRootProps()}>
|
||||||
tooltip={props.imageBasename}
|
<input {...getInputProps()} />
|
||||||
>
|
<div className="center-block">
|
||||||
{/* eslint-disable no-magic-numbers */}
|
<SVGIcon contents={selectionState.getImageLogo()} paths={[ '../../assets/image.svg' ]} />
|
||||||
{ middleEllipsis(props.imageName || props.imageBasename, 20) }
|
</div>
|
||||||
</StepNameButton>
|
|
||||||
{ !props.flashing &&
|
<div className="space-vertical-large">
|
||||||
<ChangeButton
|
{hasImage ? (
|
||||||
plain
|
<React.Fragment>
|
||||||
mb={14}
|
<StepNameButton
|
||||||
onClick={props.reselectImage}
|
plain
|
||||||
>
|
onClick={showSelectedImageDetails}
|
||||||
Change
|
tooltip={imageBasename}
|
||||||
</ChangeButton>
|
>
|
||||||
}
|
{/* eslint-disable no-magic-numbers */}
|
||||||
<DetailsText>
|
{ middleEllipsis(imageName || imageBasename, 20) }
|
||||||
{shared.bytesToClosestUnit(props.imageSize)}
|
</StepNameButton>
|
||||||
</DetailsText>
|
{ !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>
|
</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 = {
|
ImageSelector.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,
|
|
||||||
flashing: propTypes.bool,
|
flashing: propTypes.bool,
|
||||||
imageSize: propTypes.number
|
showSelectedImageDetails: propTypes.func
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = SelectImageButton
|
module.exports = ImageSelector
|
||||||
|
@ -16,6 +16,8 @@
|
|||||||
|
|
||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
|
/* eslint-disable jsdoc/require-example */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @module Etcher.Components.ImageSelector
|
* @module Etcher.Components.ImageSelector
|
||||||
*/
|
*/
|
||||||
@ -24,11 +26,15 @@ const angular = require('angular')
|
|||||||
const { react2angular } = require('react2angular')
|
const { react2angular } = require('react2angular')
|
||||||
|
|
||||||
const MODULE_NAME = 'Etcher.Components.ImageSelector'
|
const MODULE_NAME = 'Etcher.Components.ImageSelector'
|
||||||
const SelectImageButton = angular.module(MODULE_NAME, [])
|
const ImageSelector = angular.module(MODULE_NAME, [])
|
||||||
|
|
||||||
SelectImageButton.component(
|
ImageSelector.component(
|
||||||
'imageSelector',
|
'imageSelector',
|
||||||
react2angular(require('./image-selector.jsx'))
|
react2angular(require('./image-selector.jsx')),
|
||||||
|
[],
|
||||||
|
[
|
||||||
|
'FileSelectorService',
|
||||||
|
'WarningModalService'
|
||||||
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
module.exports = MODULE_NAME
|
module.exports = MODULE_NAME
|
||||||
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
|
|
@ -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())
|
|
||||||
}
|
|
||||||
}
|
|
@ -31,7 +31,8 @@ const prettyBytes = require('pretty-bytes')
|
|||||||
module.exports = function (
|
module.exports = function (
|
||||||
TooltipModalService,
|
TooltipModalService,
|
||||||
OSOpenExternalService,
|
OSOpenExternalService,
|
||||||
$filter
|
$filter,
|
||||||
|
$scope
|
||||||
) {
|
) {
|
||||||
// Expose several modules to the template for convenience
|
// Expose several modules to the template for convenience
|
||||||
this.selection = selectionState
|
this.selection = selectionState
|
||||||
@ -43,6 +44,13 @@ module.exports = function (
|
|||||||
this.progressMessage = messages.progress
|
this.progressMessage = messages.progress
|
||||||
this.isWebviewShowing = Boolean(store.getState().toJS().isWebviewShowing)
|
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
|
* @summary Determine if the drive step should be disabled
|
||||||
* @function
|
* @function
|
||||||
|
@ -47,14 +47,12 @@ const MainPage = angular.module(MODULE_NAME, [
|
|||||||
require('../../components/drive-selector'),
|
require('../../components/drive-selector'),
|
||||||
|
|
||||||
require('../../os/open-external/open-external'),
|
require('../../os/open-external/open-external'),
|
||||||
require('../../os/dropzone/dropzone'),
|
|
||||||
|
|
||||||
require('../../utils/byte-size/byte-size'),
|
require('../../utils/byte-size/byte-size'),
|
||||||
require('../../utils/middle-ellipsis/filter')
|
require('../../utils/middle-ellipsis/filter')
|
||||||
])
|
])
|
||||||
|
|
||||||
MainPage.controller('MainController', require('./controllers/main'))
|
MainPage.controller('MainController', require('./controllers/main'))
|
||||||
MainPage.controller('ImageSelectionController', require('./controllers/image-selection'))
|
|
||||||
MainPage.controller('DriveSelectionController', require('./controllers/drive-selection'))
|
MainPage.controller('DriveSelectionController', require('./controllers/drive-selection'))
|
||||||
MainPage.controller('FlashController', require('./controllers/flash'))
|
MainPage.controller('FlashController', require('./controllers/flash'))
|
||||||
|
|
||||||
|
@ -1,28 +1,10 @@
|
|||||||
<div class="page-main row around-xs">
|
<div class="page-main row around-xs">
|
||||||
<div class="col-xs" ng-controller="ImageSelectionController as image">
|
<div class="col-xs">
|
||||||
<div class="box text-center relative" os-dropzone="image.selectImageByPath($file)">
|
<image-selector
|
||||||
|
flashing="main.state.isFlashing()"
|
||||||
<div class="center-block">
|
showSelectedImageDetails={main.showSelectedImageDetails}
|
||||||
<svg-icon contents="[ main.selection.getImageLogo() ]" paths="[ '../../assets/image.svg' ]"></svg-icon>
|
>
|
||||||
</div>
|
</image-selector>
|
||||||
|
|
||||||
<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>
|
</div>
|
||||||
|
|
||||||
<div class="col-xs" ng-controller="DriveSelectionController as drive">
|
<div class="col-xs" ng-controller="DriveSelectionController as drive">
|
||||||
|
37
npm-shrinkwrap.json
generated
37
npm-shrinkwrap.json
generated
@ -2135,6 +2135,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
|
||||||
"integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg=="
|
"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": {
|
"aws-sign2": {
|
||||||
"version": "0.7.0",
|
"version": "0.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
|
||||||
@ -6400,6 +6405,14 @@
|
|||||||
"object-assign": "^4.0.1"
|
"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": {
|
"file-type": {
|
||||||
"version": "8.1.0",
|
"version": "8.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/file-type/-/file-type-8.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/file-type/-/file-type-8.1.0.tgz",
|
||||||
@ -10762,6 +10775,28 @@
|
|||||||
"scheduler": "^0.17.0"
|
"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": {
|
"react-google-recaptcha": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-google-recaptcha/-/react-google-recaptcha-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-google-recaptcha/-/react-google-recaptcha-2.0.1.tgz",
|
||||||
@ -14834,4 +14869,4 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -70,6 +70,7 @@
|
|||||||
"prop-types": "^15.5.9",
|
"prop-types": "^15.5.9",
|
||||||
"react": "^16.8.5",
|
"react": "^16.8.5",
|
||||||
"react-dom": "^16.8.5",
|
"react-dom": "^16.8.5",
|
||||||
|
"react-dropzone": "^10.2.1",
|
||||||
"react2angular": "^4.0.2",
|
"react2angular": "^4.0.2",
|
||||||
"redux": "^3.5.2",
|
"redux": "^3.5.2",
|
||||||
"rendition": "^11.24.0",
|
"rendition": "^11.24.0",
|
||||||
|
@ -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()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
@ -19,8 +19,6 @@
|
|||||||
const m = require('mochainon')
|
const m = require('mochainon')
|
||||||
const _ = require('lodash')
|
const _ = require('lodash')
|
||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
const path = require('path')
|
|
||||||
const supportedFormats = require('../../../lib/shared/supported-formats')
|
|
||||||
const angular = require('angular')
|
const angular = require('angular')
|
||||||
const flashState = require('../../../lib/gui/app/models/flash-state')
|
const flashState = require('../../../lib/gui/app/models/flash-state')
|
||||||
const availableDrives = require('../../../lib/gui/app/models/available-drives')
|
const availableDrives = require('../../../lib/gui/app/models/available-drives')
|
||||||
@ -53,7 +51,9 @@ describe('Browser: MainPage', function () {
|
|||||||
describe('.shouldDriveStepBeDisabled()', function () {
|
describe('.shouldDriveStepBeDisabled()', function () {
|
||||||
it('should return true if there is no drive', function () {
|
it('should return true if there is no drive', function () {
|
||||||
const controller = $controller('MainController', {
|
const controller = $controller('MainController', {
|
||||||
$scope: {}
|
$scope: {
|
||||||
|
$apply: _.noop
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
selectionState.clear()
|
selectionState.clear()
|
||||||
@ -63,7 +63,9 @@ describe('Browser: MainPage', function () {
|
|||||||
|
|
||||||
it('should return false if there is a drive', function () {
|
it('should return false if there is a drive', function () {
|
||||||
const controller = $controller('MainController', {
|
const controller = $controller('MainController', {
|
||||||
$scope: {}
|
$scope: {
|
||||||
|
$apply: _.noop
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
selectionState.selectImage({
|
selectionState.selectImage({
|
||||||
@ -80,7 +82,9 @@ describe('Browser: MainPage', function () {
|
|||||||
describe('.shouldFlashStepBeDisabled()', function () {
|
describe('.shouldFlashStepBeDisabled()', function () {
|
||||||
it('should return true if there is no selected drive nor image', function () {
|
it('should return true if there is no selected drive nor image', function () {
|
||||||
const controller = $controller('MainController', {
|
const controller = $controller('MainController', {
|
||||||
$scope: {}
|
$scope: {
|
||||||
|
$apply: _.noop
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
selectionState.clear()
|
selectionState.clear()
|
||||||
@ -90,7 +94,9 @@ describe('Browser: MainPage', function () {
|
|||||||
|
|
||||||
it('should return true if there is a selected image but no drive', function () {
|
it('should return true if there is a selected image but no drive', function () {
|
||||||
const controller = $controller('MainController', {
|
const controller = $controller('MainController', {
|
||||||
$scope: {}
|
$scope: {
|
||||||
|
$apply: _.noop
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
selectionState.clear()
|
selectionState.clear()
|
||||||
@ -106,7 +112,9 @@ describe('Browser: MainPage', function () {
|
|||||||
|
|
||||||
it('should return true if there is a selected drive but no image', function () {
|
it('should return true if there is a selected drive but no image', function () {
|
||||||
const controller = $controller('MainController', {
|
const controller = $controller('MainController', {
|
||||||
$scope: {}
|
$scope: {
|
||||||
|
$apply: _.noop
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
availableDrives.setDrives([
|
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 () {
|
it('should return false if there is a selected drive and a selected image', function () {
|
||||||
const controller = $controller('MainController', {
|
const controller = $controller('MainController', {
|
||||||
$scope: {}
|
$scope: {
|
||||||
|
$apply: _.noop
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
availableDrives.setDrives([
|
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 () {
|
describe('FlashController', function () {
|
||||||
let $controller
|
let $controller
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user