Recent
{
- _.map(existing(this.state.recents), (file) => {
+ _.map(this.state.recents, (file) => {
return (
- this.props.selectFile(files.getFileMetadataSync(file.dirname)) }
- plaintext={ true }>
- { middleEllipsis(path.basename(file.dirname), FILENAME_CHAR_LIMIT_SHORT) }
-
+
)
})
}
Favorite
{
- _.map(existing(this.state.favorites.slice(0, 4)), (file) => {
+ _.map(this.state.favorites.slice(0, 4), (file) => {
return (
- this.props.selectFile(files.getFileMetadataSync(file.fullpath)) }
- plaintext={ true }>
- { middleEllipsis(file.basename, FILENAME_CHAR_LIMIT_SHORT) }
-
+ fullpath={ file.fullpath }
+ title={ file.basename }
+ selectFile={ this.props.selectFile }
+ />
)
})
}
@@ -269,6 +349,32 @@ class RecentFilesUnstyled extends React.PureComponent {
window.removeEventListener('storage', this.onStorage)
}
+ componentDidMount () {
+ Bluebird.reduce(this.state.recents, (newRecents, recent) => {
+ return files.existsAsync(recent.fullpath).then((exists) => {
+ if (exists) {
+ return newRecents.concat(recent)
+ }
+
+ return newRecents
+ })
+ }, []).then((recents) => {
+ this.setState({ recents })
+ })
+
+ Bluebird.reduce(this.state.favorites, (newFavorites, favorite) => {
+ return files.existsAsync(favorite.fullpath).then((exists) => {
+ if (exists) {
+ return newFavorites.concat(favorite)
+ }
+
+ return newFavorites
+ })
+ }, []).then((favorites) => {
+ this.setState({ favorites })
+ })
+ }
+
onStorage (event) {
if (event.key === RECENT_FILES_KEY) {
this.setState(event.newValue)
@@ -278,6 +384,7 @@ class RecentFilesUnstyled extends React.PureComponent {
const RecentFiles = styled(RecentFilesUnstyled)`
display: flex;
+ flex: 0 0 auto;
flex-direction: column;
align-items: flex-start;
width: 130px;
@@ -293,43 +400,80 @@ const RecentFiles = styled(RecentFilesUnstyled)`
margin-bottom: 15px;
}
+ > h5:last-of-type {
+ margin-top: 20px;
+ }
+
> button {
margin-bottom: 10px;
text-align: start;
+ font-size: 16px;
}
`
const labels = {
- '/': 'Root'
+ '/': 'Root',
+ 'mountpoints': 'Mountpoints'
}
-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('..')
- })
+class Crumb extends React.PureComponent {
+ constructor (props) {
+ super(props)
- return (
-
- { dirs.length > MAX_DIR_CRUMBS ? '... / ' : null }
- {
- _.map(dirs.slice(-MAX_DIR_CRUMBS), (dir, index) => {
- return (
- props.selectFile(dir) }
- plaintext={ true }>
-
- { middleEllipsis(labels[dir.fullpath] || dir.basename, FILENAME_CHAR_LIMIT_SHORT) }
-
-
- )
- })
- }
-
- )
-})`
+ this.selectFile = this.selectFile.bind(this)
+ }
+
+ render () {
+ return (
+
+
+ { middleEllipsis(labels[this.props.dir.fullpath] || this.props.dir.basename, FILENAME_CHAR_LIMIT_SHORT) }
+
+
+ )
+ }
+
+ selectFile () {
+ this.props.selectFile(this.props.dir)
+ }
+}
+
+class UnstyledBreadcrumbs extends React.PureComponent {
+ render () {
+ const folderConstraints = this.props.constraints.length
+ ? this.props.constraints
+ : [ path.parse(this.props.path).root ]
+
+ const dirs = files.subpaths(this.props.path).filter((subpath) => {
+ // Guard against displaying folders outside the constrained folders
+ return folderConstraints.some((folderConstraint) => {
+ return !path.relative(folderConstraint, subpath.fullpath).startsWith('..')
+ })
+ })
+
+ return (
+
+ { dirs.length > MAX_DIR_CRUMBS ? '... / ' : null }
+ {
+ _.map(dirs.slice(-MAX_DIR_CRUMBS), (dir, index) => {
+ return (
+
+ )
+ })
+ }
+
+ )
+ }
+}
+
+const Breadcrumbs = styled(UnstyledBreadcrumbs)`
font-size: 18px;
& > button:not(:last-child)::after {
@@ -342,15 +486,12 @@ class FileSelector extends React.PureComponent {
constructor (props) {
super(props)
- const fullpath = props.path || os.homedir()
-
this.state = {
- path: fullpath,
+ path: props.path,
files: [],
history: [],
highlighted: null,
- error: null,
- filters: []
+ error: null
}
// Filters schema
@@ -370,13 +511,15 @@ class FileSelector extends React.PureComponent {
}
this.closeModal = this.closeModal.bind(this)
+ this.resolveModal = this.resolveModal.bind(this)
this.browsePath = this.browsePath.bind(this)
this.selectFile = this.selectFile.bind(this)
+ this.highlightFile = this.highlightFile.bind(this)
this.previousDirectory = this.previousDirectory.bind(this)
}
render () {
- const items = rendition.SchemaSieve.filter(this.state.filters, this.state.files)
+ const items = this.state.files
const styles = {
display: 'flex',
@@ -412,26 +555,21 @@ class FileSelector extends React.PureComponent {
- this.setFilters(filters) }
- onViewsUpdate={ views => this.setViews(views) }
- schema={ this.schema }
- renderMode={ [] } />
-
-
+
{
- items.map((item, index) => {
+ items.map((item) => {
return (
- this.setState({ highlighted: item }) }
- onDoubleClick={ _.partial(this.selectFile, item) }
+ highlightFile={ this.highlightFile }
+ selectFile={ this.selectFile }
/>
)
})
@@ -443,7 +581,7 @@ class FileSelector extends React.PureComponent {
Cancel
Select file
@@ -462,20 +600,21 @@ class FileSelector extends React.PureComponent {
this.props.close()
}
+ resolveModal () {
+ this.selectFile(this.state.highlighted)
+ }
+
setFilesProgressively (dirname) {
return fs.readdirAsync(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) => {
+ // Sort folders first and ignore case
+ fileObjs.sort((fileA, fileB) => {
+ // NOTE(Shou): the multiplication is an arbitrarily large enough number
+ // to ensure folders have precedence over filenames
+ const directoryPrecedence = (-Number(fileA.isDirectory) + Number(fileB.isDirectory)) * 3
+ return directoryPrecedence + fileA.basename.localeCompare(fileB.basename, undefined, { sensitivity: 'base' })
+ })
this.setState({ files: fileObjs })
})
}
@@ -487,11 +626,16 @@ class FileSelector extends React.PureComponent {
'dirname'
]))
- const folderConstraint = this.props.constraint || path.parse(this.state.path).root
+ const folderConstraints = this.props.constraints.length
+ ? this.props.constraints
+ : [ path.parse(this.props.path).root ]
// Guard against browsing outside the constrained folder
- if (path.relative(folderConstraint, file.fullpath).startsWith('..')) {
- const error = `Cannot browse outside constrained folder ${constrainedFolder}`
+ const isWithinAnyConstraint = folderConstraints.some((folderConstraint) => {
+ return !path.relative(folderConstraint, file.fullpath).startsWith('..')
+ })
+ if (!isWithinAnyConstraint) {
+ const error = `Cannot browse outside constrained folders ${folderConstraints}`
analytics.logException(new Error(error))
this.setState({ error })
return
@@ -595,6 +739,10 @@ class FileSelector extends React.PureComponent {
}
}
+ highlightFile (file) {
+ this.setState({ highlighted: file })
+ }
+
previousDirectory () {
analytics.logEvent('Prev directory', null)
const dir = this.state.history.shift()
@@ -604,14 +752,6 @@ class FileSelector extends React.PureComponent {
this.browsePath(dir)
}
}
-
- setFilters (filters) {
- this.setState({ filters })
- }
-
- setViews (views) {
- this.setState({ views })
- }
}
FileSelector.propTypes = {
@@ -619,7 +759,7 @@ FileSelector.propTypes = {
close: propTypes.func,
- constraint: propTypes.string
+ constraints: propTypes.arrayOf(propTypes.string)
}
module.exports = FileSelector
diff --git a/lib/gui/app/components/file-selector/styles/_file-selector.scss b/lib/gui/app/components/file-selector/styles/_file-selector.scss
index d7579c68..189a3cc3 100644
--- a/lib/gui/app/components/file-selector/styles/_file-selector.scss
+++ b/lib/gui/app/components/file-selector/styles/_file-selector.scss
@@ -18,6 +18,6 @@
width: calc(100vw - 10px);
> .modal-content {
- height: calc(100vh - 10px);
+ height: calc(100vh - 20px);
}
}
diff --git a/lib/gui/app/components/file-selector/templates/file-selector-modal.tpl.html b/lib/gui/app/components/file-selector/templates/file-selector-modal.tpl.html
index 9893fbc0..2fc18654 100644
--- a/lib/gui/app/components/file-selector/templates/file-selector-modal.tpl.html
+++ b/lib/gui/app/components/file-selector/templates/file-selector-modal.tpl.html
@@ -1 +1,4 @@
-
+
diff --git a/lib/shared/files.js b/lib/gui/app/models/files.js
similarity index 83%
rename from lib/shared/files.js
rename to lib/gui/app/models/files.js
index f7e7a9d4..92ef5df3 100644
--- a/lib/shared/files.js
+++ b/lib/gui/app/models/files.js
@@ -23,6 +23,31 @@ const fs = Bluebird.promisifyAll(require('fs'))
/* eslint-disable lodash/prefer-lodash-method */
+/**
+ * @summary Async exists function
+ * @function
+ * @public
+ *
+ * @description
+ * This is a promise for convenience, as it never fails with an exception/catch.
+ *
+ * @param {String} fullpath - full path
+ * @returns {Boolean}
+ *
+ * @example
+ * files.existsAsync('/home/user/file').then(console.log)
+ * > true
+ */
+exports.existsAsync = (fullpath) => {
+ return new Bluebird((resolve, reject) => {
+ return fs.accessAsync(fullpath, fs.constants.F_OK).then(() => {
+ resolve(true)
+ }).catch(() => {
+ resolve(false)
+ })
+ })
+}
+
/**
* @summary Get file metadata
* @function
@@ -53,7 +78,7 @@ exports.getFileMetadataSync = (dirname, basename = '') => {
basename: pathObj.base,
dirname: pathObj.dir,
fullpath,
- extension: pathObj.ext.replace('.', ''),
+ extension: pathObj.ext,
name: pathObj.name,
isDirectory: stats.isDirectory(),
isHidden,
@@ -85,7 +110,7 @@ exports.getFileMetadataAsync = (fullpath) => {
basename: pathObj.base,
dirname: pathObj.dir,
fullpath,
- extension: pathObj.ext.replace('.', ''),
+ extension: pathObj.ext,
name: pathObj.name,
isDirectory: stats.isDirectory(),
isHidden,
@@ -109,15 +134,14 @@ exports.getFileMetadataAsync = (fullpath) => {
* files.getAllFilesMetadataAsync(os.homedir(), [ 'file1.txt', 'file2.txt' ])
*/
exports.getAllFilesMetadataAsync = (dirname, basenames) => {
- return Bluebird.all(basenames.map((basename) => {
- const metadata = exports.getFileMetadataAsync(path.join(dirname, basename))
- return metadata.reflect()
- })).reduce((fileMetas, inspection) => {
- if (inspection.isFulfilled()) {
- return fileMetas.concat(inspection.value())
- }
-
- return fileMetas
+ return Bluebird.reduce(basenames, (fileMetas, basename) => {
+ return new Bluebird((resolve, reject) => {
+ exports.getFileMetadataAsync(path.join(dirname, basename)).then((metadata) => {
+ resolve(fileMetas.concat(metadata))
+ }).catch(() => {
+ resolve(fileMetas)
+ })
+ })
}, [])
}
diff --git a/lib/gui/css/main.css b/lib/gui/css/main.css
index 8b84c8a9..4f2db6d8 100644
--- a/lib/gui/css/main.css
+++ b/lib/gui/css/main.css
@@ -6505,7 +6505,7 @@ svg-icon {
.modal-file-selector-modal {
width: calc(100vw - 10px); }
.modal-file-selector-modal > .modal-content {
- height: calc(100vh - 10px); }
+ height: calc(100vh - 20px); }
/*
* Copyright 2016 resin.io
diff --git a/tests/shared/files.spec.js b/tests/gui/models/files.spec.js
similarity index 96%
rename from tests/shared/files.spec.js
rename to tests/gui/models/files.spec.js
index 09498fe5..8431252b 100644
--- a/tests/shared/files.spec.js
+++ b/tests/gui/models/files.spec.js
@@ -18,7 +18,7 @@
const m = require('mochainon')
const path = require('path')
-const files = require('../../lib/shared/files')
+const files = require('../../../lib/gui/app/models/files')
describe('Shared: Files', function () {
describe('.splitPath()', function () {