mirror of
https://github.com/balena-io/etcher.git
synced 2025-07-24 11:46:31 +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
|
||||
require('./os/open-external/open-external'),
|
||||
require('./os/dropzone/dropzone'),
|
||||
|
||||
// Utils
|
||||
require('./utils/manifest-bind/manifest-bind')
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 (
|
||||
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
|
||||
|
@ -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'))
|
||||
|
||||
|
@ -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
37
npm-shrinkwrap.json
generated
@ -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 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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",
|
||||
|
@ -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 _ = 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
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user