mirror of
https://github.com/balena-io/etcher.git
synced 2025-07-22 10:46:31 +00:00
feat(GUI): add electron-native file-picker component (#2333)
We add a file-picker written with Rendition/React. It is activated with the `ETCHER_EXPERIMENTAL_FILE_PICKER` environment variable. Further customisation can be done with the `ETCHER_FILE_BROWSER_CONSTRAIN_FOLDER` variable that takes a path and allows one to constrain the file-picker to a folder. Related: https://github.com/resin-io/etcher/issues/2238 Related: https://github.com/resin-io/etcher/issues/2285 Change-Type: patch Changelog-Entry: Add electron-native file-picker component.
This commit is contained in:
parent
7c97dc8004
commit
7782f94daa
@ -77,6 +77,7 @@ const app = angular.module('Etcher', [
|
||||
require('./components/svg-icon'),
|
||||
require('./components/warning-modal/warning-modal'),
|
||||
require('./components/safe-webview'),
|
||||
require('./components/file-selector'),
|
||||
|
||||
// Pages
|
||||
require('./pages/main/main'),
|
||||
|
@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright 2018 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.exports = function (
|
||||
$uibModalInstance
|
||||
) {
|
||||
/**
|
||||
* @summary Close the modal
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @example
|
||||
* FileSelectorController.close();
|
||||
*/
|
||||
this.close = () => {
|
||||
$uibModalInstance.close()
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Folder to constrain the file picker to
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @returns {String} - folder to constrain by
|
||||
*
|
||||
* @example
|
||||
* FileSelectorController.getFolderConstraint()
|
||||
*/
|
||||
this.getFolderConstraint = () => {
|
||||
// TODO(Shou): get this dynamically from the mountpoint of a specific port in Etcher Pro
|
||||
return process.env.ETCHER_FILE_BROWSER_CONSTRAIN_FOLDER
|
||||
}
|
||||
}
|
@ -0,0 +1,620 @@
|
||||
/*
|
||||
* Copyright 2018 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 os = require('os')
|
||||
const path = require('path')
|
||||
const React = require('react')
|
||||
const propTypes = require('prop-types')
|
||||
const { default: styled } = require('styled-components')
|
||||
const rendition = require('rendition')
|
||||
const prettyBytes = require('pretty-bytes')
|
||||
const Bluebird = require('bluebird')
|
||||
const fontAwesome = require('@fortawesome/fontawesome')
|
||||
const {
|
||||
faFileAlt,
|
||||
faFolder,
|
||||
faAngleLeft,
|
||||
faHdd
|
||||
} = require('@fortawesome/fontawesome-free-solid')
|
||||
const Storage = require('../../../models/storage')
|
||||
const analytics = require('../../../modules/analytics')
|
||||
const middleEllipsis = require('../../../utils/middle-ellipsis')
|
||||
const files = require('../../../../../shared/files')
|
||||
const selectionState = require('../../../../../shared/models/selection-state')
|
||||
const imageStream = require('../../../../../sdk/image-stream')
|
||||
const errors = require('../../../../../shared/errors')
|
||||
const messages = require('../../../../../shared/messages')
|
||||
const supportedFormats = require('../../../../../shared/supported-formats')
|
||||
|
||||
/**
|
||||
* @summary Recent files localStorage object key
|
||||
* @constant
|
||||
* @private
|
||||
*/
|
||||
const RECENT_FILES_KEY = 'file-selector-recent-files'
|
||||
const recentStorage = new Storage(RECENT_FILES_KEY)
|
||||
|
||||
/**
|
||||
* @summary How many directories to show with the breadcrumbs
|
||||
* @type {Number}
|
||||
* @constant
|
||||
* @private
|
||||
*/
|
||||
const MAX_DIR_CRUMBS = 3
|
||||
|
||||
/**
|
||||
* @summary Character limit of a filename before a middle-ellipsis is added
|
||||
* @constant
|
||||
* @private
|
||||
*/
|
||||
const FILENAME_CHAR_LIMIT = 20
|
||||
|
||||
/**
|
||||
* @summary Character limit of a filename before a middle-ellipsis is added
|
||||
* @constant
|
||||
* @private
|
||||
*/
|
||||
const FILENAME_CHAR_LIMIT_SHORT = 15
|
||||
|
||||
/**
|
||||
* @summary Color scheme
|
||||
* @constant
|
||||
* @private
|
||||
*/
|
||||
const colors = {
|
||||
primary: {
|
||||
color: '#3a3c41',
|
||||
background: '#ffffff',
|
||||
subColor: '#ababab'
|
||||
},
|
||||
secondary: {
|
||||
color: '#1c1d1e',
|
||||
background: '#ebeff4',
|
||||
title: '#b3b6b9'
|
||||
},
|
||||
highlight: {
|
||||
color: 'white',
|
||||
background: '#2297de'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Awesome icons HTML object
|
||||
* @constant
|
||||
* @type {Object<HTMLElement>}
|
||||
*/
|
||||
const icons = {
|
||||
faFileAlt: fontAwesome.icon(faFileAlt).html[0],
|
||||
faFolder: fontAwesome.icon(faFolder).html[0],
|
||||
faAngleLeft: fontAwesome.icon(faAngleLeft).html[0],
|
||||
faHdd: fontAwesome.icon(faHdd).html[0]
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Icon React component
|
||||
* @function
|
||||
* @type {ReactElement}
|
||||
*/
|
||||
const Icon = styled((props) => {
|
||||
const { type, ...restProps } = props
|
||||
|
||||
return (
|
||||
<span className={ props.className } dangerouslySetInnerHTML={ { __html: icons[type] } } { ...restProps } />
|
||||
)
|
||||
})`
|
||||
color: ${props => props.color};
|
||||
font-size: ${props => props.size};
|
||||
`
|
||||
|
||||
/**
|
||||
* @summary Flex styled component
|
||||
* @function
|
||||
* @type {ReactElement}
|
||||
*/
|
||||
const Flex = styled.div`
|
||||
display: flex;
|
||||
flex: ${ props => props.flex };
|
||||
flex-direction: ${ props => props.direction };
|
||||
justify-content: ${ props => props.justifyContent };
|
||||
align-items: ${ props => props.alignItems };
|
||||
flex-wrap: ${ props => props.wrap };
|
||||
flex-grow: ${ props => props.grow };
|
||||
`
|
||||
|
||||
const FileLink = styled((props) => {
|
||||
const icon = props.isDirectory ? 'faFolder' : 'faFileAlt'
|
||||
|
||||
return (
|
||||
<Flex
|
||||
direction="column"
|
||||
alignItems="stretch"
|
||||
className={ props.className }
|
||||
onClick={ props.onClick }
|
||||
onDoubleClick={ props.onDoubleClick }>
|
||||
<Icon type={ icon } size="48px" />
|
||||
<rendition.Button plaintext={ true }>
|
||||
{ middleEllipsis(props.basename || '', FILENAME_CHAR_LIMIT) }
|
||||
</rendition.Button>
|
||||
<div>{ prettyBytes(props.size || 0) }</div>
|
||||
</Flex>
|
||||
)
|
||||
})`
|
||||
width: 80px;
|
||||
max-height: 128px;
|
||||
margin: 5px 10px;
|
||||
padding: 5px;
|
||||
background-color: ${ props => props.highlight ? colors.highlight.background : 'none' };
|
||||
transition: 0.15s background-color ease-out;
|
||||
color: ${ props => props.highlight ? colors.highlight.color : colors.primary.color };
|
||||
cursor: pointer;
|
||||
border-radius: 5px;
|
||||
|
||||
> span:first-child {
|
||||
align-self: center;
|
||||
line-height: 1;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
> button,
|
||||
> button:hover,
|
||||
> button:focus {
|
||||
color: inherit;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
> div:last-child {
|
||||
background-color: ${ props => props.highlight ? colors.highlight.background : 'none' }
|
||||
color: ${ props => props.highlight ? colors.highlight.color : colors.primary.subColor }
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
}
|
||||
`
|
||||
|
||||
const Header = Flex.extend`
|
||||
margin: 10px 15px 0;
|
||||
|
||||
> * {
|
||||
margin: 5px;
|
||||
}
|
||||
`
|
||||
|
||||
const Main = Flex.extend``
|
||||
|
||||
const Footer = Flex.extend`
|
||||
margin: 10px 20px;
|
||||
flex: 0 0 auto;
|
||||
|
||||
> * {
|
||||
margin: 0 10px;
|
||||
}
|
||||
`
|
||||
|
||||
const FileListWrap = Flex.extend`
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
padding: 0 20px;
|
||||
`
|
||||
|
||||
class RecentFilesUnstyled extends React.PureComponent {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
recents: recentStorage.get('recents'),
|
||||
favorites: recentStorage.get('favorites')
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const existing = (fileObjs) => {
|
||||
return _.filter(fileObjs, (fileObj) => {
|
||||
return files.exists(fileObj.fullpath)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex className={ this.props.className }>
|
||||
<h5>Recent</h5>
|
||||
{
|
||||
_.map(existing(this.state.recents), (file) => {
|
||||
return (
|
||||
<rendition.Button
|
||||
key={ file.fullpath }
|
||||
onClick={ () => this.props.selectFile(files.getFileMetadataSync(file.dirname)) }
|
||||
plaintext={ true }>
|
||||
{ middleEllipsis(path.basename(file.dirname), FILENAME_CHAR_LIMIT_SHORT) }
|
||||
</rendition.Button>
|
||||
)
|
||||
})
|
||||
}
|
||||
<h5>Favorite</h5>
|
||||
{
|
||||
_.map(existing(this.state.favorites.slice(0, 4)), (file) => {
|
||||
return (
|
||||
<rendition.Button
|
||||
key={ file.fullpath }
|
||||
onClick={ () => this.props.selectFile(files.getFileMetadataSync(file.fullpath)) }
|
||||
plaintext={ true }>
|
||||
{ middleEllipsis(file.basename, FILENAME_CHAR_LIMIT_SHORT) }
|
||||
</rendition.Button>
|
||||
)
|
||||
})
|
||||
}
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
window.addEventListener('storage', this.onStorage)
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
window.removeEventListener('storage', this.onStorage)
|
||||
}
|
||||
|
||||
onStorage (event) {
|
||||
if (event.key === RECENT_FILES_KEY) {
|
||||
this.setState(event.newValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const RecentFiles = styled(RecentFilesUnstyled)`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
width: 130px;
|
||||
background-color: ${ colors.secondary.background };
|
||||
padding: 20px;
|
||||
color: ${ colors.secondary.color };
|
||||
|
||||
> h5 {
|
||||
color: ${ colors.secondary.title };
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
> button {
|
||||
margin-bottom: 10px;
|
||||
text-align: start;
|
||||
}
|
||||
`
|
||||
|
||||
const labels = {
|
||||
'/': 'Root'
|
||||
}
|
||||
|
||||
const Breadcrumbs = styled((props) => {
|
||||
const folderConstraint = props.constraint || path.parse(props.path).root
|
||||
const dirs = files.subpaths(props.path).filter((subpath) => {
|
||||
// Guard against displaying folders outside the constrained folder
|
||||
return !path.relative(folderConstraint, subpath.fullpath).startsWith('..')
|
||||
})
|
||||
|
||||
return (
|
||||
<div className={ props.className }>
|
||||
{ dirs.length > MAX_DIR_CRUMBS ? '... / ' : null }
|
||||
{
|
||||
_.map(dirs.slice(-MAX_DIR_CRUMBS), (dir, index) => {
|
||||
return (
|
||||
<rendition.Button
|
||||
onClick={ () => props.selectFile(dir) }
|
||||
plaintext={ true }>
|
||||
<rendition.Txt bold={ index === dirs.length - 1 }>
|
||||
{ middleEllipsis(labels[dir.fullpath] || dir.basename, FILENAME_CHAR_LIMIT_SHORT) }
|
||||
</rendition.Txt>
|
||||
</rendition.Button>
|
||||
)
|
||||
})
|
||||
}
|
||||
</div>
|
||||
)
|
||||
})`
|
||||
font-size: 18px;
|
||||
|
||||
& > button:not(:last-child)::after {
|
||||
content: '/';
|
||||
margin: 9px;
|
||||
}
|
||||
`
|
||||
|
||||
class FileSelector extends React.PureComponent {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
const fullpath = props.path || os.homedir()
|
||||
|
||||
this.state = {
|
||||
path: fullpath,
|
||||
files: [],
|
||||
history: [],
|
||||
highlighted: null,
|
||||
error: null,
|
||||
filters: []
|
||||
}
|
||||
|
||||
// Filters schema
|
||||
this.schema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
basename: {
|
||||
type: 'string'
|
||||
},
|
||||
isHidden: {
|
||||
type: 'boolean'
|
||||
},
|
||||
isDirectory: {
|
||||
type: 'boolean'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.closeModal = this.closeModal.bind(this)
|
||||
this.browsePath = this.browsePath.bind(this)
|
||||
this.selectFile = this.selectFile.bind(this)
|
||||
this.previousDirectory = this.previousDirectory.bind(this)
|
||||
}
|
||||
|
||||
render () {
|
||||
const items = rendition.SchemaSieve.filter(this.state.filters, this.state.files)
|
||||
|
||||
const styles = {
|
||||
display: 'flex',
|
||||
height: 'calc(100vh - 20px)',
|
||||
}
|
||||
|
||||
const errorModal = (
|
||||
<rendition.Modal
|
||||
title={ _.get(this.state.error, [ 'title' ]) || "Error" }
|
||||
done={ () => this.setState({ error: null }) }
|
||||
action="Dismiss"
|
||||
primaryButtonProps={ { danger: true, primary: false } }
|
||||
>
|
||||
{ _.get(this.state.error, [ 'message' ]) || this.state.error }
|
||||
</rendition.Modal>
|
||||
)
|
||||
|
||||
return (
|
||||
<rendition.Provider
|
||||
style={ styles }>
|
||||
<RecentFiles selectFile={ this.selectFile } flex="0 0 auto" />
|
||||
<Flex direction="column" grow="1">
|
||||
<Header flex="0 0 auto" alignItems="baseline">
|
||||
<rendition.Button
|
||||
bg={ colors.secondary.background }
|
||||
color={ colors.primary.color }
|
||||
disabled={ !this.state.history.length }
|
||||
onClick={ this.previousDirectory }>
|
||||
<Icon type={ 'faAngleLeft' } />
|
||||
Back
|
||||
</rendition.Button>
|
||||
<Icon type={ 'faHdd' } />
|
||||
<Breadcrumbs
|
||||
path={ this.state.path }
|
||||
selectFile={ this.selectFile }
|
||||
constraint={ this.props.constraint }
|
||||
/>
|
||||
</Header>
|
||||
<Main flex="1">
|
||||
<Flex direction="column" grow="1">
|
||||
<rendition.Filters
|
||||
onFiltersUpdate={ filters => this.setFilters(filters) }
|
||||
onViewsUpdate={ views => this.setViews(views) }
|
||||
schema={ this.schema }
|
||||
renderMode={ [] } />
|
||||
|
||||
<FileListWrap wrap="wrap">
|
||||
{
|
||||
items.map((item, index) => {
|
||||
return (
|
||||
<FileLink { ...item }
|
||||
highlight={ _.get(this.state.highlighted, 'fullpath') === _.get(item, 'fullpath') }
|
||||
onClick={ () => this.setState({ highlighted: item }) }
|
||||
onDoubleClick={ _.partial(this.selectFile, item) }
|
||||
/>
|
||||
)
|
||||
})
|
||||
}
|
||||
</FileListWrap>
|
||||
</Flex>
|
||||
</Main>
|
||||
<Footer justifyContent="flex-end">
|
||||
<rendition.Button onClick={ this.closeModal }>Cancel</rendition.Button>
|
||||
<rendition.Button
|
||||
primary
|
||||
onClick={ _.partial(this.selectFile, this.state.highlighted) }
|
||||
disabled={ !this.state.highlighted }>
|
||||
Select file
|
||||
</rendition.Button>
|
||||
</Footer>
|
||||
</Flex>
|
||||
{ this.state.error ? errorModal : null }
|
||||
</rendition.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.setFilesProgressively(this.state.path)
|
||||
}
|
||||
|
||||
closeModal () {
|
||||
this.props.close()
|
||||
}
|
||||
|
||||
setFilesProgressively (dirname) {
|
||||
return files.getDirectory(dirname).then((basenames) => {
|
||||
const fileObjs = basenames.map((basename) => {
|
||||
return {
|
||||
dirname: this.state.path,
|
||||
basename,
|
||||
fullpath: path.join(dirname, basename)
|
||||
}
|
||||
})
|
||||
|
||||
this.setState({ files: fileObjs })
|
||||
|
||||
return files.getAllFilesMetadataAsync(dirname, basenames)
|
||||
}).then((fileObjs) => {
|
||||
this.setState({ files: fileObjs })
|
||||
})
|
||||
}
|
||||
|
||||
browsePath (file) {
|
||||
analytics.logEvent('File browse', _.omit(file, [
|
||||
'fullpath',
|
||||
'basename',
|
||||
'dirname'
|
||||
]))
|
||||
|
||||
const folderConstraint = this.props.constraint || path.parse(this.state.path).root
|
||||
|
||||
// Guard against browsing outside the constrained folder
|
||||
if (path.relative(folderConstraint, file.fullpath).startsWith('..')) {
|
||||
const error = `Cannot browse outside constrained folder ${constrainedFolder}`
|
||||
analytics.logException(new Error(error))
|
||||
this.setState({ error })
|
||||
return
|
||||
}
|
||||
|
||||
this.setFilesProgressively(file.fullpath).then(() => {
|
||||
this.setState({ path: file.fullpath })
|
||||
})
|
||||
}
|
||||
|
||||
selectFile (file, event) {
|
||||
if (file === null) {
|
||||
analytics.logEvent('File dismiss', null)
|
||||
} else if (file.isDirectory) {
|
||||
const prevFile = files.getFileMetadataSync(this.state.path)
|
||||
this.setState({
|
||||
history: this.state.history.concat(prevFile)
|
||||
})
|
||||
this.browsePath(file)
|
||||
} else {
|
||||
analytics.logEvent('File select', file.basename)
|
||||
|
||||
imageStream.getImageMetadata(file.fullpath)
|
||||
.then((image) => {
|
||||
if (!supportedFormats.isSupportedImage(image.path)) {
|
||||
const invalidImageError = errors.createUserError({
|
||||
title: 'Invalid image',
|
||||
description: messages.error.invalidImage(image)
|
||||
})
|
||||
|
||||
this.setState({ error: invalidImageError })
|
||||
analytics.logEvent('Invalid image', image)
|
||||
return
|
||||
}
|
||||
|
||||
return Bluebird.try(() => {
|
||||
let message = null
|
||||
|
||||
if (supportedFormats.looksLikeWindowsImage(image.path)) {
|
||||
analytics.logEvent('Possibly Windows image', image)
|
||||
message = messages.warning.looksLikeWindowsImage()
|
||||
} else if (!image.hasMBR) {
|
||||
analytics.logEvent('Missing partition table', image)
|
||||
message = messages.warning.missingPartitionTable()
|
||||
}
|
||||
|
||||
if (message) {
|
||||
// TODO(Shou): `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`
|
||||
this.setState({ error: message })
|
||||
return
|
||||
}
|
||||
|
||||
return image
|
||||
}).then((image) => {
|
||||
if (image) {
|
||||
selectionState.selectImage(image)
|
||||
}
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
const imageError = errors.createUserError({
|
||||
title: 'Error opening image',
|
||||
description: messages.error.openImage(file.basename, error.message)
|
||||
})
|
||||
|
||||
this.setState({ error: imageError })
|
||||
analytics.logException(error)
|
||||
})
|
||||
|
||||
// Add folder to recently used
|
||||
recentStorage.modify('recents', (recents) => {
|
||||
const newRecents = _.uniqBy([ file ].concat(recents), (recentFile) => {
|
||||
return recentFile.dirname
|
||||
})
|
||||
|
||||
// NOTE(Shou): we want to limit how many recent directories are stored - since we only
|
||||
// display four and don't rely on past state, we can only store four
|
||||
return newRecents.slice(0, 4)
|
||||
}, [])
|
||||
|
||||
// Add file to potential favorites list
|
||||
recentStorage.modify('favorites', (favorites) => {
|
||||
const favorite = _.find(favorites, (favoriteFile) => {
|
||||
return favoriteFile.fullpath === file.fullpath
|
||||
}) || _.assign({}, file, { frequency: 1 })
|
||||
|
||||
const newFavorites = _.uniqBy([ favorite ].concat(favorites), (favoriteFile) => {
|
||||
return favoriteFile.fullpath
|
||||
})
|
||||
|
||||
// NOTE(Shou): we want to limit how many favorite files are stored - since we
|
||||
// *do* rely on past state, we need to store a reasonable amount of favorites so
|
||||
// they can be sorted by frequency. We only display four.
|
||||
return newFavorites.slice(0, 256)
|
||||
}, [])
|
||||
|
||||
this.closeModal()
|
||||
}
|
||||
}
|
||||
|
||||
previousDirectory () {
|
||||
analytics.logEvent('Prev directory', null)
|
||||
const dir = this.state.history.shift()
|
||||
this.setState({ history: this.state.history })
|
||||
|
||||
if (dir) {
|
||||
this.browsePath(dir)
|
||||
}
|
||||
}
|
||||
|
||||
setFilters (filters) {
|
||||
this.setState({ filters })
|
||||
}
|
||||
|
||||
setViews (views) {
|
||||
this.setState({ views })
|
||||
}
|
||||
}
|
||||
|
||||
FileSelector.propTypes = {
|
||||
path: propTypes.string,
|
||||
|
||||
close: propTypes.func,
|
||||
|
||||
constraint: propTypes.string
|
||||
}
|
||||
|
||||
module.exports = FileSelector
|
37
lib/gui/app/components/file-selector/index.js
Normal file
37
lib/gui/app/components/file-selector/index.js
Normal file
@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright 2018 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'
|
||||
|
||||
/* eslint-disable jsdoc/require-example */
|
||||
|
||||
/**
|
||||
* @module Etcher.Components.SVGIcon
|
||||
*/
|
||||
|
||||
const angular = require('angular')
|
||||
const react2angular = require('react2angular').react2angular
|
||||
|
||||
const MODULE_NAME = 'Etcher.Components.FileSelector'
|
||||
const angularFileSelector = angular.module(MODULE_NAME, [
|
||||
require('../modal/modal')
|
||||
])
|
||||
|
||||
angularFileSelector.component('fileSelector', react2angular(require('./file-selector/file-selector.jsx')))
|
||||
angularFileSelector.controller('FileSelectorController', require('./controllers/file-selector'))
|
||||
angularFileSelector.service('FileSelectorService', require('./services/file-selector'))
|
||||
|
||||
module.exports = MODULE_NAME
|
@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright 2018 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.exports = function (ModalService, $q) {
|
||||
let modal = null
|
||||
|
||||
/**
|
||||
* @summary Open the file selector widget
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @example
|
||||
* DriveSelectorService.open()
|
||||
*/
|
||||
this.open = () => {
|
||||
modal = ModalService.open({
|
||||
name: 'file-selector',
|
||||
template: require('../templates/file-selector-modal.tpl.html'),
|
||||
controller: 'FileSelectorController as selector',
|
||||
size: 'file-selector-modal'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Close the file selector widget
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @example
|
||||
* DriveSelectorService.close()
|
||||
*/
|
||||
this.close = () => {
|
||||
if (modal) {
|
||||
modal.close()
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright 2018 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.
|
||||
*/
|
||||
|
||||
.modal-file-selector-modal {
|
||||
width: calc(100vw - 10px);
|
||||
|
||||
> .modal-content {
|
||||
height: calc(100vh - 10px);
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
<file-selector close="selector.close" constraint="selector.getFolderConstraint()" ></file-selector>
|
@ -20,6 +20,7 @@
|
||||
flex-direction: column;
|
||||
margin: 0 auto;
|
||||
height: auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
@ -101,5 +102,4 @@
|
||||
.modal-dialog {
|
||||
margin: 0;
|
||||
position: initial;
|
||||
max-width: 50%;
|
||||
}
|
||||
|
@ -30,6 +30,7 @@ const exceptionReporter = require('../../../modules/exception-reporter')
|
||||
|
||||
module.exports = function (
|
||||
$timeout,
|
||||
FileSelectorService,
|
||||
WarningModalService
|
||||
) {
|
||||
/**
|
||||
@ -155,16 +156,20 @@ module.exports = function (
|
||||
this.openImageSelector = () => {
|
||||
analytics.logEvent('Open image selector')
|
||||
|
||||
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')
|
||||
return
|
||||
}
|
||||
if (process.env.ETCHER_EXPERIMENTAL_FILE_PICKER) {
|
||||
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')
|
||||
return
|
||||
}
|
||||
|
||||
this.selectImageByPath(imagePath)
|
||||
}).catch(exceptionReporter.report)
|
||||
this.selectImageByPath(imagePath)
|
||||
}).catch(exceptionReporter.report)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -38,6 +38,7 @@ const MainPage = angular.module(MODULE_NAME, [
|
||||
require('../../components/flash-error-modal/flash-error-modal'),
|
||||
require('../../components/progress-button/progress-button'),
|
||||
require('../../components/warning-modal/warning-modal'),
|
||||
require('../../components/file-selector'),
|
||||
|
||||
require('../../os/open-external/open-external'),
|
||||
require('../../os/dropzone/dropzone'),
|
||||
|
@ -37,6 +37,7 @@ $disabled-opacity: 0.2;
|
||||
@import "../components/svg-icon/styles/svg-icon";
|
||||
@import "../components/tooltip-modal/styles/tooltip-modal";
|
||||
@import "../components/warning-modal/styles/warning-modal";
|
||||
@import "../components/file-selector/styles/file-selector";
|
||||
@import "../pages/main/styles/main";
|
||||
@import "../pages/settings/styles/settings";
|
||||
@import "../pages/finish/styles/finish";
|
||||
|
@ -6225,7 +6225,8 @@ body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0 auto;
|
||||
height: auto; }
|
||||
height: auto;
|
||||
overflow: hidden; }
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
@ -6277,8 +6278,7 @@ body {
|
||||
|
||||
.modal-dialog {
|
||||
margin: 0;
|
||||
position: initial;
|
||||
max-width: 50%; }
|
||||
position: initial; }
|
||||
|
||||
/*
|
||||
* Copyright 2016 resin.io
|
||||
@ -6487,6 +6487,26 @@ svg-icon {
|
||||
max-height: 200px;
|
||||
overflow-y: auto; }
|
||||
|
||||
/*
|
||||
* Copyright 2018 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.
|
||||
*/
|
||||
.modal-file-selector-modal {
|
||||
width: calc(100vw - 10px); }
|
||||
.modal-file-selector-modal > .modal-content {
|
||||
height: calc(100vh - 10px); }
|
||||
|
||||
/*
|
||||
* Copyright 2016 resin.io
|
||||
*
|
||||
|
675
npm-shrinkwrap.json
generated
675
npm-shrinkwrap.json
generated
File diff suppressed because it is too large
Load Diff
@ -78,6 +78,7 @@
|
||||
"react2angular": "1.1.3",
|
||||
"readable-stream": "2.3.3",
|
||||
"redux": "3.5.2",
|
||||
"rendition": "4.6.0",
|
||||
"request": "2.81.0",
|
||||
"resin-cli-form": "1.4.1",
|
||||
"resin-cli-visuals": "1.4.1",
|
||||
@ -85,6 +86,8 @@
|
||||
"roboto-fontface": "0.9.0",
|
||||
"semver": "5.1.1",
|
||||
"speedometer": "1.0.0",
|
||||
"styled-components": "3.2.3",
|
||||
"styled-system": "1.1.7",
|
||||
"sudo-prompt": "8.0.0",
|
||||
"udif": "0.13.0",
|
||||
"unbzip2-stream": "github:resin-io-modules/unbzip2-stream#core-streams",
|
||||
|
@ -35,6 +35,10 @@ require.extensions['.html'] = (module, filename) => {
|
||||
})
|
||||
}
|
||||
|
||||
// NOTE(Shou): since we don't test React yet we just ignore JSX files
|
||||
// eslint-disable-next-line node/no-deprecated-api
|
||||
require.extensions['.jsx'] = _.constant(null)
|
||||
|
||||
describe('Browser: MainPage', function () {
|
||||
beforeEach(angular.mock.module(
|
||||
require('../../../lib/gui/app/pages/main/main')
|
||||
|
Loading…
x
Reference in New Issue
Block a user