diff --git a/package-lock.json b/package-lock.json index 80b127285..fa2bb101a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10902,7 +10902,8 @@ }, "ansi-regex": { "version": "2.1.1", - "bundled": true + "bundled": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -10920,11 +10921,13 @@ }, "balanced-match": { "version": "1.0.0", - "bundled": true + "bundled": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -10937,15 +10940,18 @@ }, "code-point-at": { "version": "1.1.0", - "bundled": true + "bundled": true, + "optional": true }, "concat-map": { "version": "0.0.1", - "bundled": true + "bundled": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", - "bundled": true + "bundled": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -11048,7 +11054,8 @@ }, "inherits": { "version": "2.0.3", - "bundled": true + "bundled": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -11058,6 +11065,7 @@ "is-fullwidth-code-point": { "version": "1.0.0", "bundled": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -11070,17 +11078,20 @@ "minimatch": { "version": "3.0.4", "bundled": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } }, "minimist": { "version": "0.0.8", - "bundled": true + "bundled": true, + "optional": true }, "minipass": { "version": "2.3.5", "bundled": true, + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -11097,6 +11108,7 @@ "mkdirp": { "version": "0.5.1", "bundled": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -11169,7 +11181,8 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true + "bundled": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -11179,6 +11192,7 @@ "once": { "version": "1.4.0", "bundled": true, + "optional": true, "requires": { "wrappy": "1" } @@ -11254,7 +11268,8 @@ }, "safe-buffer": { "version": "5.1.2", - "bundled": true + "bundled": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -11284,6 +11299,7 @@ "string-width": { "version": "1.0.2", "bundled": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -11301,6 +11317,7 @@ "strip-ansi": { "version": "3.0.1", "bundled": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -11339,11 +11356,13 @@ }, "wrappy": { "version": "1.0.2", - "bundled": true + "bundled": true, + "optional": true }, "yallist": { "version": "3.0.3", - "bundled": true + "bundled": true, + "optional": true } } }, @@ -12799,9 +12818,9 @@ } }, "ipfs-css": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/ipfs-css/-/ipfs-css-0.10.0.tgz", - "integrity": "sha512-e8UWepgq3+3Ht/pBaP5fQSZtXjs0m/9RQBbd32H6u9Q9ULlrFxTM2P8jRn0PFP90I74ke38Y8agAB4U1bf846A==" + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/ipfs-css/-/ipfs-css-0.13.1.tgz", + "integrity": "sha512-hofJSYVBE3VC3/MOYZKfF66SKuHgnYkhXUmPDS8PISI8ygcljGOyBSSU4Je3dfgZX5UHDBEnzq5XyrTU822EDg==" }, "ipfs-geoip": { "version": "3.0.0", @@ -12824,7 +12843,7 @@ "bs58": "^4.0.1", "buffer": "^5.2.1", "cids": "~0.5.5", - "concat-stream": "github:hugomrdias/concat-stream#057bc7b5d6d8df26c8cf00a3f151b6721a0a8034", + "concat-stream": "github:hugomrdias/concat-stream#feat/smaller", "debug": "^4.1.0", "detect-node": "^2.0.4", "end-of-stream": "^1.4.1", @@ -12846,7 +12865,7 @@ "multibase": "~0.6.0", "multicodec": "~0.5.0", "multihashes": "~0.4.14", - "ndjson": "github:hugomrdias/ndjson#4db16da6b42e5b39bf300c3a7cde62abb3fa3a11", + "ndjson": "github:hugomrdias/ndjson#feat/readable-stream3", "once": "^1.4.0", "peer-id": "~0.12.2", "peer-info": "~0.15.1", @@ -23526,7 +23545,8 @@ }, "ansi-regex": { "version": "2.1.1", - "bundled": true + "bundled": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -23544,11 +23564,13 @@ }, "balanced-match": { "version": "1.0.0", - "bundled": true + "bundled": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -23561,15 +23583,18 @@ }, "code-point-at": { "version": "1.1.0", - "bundled": true + "bundled": true, + "optional": true }, "concat-map": { "version": "0.0.1", - "bundled": true + "bundled": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", - "bundled": true + "bundled": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -23672,7 +23697,8 @@ }, "inherits": { "version": "2.0.3", - "bundled": true + "bundled": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -23682,6 +23708,7 @@ "is-fullwidth-code-point": { "version": "1.0.0", "bundled": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -23694,17 +23721,20 @@ "minimatch": { "version": "3.0.4", "bundled": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } }, "minimist": { "version": "0.0.8", - "bundled": true + "bundled": true, + "optional": true }, "minipass": { "version": "2.2.4", "bundled": true, + "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -23721,6 +23751,7 @@ "mkdirp": { "version": "0.5.1", "bundled": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -23793,7 +23824,8 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true + "bundled": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -23803,6 +23835,7 @@ "once": { "version": "1.4.0", "bundled": true, + "optional": true, "requires": { "wrappy": "1" } @@ -23878,7 +23911,8 @@ }, "safe-buffer": { "version": "5.1.1", - "bundled": true + "bundled": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -23908,6 +23942,7 @@ "string-width": { "version": "1.0.2", "bundled": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -23925,6 +23960,7 @@ "strip-ansi": { "version": "3.0.1", "bundled": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -23963,11 +23999,13 @@ }, "wrappy": { "version": "1.0.2", - "bundled": true + "bundled": true, + "optional": true }, "yallist": { "version": "3.0.2", - "bundled": true + "bundled": true, + "optional": true } } }, @@ -25737,6 +25775,11 @@ } } }, + "simplify-number": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/simplify-number/-/simplify-number-1.0.0.tgz", + "integrity": "sha512-/rODISYl4lm+Y2wuhdOHiYuwZ/DZgLIIGfyXY9NCJy0gtTs40VfiCyDUXTPro3tY9Du27NFA8tGTgVQqzEy8mA==" + }, "sisteransi": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-0.1.1.tgz", diff --git a/package.json b/package.json index 6286f0259..4870adbd1 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "i18next-icu": "^0.6.0", "i18next-xhr-backend": "^1.5.1", "internal-nav-helper": "^1.0.2", - "ipfs-css": "^0.10.0", + "ipfs-css": "^0.13.1", "ipfs-geoip": "^3.0.0", "ipfs-redux-bundle": "^5.1.2", "ipfs-unixfs": "^0.1.15", @@ -73,6 +73,7 @@ "react-virtualized": "^9.20.1", "redux-bundler": "^21.2.2", "redux-bundler-react": "^1.0.1", + "simplify-number": "^1.0.0", "tachyons": "^4.11.1", "topojson": "^3.0.2", "video-extensions": "^1.1.0", diff --git a/public/locales/en/files.json b/public/locales/en/files.json index 895f6c01e..558bc157a 100644 --- a/public/locales/en/files.json +++ b/public/locales/en/files.json @@ -32,6 +32,8 @@ "clear": "Clear", "create": "Create", "save": "Save", + "pin": "Pin", + "unpin": "Unpin", "unselectAll": "Unselect all" }, "shareModal": { @@ -74,5 +76,14 @@ "noFiles": "<0>No files in this directory. Click the “Add” button to add some." }, "addFilesInfo": "<0>Add files to your local IPFS node by clicking the <1>Add to IPFS button above.", - "companionInfo": "<0>As you are using <1>IPFS Companion, the files view is limited to files added while using the extension." + "companionInfo": "<0>As you are using <1>IPFS Companion, the files view is limited to files added while using the extension.", + "exploreForm": { + "explore": "Explore" + }, + "hashUnavailable": "hash unavailable", + "pins": "Pins", + "pinned": "Pinned", + "blocks": "Blocks", + "more": "More", + "files": "Files" } diff --git a/public/locales/en/notify.json b/public/locales/en/notify.json index a733c3467..7173ab18a 100644 --- a/public/locales/en/notify.json +++ b/public/locales/en/notify.json @@ -4,8 +4,14 @@ "ipfsApiRequestFailed": "IPFS request failed. Please check if your daemon is running.", "windowIpfsRequestFailed": "IPFS request failed. Please check your IPFS Companion settings.", "ipfsIsBack": "Normal IPFS service has resumed. Enjoy!", - "filesEventFailed": "Failed to add files to IPFS. Please try again.", "folderExists": "A folder with that name already exists. Please choose another.", + "filesFetchFailed": "Failed to get those files. Please check the path and try again.", + "filesRenameFailed": "Failed to rename the files. Please try again.", + "filesMakeDirFailed": "Failed to create directory. Please try again.", + "filesCopyFailed": "Failed to copy the files. Please try again.", + "filesDeleteFailed": "Failed to delete the files. Please try again.", + "filesAddFailed": "Failed to add files to IPFS. Please try again.", + "filesEventFailed": "Failed to process your request. Please try again.", "couldntConnectToPeer": "Could not connect to the provided peer.", "connectedToPeer": "Successfully connected to the provided peer.", "experimentsErrors": { diff --git a/src/App.js b/src/App.js index 6515aed4d..a55139060 100644 --- a/src/App.js +++ b/src/App.js @@ -3,7 +3,6 @@ import PropTypes from 'prop-types' import { connect } from 'redux-bundler-react' import navHelper from 'internal-nav-helper' import { filesToStreams } from './lib/files' -import { IpldExploreForm } from 'ipld-explorer-components' // React DnD import { DragDropContext, DropTarget } from 'react-dnd' import { NativeTypes } from 'react-dnd-html5-backend' @@ -13,6 +12,7 @@ import NavBar from './navigation/NavBar' import ComponentLoader from './loader/ComponentLoader' import Notify from './components/notify/Notify' import Connected from './components/connected/Connected' +import FilesExploreForm from './files/explore-form/FilesExploreForm' export class App extends Component { static propTypes = { @@ -40,7 +40,7 @@ export class App extends Component { const addAtPath = isFilesPage ? routeInfo.params.path : '/' const files = await filesPromise - doFilesWrite(addAtPath, await filesToStreams(files)) + doFilesWrite(await filesToStreams(files), addAtPath) // Change to the files pages if the user is not there if (!isFilesPage) { doUpdateHash('/files') @@ -48,12 +48,12 @@ export class App extends Component { } render () { - const { route: Page, ipfsReady, routeInfo: { url }, navbarIsOpen, connectDropTarget, isOver } = this.props + const { route: Page, ipfsReady, routeInfo: { url }, doFilesNavigateTo, navbarIsOpen, connectDropTarget, canDrop, isOver } = this.props return connectDropTarget(
{/* Tinted overlay that appears when dragging and dropping an item */} - { isOver &&
} + { canDrop && isOver &&
}
@@ -61,13 +61,13 @@ export class App extends Component {
- +
-
+
{ (ipfsReady || url === '/welcome' || url.startsWith('/settings')) ? : @@ -89,7 +89,8 @@ const dropTarget = { const { filesPromise } = monitor.getItem() App.addFiles(filesPromise) - } + }, + canDrop: (props) => props.filesPathInfo ? props.filesPathInfo.isMfs : true } const dropCollect = (connect, monitor) => ({ @@ -104,10 +105,12 @@ export default connect( 'selectRoute', 'selectNavbarIsOpen', 'selectRouteInfo', + 'doFilesNavigateTo', 'doUpdateUrl', 'doUpdateHash', 'doInitIpfs', 'doFilesWrite', 'selectIpfsReady', + 'selectFilesPathInfo', DragDropContext(DnDBackend)(AppWithDropTarget) ) diff --git a/src/bundles/files.js b/src/bundles/files.js deleted file mode 100644 index 34eebf84a..000000000 --- a/src/bundles/files.js +++ /dev/null @@ -1,408 +0,0 @@ -import { join, dirname, basename } from 'path' -import { createSelector } from 'redux-bundler' -import { getDownloadLink, getShareableLink } from '../lib/files' -import countDirs from '../lib/count-dirs' -import { sortByName, sortBySize } from '../lib/sort' - -const isMac = navigator.userAgent.indexOf('Mac') !== -1 - -export const actions = { - FETCH: 'FETCH', - MOVE: 'MOVE', - COPY: 'COPY', - DELETE: 'DELETE', - MAKE_DIR: 'MAKEDIR', - WRITE: 'WRITE', - DOWNLOAD_LINK: 'DOWNLOADLINK', - ADD_BY_PATH: 'ADDBYPATH' -} - -export const sorts = { - BY_NAME: 'name', - BY_SIZE: 'size' -} - -const ignore = [ - '.DS_Store', - 'thumbs.db', - 'desktop.ini' -] - -const make = (basename, action) => (...args) => async (args2) => { - const id = Symbol(basename) - const { dispatch, getIpfs, store } = args2 - dispatch({ type: `FILES_${basename}_STARTED`, payload: { id } }) - - let data - - try { - data = await action(getIpfs(), ...args, id, args2) - dispatch({ type: `FILES_${basename}_FINISHED`, payload: { id, ...data } }) - - // Rename specific logic - if (basename === actions.MOVE) { - const src = args[0][0] - const dst = args[0][1] - - if (src === store.selectFiles().path) { - await store.doUpdateHash(`/files${dst}`) - } - } - - // Delete specific logic - if (basename === actions.DELETE) { - const src = args[0][0] - - let path = src.split('/') - path.pop() - path = path.join('/') - - await store.doUpdateHash(`/files${path}`) - } - } catch (error) { - console.log(error) - dispatch({ type: `FILES_${basename}_FAILED`, payload: { id, error } }) - } finally { - if (basename !== actions.FETCH) { - await store.doFilesFetch() - } - } - - return data -} - -const sortFiles = (files, sorting) => { - const sortDir = sorting.asc ? 1 : -1 - const nameSort = sortByName(sortDir) - const sizeSort = sortBySize(sortDir) - - return files.sort((a, b) => { - if (a.type === b.type || isMac) { - if (sorting.by === sorts.BY_NAME) { - return nameSort(a.name, b.name) - } else { - return sizeSort(a.cumulativeSize || a.size, b.cumulativeSize || b.size) - } - } - - if (a.type === 'directory') { - return -1 - } else { - return 1 - } - }) -} - -const fetchFiles = make(actions.FETCH, async (ipfs, id, { store }) => { - const path = store.selectFilesPathFromHash() - const stats = await ipfs.files.stat(path) - - if (stats.type === 'file') { - return { - path: path, - fetched: Date.now(), - type: 'file', - read: () => ipfs.files.read(path), - name: path.split('/').pop(), - size: stats.size, - hash: stats.hash - } - } - - // Otherwise get the directory info - const res = await ipfs.files.ls(path, { l: true }) || [] - const files = [] - const showStats = res.length < 100 - - for (const f of res) { - let file = { - ...f, - path: join(path, f.name), - type: f.type === 0 ? 'file' : 'directory' - } - - if (showStats && file.type === 'directory') { - file = { - ...file, - ...await ipfs.files.stat(file.path) - } - } - - files.push(file) - } - - const upperPath = dirname(path) - const upper = path === '/' ? null : await ipfs.files.stat(upperPath) - if (upper) { - upper.path = upperPath - } - - return { - path: path, - fetched: Date.now(), - type: 'directory', - upper: upper, - content: sortFiles(files, store.selectFilesSorting()) - } -}) - -const defaultState = { - pageContent: null, - sorting: { // TODO: cache this - by: sorts.BY_NAME, - asc: true - }, - pending: [], - finished: [], - failed: [] -} - -export default (opts = {}) => { - opts.baseUrl = opts.baseUrl || '/files' - - return { - name: 'files', - - reducer: (state = defaultState, action) => { - if (!action.type.startsWith('FILES_')) { - return state - } - - if (action.type === 'FILES_DISMISS_ERRORS') { - return { - ...state, - failed: [] - } - } - - if (action.type === 'FILES_UPDATE_SORT') { - const pageContent = state.pageContent - - return { - ...state, - pageContent: { - ...pageContent, - content: sortFiles(pageContent.content, action.payload) - }, - sorting: action.payload - } - } - - const [ type, status ] = action.type.split('_').splice(1) - const { id, ...data } = action.payload - - if (status === 'STARTED') { - return { - ...state, - pending: [ - ...state.pending, - { - type: type, - id: id, - start: Date.now(), - data: data - } - ] - } - } else if (status === 'UPDATED') { - const pendingAction = state.pending.find(a => a.id === id) - - return { - ...state, - pending: [ - ...state.pending.filter(a => a.id !== id), - { - ...pendingAction, - data: data - } - ] - } - } else if (status === 'FAILED') { - const pendingAction = state.pending.find(a => a.id === id) - return { - ...state, - pending: state.pending.filter(a => a.id !== id), - failed: [ - ...state.failed, - { - ...pendingAction, - end: Date.now(), - error: data.error - } - ] - } - } else if (status === 'FINISHED') { - const action = state.pending.find(a => a.id === id) - let additional - - if (type === actions.FETCH) { - additional = { - pageContent: data - } - } - - return { - ...state, - ...additional, - pending: state.pending.filter(a => a.id !== id), - finished: [ - ...state.finished, - { - ...action, - data: data, - end: Date.now() - } - ] - } - } - - return state - }, - - doFilesFetch: () => async ({ store, ...args }) => { - const isReady = store.selectIpfsReady() - const isConnected = store.selectIpfsConnected() - const isFetching = store.selectFilesIsFetching() - const path = store.selectFilesPathFromHash() - - if (isReady && isConnected && !isFetching && path) { - fetchFiles()({ store, ...args }) - } - }, - - doFilesWrite: make(actions.WRITE, async (ipfs, root, files, id, { dispatch }) => { - files = files.filter(f => !ignore.includes(basename(f.path))) - const totalSize = files.reduce((prev, { size }) => prev + size, 0) - - // Normalise all paths to be relative. Dropped files come as absolute, - // those added by the file input come as relative paths, so normalise them. - files.forEach(s => { - if (s.path[0] === '/') { - s.path = s.path.slice(1) - } - }) - - const updateProgress = (sent) => { - dispatch({ type: 'FILES_WRITE_UPDATED', payload: { id: id, progress: sent / totalSize * 100 } }) - } - - updateProgress(0) - - let res = null - try { - res = await ipfs.add(files, { - pin: false, - wrapWithDirectory: false, - progress: updateProgress - }) - } catch (error) { - console.error(error) - throw error - } - - const numberOfFiles = files.length - const numberOfDirs = countDirs(files) - const expectedResponseCount = numberOfFiles + numberOfDirs - - if (res.length !== expectedResponseCount) { - // See https://github.com/ipfs/js-ipfs-api/issues/797 - throw Object.assign(new Error(`API returned a partial response.`), { code: 'ERR_API_RESPONSE' }) - } - - for (const { path, hash } of res) { - // Only go for direct children - if (path.indexOf('/') === -1 && path !== '') { - const src = `/ipfs/${hash}` - const dst = join(root, path) - - try { - await ipfs.files.cp([src, dst]) - } catch (err) { - console.log(err, { root, path, src, dst }) - throw Object.assign(new Error(`Folder already exists.`), { code: 'ERR_FOLDER_EXISTS' }) - } - } - } - - updateProgress(totalSize) - }), - - doFilesDelete: make(actions.DELETE, (ipfs, files) => { - const promises = files.map(file => ipfs.files.rm(file, { recursive: true })) - return Promise.all(promises) - }), - - doFilesAddPath: make(actions.ADD_BY_PATH, (ipfs, root, src) => { - const name = src.split('/').pop() - const dst = join(root, name) - const srcPath = src.startsWith('/') ? src : `/ipfs/${name}` - return ipfs.files.cp([srcPath, dst]) - }), - - doFilesDownloadLink: make(actions.DOWNLOAD_LINK, async (ipfs, files, id, { store }) => { - const apiUrl = store.selectApiUrl() - const gatewayUrl = store.selectGatewayUrl() - return getDownloadLink(files, gatewayUrl, apiUrl, ipfs) - }), - - doFilesShareLink: make(actions.SHARE_LINK, async (ipfs, files) => getShareableLink(files, ipfs)), - - doFilesMove: make(actions.MOVE, (ipfs, src, dst) => ipfs.files.mv([src, dst])), - - doFilesCopy: make(actions.COPY, (ipfs, src, dst) => ipfs.files.cp([src, dst])), - - doFilesMakeDir: make(actions.MAKE_DIR, (ipfs, path) => ipfs.files.mkdir(path, { parents: true })), - - doFilesDismissErrors: () => async ({ dispatch }) => dispatch({ type: 'FILES_DISMISS_ERRORS' }), - - doFilesNavigateTo: (path) => async ({ store }) => { - const link = path.split('/').map(p => encodeURIComponent(p)).join('/') - const files = store.selectFiles() - - if (files.path === link) { - store.doFilesFetch() - } else { - store.doUpdateHash(`${opts.baseUrl}${link}`) - } - }, - - doFilesUpdateSorting: (by, asc) => async ({ dispatch }) => { - dispatch({ type: 'FILES_UPDATE_SORT', payload: { by, asc } }) - }, - - selectFiles: (state) => state.files.pageContent, - - selectFilesIsFetching: (state) => state.files.pending.some(a => a.type === actions.FETCH), - - selectShowLoadingAnimation: (state) => { - const pending = state.files.pending.find(a => a.type === actions.FETCH) - return pending ? (Date.now() - pending.start) > 1000 : false - }, - - selectFilesSorting: (state) => state.files.sorting, - - selectWriteFilesProgress: (state) => { - const writes = state.files.pending.filter(s => s.type === actions.WRITE && s.data.progress) - - if (writes.length === 0) { - return null - } - - const sum = writes.reduce((acc, s) => s.data.progress + acc, 0) - return sum / writes.length - }, - - selectFilesHasError: (state) => state.files.failed.length > 0, - - selectFilesErrors: (state) => state.files.failed, - - selectFilesPathFromHash: createSelector( - 'selectRouteInfo', - (routeInfo) => { - if (!routeInfo.url.startsWith(opts.baseUrl)) return - if (!routeInfo.params.path) return - return decodeURIComponent(routeInfo.params.path) - } - ) - } -} diff --git a/src/bundles/files.test.js b/src/bundles/files.test.js index 3bc5d9323..c4d424b28 100644 --- a/src/bundles/files.test.js +++ b/src/bundles/files.test.js @@ -46,7 +46,7 @@ it('write single file to root', async () => { size: 400 }] - await store.doFilesWrite('/', input) + await store.doFilesWrite(input, '/') expect(ipfs.add.mock.calls.length).toBe(1) expect(ipfs.add.mock.calls[0][0][0]).toBe(input[0]) @@ -65,7 +65,7 @@ it('write single file to directory', async () => { size: 400 }] - await store.doFilesWrite('/dir', input) + await store.doFilesWrite(input, '/dir') expect(ipfs.add.mock.calls.length).toBe(1) expect(ipfs.add.mock.calls[0][0][0]).toBe(input[0]) @@ -89,7 +89,7 @@ it('write multiple file', async () => { size: 400 }] - await store.doFilesWrite('/', input) + await store.doFilesWrite(input, '/') expect(ipfs.add.mock.calls.length).toBe(1) expect(ipfs.add.mock.calls[0][0][0]).toBe(input[0]) @@ -121,7 +121,7 @@ it('write multiple file with ignored file', async () => { size: 400 }] - await store.doFilesWrite('/', input) + await store.doFilesWrite(input, '/') expect(ipfs.add.mock.calls.length).toBe(1) expect(ipfs.add.mock.calls[0][0][0]).toBe(input[0]) diff --git a/src/bundles/files/actions.js b/src/bundles/files/actions.js new file mode 100644 index 000000000..a5ecc7bd6 --- /dev/null +++ b/src/bundles/files/actions.js @@ -0,0 +1,252 @@ +import { join, dirname, basename } from 'path' +import { getDownloadLink, getShareableLink } from '../../lib/files' +import countDirs from '../../lib/count-dirs' + +import { make, sortFiles, infoFromPath } from './utils' +import { IGNORED_FILES, ACTIONS } from './consts' + +const fileFromStats = ({ cumulativeSize, type, size, hash, name }, path, prefix = '/ipfs') => ({ + size: cumulativeSize || size || null, + type: (type === 'dir' || type === 'directory') ? 'directory' : 'file', + hash: hash, + name: name || path ? path.split('/').pop() : hash, + path: path || `${prefix}/${hash}` +}) + +// TODO: use sth else +const realMfsPath = (path) => { + if (path.startsWith('/files')) { + return path.substr('/files'.length) || '/' + } + + return path +} + +const getRawPins = async (ipfs) => { + const recursive = await ipfs.pin.ls({ type: 'recursive' }) + const direct = await ipfs.pin.ls({ type: 'direct' }) + + return recursive.concat(direct) +} + +const getPins = async (ipfs) => { + const pins = await getRawPins(ipfs) + + const stats = await Promise.all( + pins.map(({ hash }) => { + return ipfs.files.stat(`/ipfs/${hash}`) + }) + ) + + return stats.map(item => { + item = fileFromStats(item, null, '/pins') + item.pinned = true + return item + }) +} + +const fetchFiles = make(ACTIONS.FETCH, async (ipfs, id, { store }) => { + let { path, realPath, isMfs, isPins, isRoot } = store.selectFilesPathInfo() + + if (isRoot && !isMfs && !isPins) { + throw new Error('not supposed to be here') + } + + if (isRoot && isPins) { + const pins = await getPins(ipfs) // FIX: pins path + + return { + path: '/pins', + fetched: Date.now(), + type: 'directory', + content: pins + } + } + + if (realPath.startsWith('/ipns')) { + realPath = await ipfs.name.resolve(realPath) + } + + const stats = await ipfs.files.stat(realPath) + + if (stats.type === 'file') { + return { + ...fileFromStats(stats, path), + fetched: Date.now(), + type: 'file', + read: () => ipfs.cat(stats.hash), + name: path.split('/').pop(), + size: stats.size, + hash: stats.hash + } + } + + // Otherwise get the directory info + const res = await ipfs.ls(stats.hash) || [] + const files = [] + const showStats = res.length < 100 + + for (const f of res) { + const absPath = join(path, f.name) + let file = null + + if (showStats && f.type === 'dir') { + file = fileFromStats(await ipfs.files.stat(`/ipfs/${f.hash}`), absPath) + } else { + file = fileFromStats(f, absPath) + } + + files.push(file) + } + + let parent = null + + if (!isRoot) { + const parentPath = dirname(path) + const parentInfo = infoFromPath(parentPath) + + if (parentInfo.isMfs || !parentInfo.isRoot) { + if (parentInfo.realPath.startsWith('/ipns')) { + parentInfo.realPath = await ipfs.name.resolve(parentInfo.realPath) + } + + console.log(parentInfo.realPath) + parent = fileFromStats(await ipfs.files.stat(parentInfo.realPath)) + + parent.name = '..' + parent.path = parentInfo.path + parent.isParent = true + } + } + + return { + path: path, + fetched: Date.now(), + type: 'directory', + hash: stats.hash, + upper: parent, + content: sortFiles(files, store.selectFilesSorting()) + } +}) + +export default () => ({ + doPinsFetch: make(ACTIONS.PIN_LIST, async (ipfs) => { + return { pins: (await getRawPins(ipfs)).map(f => f.hash) } + }), + + doFilesFetch: () => async ({ store, ...args }) => { + const isReady = store.selectIpfsReady() + const isConnected = store.selectIpfsConnected() + const isFetching = store.selectFilesIsFetching() + const info = store.selectFilesPathInfo() + + if (isReady && isConnected && !isFetching && info) { + fetchFiles()({ store, ...args }) + } + }, + + doFilesWrite: make(ACTIONS.WRITE, async (ipfs, files, root, id, { dispatch }) => { + files = files.filter(f => !IGNORED_FILES.includes(basename(f.path))) + const totalSize = files.reduce((prev, { size }) => prev + size, 0) + + // Normalise all paths to be relative. Dropped files come as absolute, + // those added by the file input come as relative paths, so normalise them. + files.forEach(s => { + if (s.path[0] === '/') { + s.path = s.path.slice(1) + } + }) + + const updateProgress = (sent) => { + dispatch({ type: 'FILES_WRITE_UPDATED', payload: { id: id, progress: sent / totalSize * 100 } }) + } + + updateProgress(0) + + let res = null + try { + res = await ipfs.add(files, { + pin: false, + wrapWithDirectory: false, + progress: updateProgress + }) + } catch (error) { + console.error(error) + throw error + } + + const numberOfFiles = files.length + const numberOfDirs = countDirs(files) + const expectedResponseCount = numberOfFiles + numberOfDirs + + if (res.length !== expectedResponseCount) { + // See https://github.com/ipfs/js-ipfs-api/issues/797 + throw Object.assign(new Error(`API returned a partial response.`), { code: 'ERR_API_RESPONSE' }) + } + + for (const { path, hash } of res) { + // Only go for direct children + if (path.indexOf('/') === -1 && path !== '') { + const src = `/ipfs/${hash}` + const dst = join(realMfsPath(root), path) + + try { + await ipfs.files.cp([src, dst]) + } catch (err) { + throw Object.assign(new Error(`Folder already exists.`), { code: 'ERR_FOLDER_EXISTS' }) + } + } + } + + updateProgress(totalSize) + }), + + doFilesDelete: make(ACTIONS.DELETE, (ipfs, files) => { + const promises = files.map(file => ipfs.files.rm(realMfsPath(file), { recursive: true })) + return Promise.all(promises) + }, { mfsOnly: true }), + + doFilesAddPath: make(ACTIONS.ADD_BY_PATH, (ipfs, root, src) => { + src = realMfsPath(src) + const name = src.split('/').pop() + const dst = realMfsPath(join(root, name)) + const srcPath = src.startsWith('/') ? src : `/ipfs/${name}` + + return ipfs.files.cp([srcPath, dst]) + }, { mfsOnly: true }), + + doFilesDownloadLink: make(ACTIONS.DOWNLOAD_LINK, async (ipfs, files, id, { store }) => { + const apiUrl = store.selectApiUrl() + const gatewayUrl = store.selectGatewayUrl() + return getDownloadLink(files, gatewayUrl, apiUrl, ipfs) + }), + + doFilesShareLink: make(ACTIONS.SHARE_LINK, async (ipfs, files) => getShareableLink(files, ipfs)), + + doFilesMove: make(ACTIONS.MOVE, (ipfs, src, dst) => ipfs.files.mv([realMfsPath(src), realMfsPath(dst)]), { mfsOnly: true }), + + doFilesCopy: make(ACTIONS.COPY, (ipfs, src, dst) => ipfs.files.cp([realMfsPath(src), realMfsPath(dst)]), { mfsOnly: true }), + + doFilesMakeDir: make(ACTIONS.MAKE_DIR, (ipfs, path) => ipfs.files.mkdir(realMfsPath(path), { parents: true }), { mfsOnly: true }), + + doFilesPin: make(ACTIONS.PIN_ADD, (ipfs, hash) => ipfs.pin.add(hash)), + + doFilesUnpin: make(ACTIONS.PIN_REMOVE, (ipfs, hash) => ipfs.pin.rm(hash)), + + doFilesDismissErrors: () => async ({ dispatch }) => dispatch({ type: 'FILES_DISMISS_ERRORS' }), + + doFilesNavigateTo: (path) => async ({ store }) => { + const link = path.split('/').map(p => encodeURIComponent(p)).join('/') + const files = store.selectFiles() + + if (files && files.path === link) { + store.doFilesFetch() + } else { + store.doUpdateHash(`${link}`) + } + }, + + doFilesUpdateSorting: (by, asc) => async ({ dispatch }) => { + dispatch({ type: 'FILES_UPDATE_SORT', payload: { by, asc } }) + } +}) diff --git a/src/bundles/files/consts.js b/src/bundles/files/consts.js new file mode 100644 index 000000000..8062b80e7 --- /dev/null +++ b/src/bundles/files/consts.js @@ -0,0 +1,38 @@ +export const IS_MAC = navigator.userAgent.indexOf('Mac') !== -1 + +export const ACTIONS = { + FETCH: 'FETCH', + MOVE: 'MOVE', + COPY: 'COPY', + DELETE: 'DELETE', + MAKE_DIR: 'MAKEDIR', + WRITE: 'WRITE', + DOWNLOAD_LINK: 'DOWNLOADLINK', + ADD_BY_PATH: 'ADDBYPATH', + PIN_ADD: 'PIN_ADD', + PIN_REMOVE: 'PIN_REMOVE', + PIN_LIST: 'PIN_LIST' +} + +export const SORTING = { + BY_NAME: 'name', + BY_SIZE: 'size' +} + +export const IGNORED_FILES = [ + '.DS_Store', + 'thumbs.db', + 'desktop.ini' +] + +export const DEFAULT_STATE = { + pageContent: null, + pins: [], + sorting: { // TODO: cache this + by: SORTING.BY_NAME, + asc: true + }, + pending: [], + finished: [], + failed: [] +} diff --git a/src/bundles/files/index.js b/src/bundles/files/index.js new file mode 100644 index 000000000..924df5ee8 --- /dev/null +++ b/src/bundles/files/index.js @@ -0,0 +1,136 @@ +import { sortFiles } from './utils' +import { DEFAULT_STATE, ACTIONS, SORTING } from './consts' +import selectors from './selectors' +import actions from './actions' + +export { ACTIONS } + +export const sorts = SORTING + +export default () => { + return { + name: 'files', + + reducer: (state = DEFAULT_STATE, action) => { + if (!action.type.startsWith('FILES_')) { + return state + } + + if (action.type === 'FILES_DISMISS_ERRORS') { + return { + ...state, + failed: [] + } + } + + if (action.type === 'FILES_UPDATE_SORT') { + const pageContent = state.pageContent + + return { + ...state, + pageContent: { + ...pageContent, + content: sortFiles(pageContent.content, action.payload) + }, + sorting: action.payload + } + } + + const parts = action.type.split('_').splice(1) + + const status = parts.pop() + const type = parts.join('_') + + const { id, ...data } = action.payload + + if (status === 'STARTED') { + return { + ...state, + pending: [ + ...state.pending, + { + type: type, + id: id, + start: Date.now(), + data: data + } + ] + } + } else if (status === 'UPDATED') { + const pendingAction = state.pending.find(a => a.id === id) + + return { + ...state, + pending: [ + ...state.pending.filter(a => a.id !== id), + { + ...pendingAction, + data: data + } + ] + } + } else if (status === 'FAILED') { + const pendingAction = state.pending.find(a => a.id === id) + let additional + + if (type === ACTIONS.FETCH) { + additional = { + pageContent: null + } + } + + return { + ...state, + ...additional, + pending: state.pending.filter(a => a.id !== id), + failed: [ + ...state.failed, + { + ...pendingAction, + end: Date.now(), + error: data.error + } + ] + } + } else if (status === 'FINISHED') { + const action = state.pending.find(a => a.id === id) + let additional + + if (type === ACTIONS.FETCH) { + additional = { + pageContent: data + } + + if (data.path === '/pins') { + additional.pins = data.content.map(f => f.hash) + } + } + + if (type === ACTIONS.PIN_LIST) { + additional = { + pins: data.pins + } + } + + return { + ...state, + ...additional, + pending: state.pending.filter(a => a.id !== id), + finished: [ + ...state.finished, + { + ...action, + data: data, + end: Date.now() + } + ] + } + } + + return state + }, + + ...actions(), + ...selectors() + } +} diff --git a/src/bundles/files/selectors.js b/src/bundles/files/selectors.js new file mode 100644 index 000000000..f54a923fd --- /dev/null +++ b/src/bundles/files/selectors.js @@ -0,0 +1,40 @@ +import { createSelector } from 'redux-bundler' +import { ACTIONS } from './consts' +import { infoFromPath } from './utils' + +export default () => ({ + selectFiles: (state) => state.files.pageContent, + + selectPins: (state) => state.files.pins, + + selectFilesIsFetching: (state) => state.files.pending.some(a => a.type === ACTIONS.FETCH), + + selectShowLoadingAnimation: (state) => { + const pending = state.files.pending.find(a => a.type === ACTIONS.FETCH) + return pending ? (Date.now() - pending.start) > 1000 : false + }, + + selectFilesSorting: (state) => state.files.sorting, + + selectWriteFilesProgress: (state) => { + const writes = state.files.pending.filter(s => s.type === ACTIONS.WRITE && s.data.progress) + + if (writes.length === 0) { + return null + } + + const sum = writes.reduce((acc, s) => s.data.progress + acc, 0) + return sum / writes.length + }, + + selectFilesHasError: (state) => state.files.failed.length > 0, + + selectFilesErrors: (state) => state.files.failed, + + selectFilesPathInfo: createSelector( + 'selectRouteInfo', + (routeInfo) => { + return infoFromPath(routeInfo.url) + } + ) +}) diff --git a/src/bundles/files/utils.js b/src/bundles/files/utils.js new file mode 100644 index 000000000..d099ed5cb --- /dev/null +++ b/src/bundles/files/utils.js @@ -0,0 +1,125 @@ +import { sortByName, sortBySize } from '../../lib/sort' +import { ACTIONS, IS_MAC, SORTING } from './consts' + +export const make = (basename, action, options = {}) => (...args) => async (args2) => { + const id = Symbol(basename) + const { dispatch, getIpfs, store } = args2 + dispatch({ type: `FILES_${basename}_STARTED`, payload: { id } }) + + let data + + if (options.mfsOnly) { + const info = store.selectFilesPathInfo() + if (!info || !info.isMfs) { + // musn't happen + return + } + } + + try { + data = await action(getIpfs(), ...args, id, args2) + dispatch({ type: `FILES_${basename}_FINISHED`, payload: { id, ...data } }) + + // Rename specific logic + if (basename === ACTIONS.MOVE) { + const src = args[0] + const dst = args[1] + + if (src === store.selectFiles().path) { + await store.doUpdateHash(dst) + } + } + + // Delete specific logic + if (basename === ACTIONS.DELETE) { + const src = args[0][0] + + let path = src.split('/') + path.pop() + path = path.join('/') + + await store.doUpdateHash(path) + } + } catch (error) { + if (!error.code) { + error.code = `ERR_${basename}` + } + + console.error(error) + dispatch({ type: `FILES_${basename}_FAILED`, payload: { id, error } }) + } finally { + if (basename !== ACTIONS.FETCH) { + await store.doFilesFetch() + } + + if (basename === ACTIONS.PIN_ADD || basename === ACTIONS.PIN_REMOVE) { + await store.doPinsFetch() + } + } + + return data +} + +export const sortFiles = (files, sorting) => { + const sortDir = sorting.asc ? 1 : -1 + const nameSort = sortByName(sortDir) + const sizeSort = sortBySize(sortDir) + + return files.sort((a, b) => { + if (a.type === b.type || IS_MAC) { + if (sorting.by === SORTING.BY_NAME) { + return nameSort(a.name, b.name) + } else { + return sizeSort(a.cumulativeSize || a.size, b.cumulativeSize || b.size) + } + } + + if (a.type === 'directory') { + return -1 + } else { + return 1 + } + }) +} + +export const infoFromPath = (path) => { + const info = { + path: path, + realPath: null, + isMfs: false, + isPins: false, + isRoot: false + } + + const check = (prefix) => { + info.realPath = info.path.substr(prefix.length).trim() || '/' + info.isRoot = info.realPath === '/' + } + + if (info.path.startsWith('/ipns') || info.path.startsWith('/ipfs')) { + info.realPath = info.path + info.isRoot = info.path === '/ipns' || info.path === '/ipfs' + } else if (info.path.startsWith('/files')) { + check('/files') + info.isMfs = true + } else if (info.path.startsWith('/pins')) { + check('/pins') + info.isPins = true + + if (info.realPath !== '/') { + info.realPath = `/ipfs${info.realPath}` + } + } else { + return + } + + if (info.path.endsWith('/') && info.realPath !== '/') { + info.path = info.path.substring(0, info.path.length - 1) + info.realPath = info.realPath.substring(0, info.realPath.length - 1) + } + + info.realPath = decodeURIComponent(info.realPath) + info.path = decodeURIComponent(info.path) + + return info +} diff --git a/src/bundles/notify.js b/src/bundles/notify.js index 6031bce45..77a4026b9 100644 --- a/src/bundles/notify.js +++ b/src/bundles/notify.js @@ -1,5 +1,6 @@ import { createSelector } from 'redux-bundler' import { ACTIONS as EXP_ACTIONS } from './experiments' +import { ACTIONS as FILES_ACTIONS } from './files' /* # Notify @@ -85,7 +86,28 @@ const notify = { } if (eventId === 'FILES_EVENT_FAILED') { - return code === 'ERR_FOLDER_EXISTS' ? 'folderExists' : 'filesEventFailed' + let type = code ? code.replace(/^(ERR_)/, '') : '' + + switch (type) { + case 'FOLDER_EXISTS': + return 'folderExists' + case FILES_ACTIONS.WRITE: + case FILES_ACTIONS.ADD_BY_PATH: + case 'API_RESPONSE': + return 'filesAddFailed' + case FILES_ACTIONS.FETCH: + return 'filesFetchFailed' + case FILES_ACTIONS.MOVE: + return 'filesRenameFailed' + case FILES_ACTIONS.MAKE_DIR: + return 'filesMakeDirFailed' + case FILES_ACTIONS.COPY: + return 'filesCopyFailed' + case FILES_ACTIONS.DELETE: + return 'filesDeleteFailed' + default: + return 'filesEventFailed' + } } if (eventId === 'STATS_FETCH_FINISHED') { diff --git a/src/bundles/redirects.js b/src/bundles/redirects.js index 4a46975b3..50df8ce34 100644 --- a/src/bundles/redirects.js +++ b/src/bundles/redirects.js @@ -12,15 +12,6 @@ export default { } ), - reactToEmptyFiles: createSelector( - 'selectHash', - (hash) => { - if (hash === '/files') { - return { actionCreator: 'doUpdateHash', args: ['#/files/'] } - } - } - ), - reactToIpfsConnectionFail: createSelector( 'selectIpfsInitFailed', 'selectHash', diff --git a/src/bundles/repo-stats.js b/src/bundles/repo-stats.js index f51b879bd..c618a67d2 100644 --- a/src/bundles/repo-stats.js +++ b/src/bundles/repo-stats.js @@ -19,6 +19,15 @@ bundle.selectRepoSize = createSelector( } ) +bundle.selectRepoNumObjects = createSelector( + 'selectRepoStats', + (repoStats) => { + if (repoStats && repoStats.numObjects) { + return repoStats.numObjects.toString() + } + } +) + // Fetch the config if we don't have it or it's more than `staleAfter` ms old bundle.reactRepoStatsFetch = createSelector( 'selectRepoStatsShouldUpdate', diff --git a/src/bundles/routes.js b/src/bundles/routes.js index bd0391c80..6a2efb023 100644 --- a/src/bundles/routes.js +++ b/src/bundles/routes.js @@ -11,6 +11,9 @@ export default createRouteBundle({ '/explore': StartExploringPage, '/explore*': ExplorePage, '/files*': FilesPage, + '/ipfs*': FilesPage, + '/ipns*': FilesPage, + '/pins*': FilesPage, '/peers': PeersPage, '/settings': SettingsPage, '/settings/analytics': AnalyticsPage, diff --git a/src/components/button/Button.js b/src/components/button/Button.js index 0ad321c2f..e3eddf8b2 100644 --- a/src/components/button/Button.js +++ b/src/components/button/Button.js @@ -1,10 +1,11 @@ import React from 'react' import './Button.css' -const Button = ({ bg = 'bg-aqua', color = 'white', className = '', disabled, danger, minWidth = 140, children, style, ...props }) => { +const Button = ({ bg = 'bg-aqua', color = 'white', fill = 'white', className = '', disabled, danger, minWidth = 140, children, style, ...props }) => { const bgClass = danger ? 'bg-red' : disabled ? 'bg-gray-muted' : bg + const fillClass = danger ? 'fill-white' : disabled ? 'fill-snow' : fill const colorClass = danger ? 'white' : disabled ? 'light-gray' : color - const cls = `Button transition-all sans-serif dib v-mid fw5 nowrap lh-copy bn br1 pa2 pointer focus-outline ${bgClass} ${colorClass} ${className}` + const cls = `Button transition-all sans-serif dib v-mid fw5 nowrap lh-copy bn br1 pa2 pointer focus-outline ${fillClass} ${bgClass} ${colorClass} ${className}` return ( +
+ + ) + } +} + +FilesExploreForm.propTypes = { + t: PropTypes.func.isRequired, + onNavigate: PropTypes.func.isRequired +} + +export default translate('files')(FilesExploreForm) diff --git a/src/files/explore-form/FilesExploreForm.stories.js b/src/files/explore-form/FilesExploreForm.stories.js new file mode 100644 index 000000000..6aa3b9f59 --- /dev/null +++ b/src/files/explore-form/FilesExploreForm.stories.js @@ -0,0 +1,15 @@ +import React from 'react' +import { storiesOf } from '@storybook/react' +import { action } from '@storybook/addon-actions' +import { checkA11y } from '@storybook/addon-a11y' +import i18n from '../../i18n-decorator' +import FilesExploreForm from './FilesExploreForm' + +storiesOf('Files', module) + .addDecorator(checkA11y) + .addDecorator(i18n) + .add('Explore Form', () => ( +
+ +
+ )) diff --git a/src/files/file-input/FileInput.js b/src/files/file-input/FileInput.js index 81243871d..d6b09943b 100644 --- a/src/files/file-input/FileInput.js +++ b/src/files/file-input/FileInput.js @@ -1,25 +1,24 @@ import React from 'react' -import PropTypes from 'prop-types' import { connect } from 'redux-bundler-react' +import PropTypes from 'prop-types' import { translate } from 'react-i18next' import { filesToStreams } from '../../lib/files' // Icons import DocumentIcon from '../../icons/StrokeDocument' import FolderIcon from '../../icons/StrokeFolder' +import NewFolderIcon from '../../icons/StrokeNewFolder' import DecentralizationIcon from '../../icons/StrokeDecentralization' // Components import { Dropdown, DropdownMenu, Option } from '../dropdown/Dropdown' import Button from '../../components/button/Button' -import Overlay from '../../components/overlay/Overlay' -import ByPathModal from './ByPathModal' -const AddButton = translate('files')(({ progress = null, t, tReady, i18n, lng, ...props }) => { +const AddButton = translate('files')(({ progress = null, disabled, t, tReady, i18n, lng, ...props }) => { const sending = progress !== null return ( -
)} -
+
+ { pinned &&
+ +
} +
+
{size}
{ this.dotsWrapper = el }} className='ph2' style={{ width: '2.5rem' }}> @@ -139,6 +146,7 @@ const dragSource = { setIsDragging() return { name, type, path } }, + canDrag: props => props.isMfs, endDrag: (props) => { props.setIsDragging(false) } } @@ -161,10 +169,11 @@ const dropTarget = { const src = item.path const dst = join(props.path, basename(item.path)) - props.onMove([src, dst]) + props.onMove(src, dst) } }, canDrop: (props, monitor) => { + if (!props.isMfs) return false const item = monitor.getItem() if (item.hasOwnProperty('name')) { @@ -185,6 +194,6 @@ const dropCollect = (connect, monitor) => ({ export default DragSource(File.TYPE, dragSource, dragCollect)( DropTarget([File.TYPE, NativeTypes.FILE], dropTarget, dropCollect)( - File + translate('files')(File) ) ) diff --git a/src/files/files-list/FilesList.js b/src/files/files-list/FilesList.js index ea352a2a6..25eb8bcd1 100644 --- a/src/files/files-list/FilesList.js +++ b/src/files/files-list/FilesList.js @@ -28,7 +28,7 @@ export class FilesList extends React.Component { className: PropTypes.string, files: PropTypes.array.isRequired, upperDir: PropTypes.object, - sort: PropTypes.shape({ + filesSorting: PropTypes.shape({ by: PropTypes.string.isRequired, asc: PropTypes.bool.isRequired }), @@ -36,6 +36,7 @@ export class FilesList extends React.Component { root: PropTypes.string.isRequired, downloadProgress: PropTypes.number, filesIsFetching: PropTypes.bool, + filesPathInfo: PropTypes.object, // React Drag'n'Drop isOver: PropTypes.bool.isRequired, canDrop: PropTypes.bool.isRequired, @@ -83,13 +84,7 @@ export class FilesList extends React.Component { get selectedMenu () { const unselectAll = () => this.toggleAll(false) - const size = this.selectedFiles.reduce((a, b) => { - if (b.cumulativeSize) { - return a + b.cumulativeSize - } - - return a + b.size - }, 0) + const size = this.selectedFiles.reduce((a, b) => a + (b.size || 0), 0) const show = this.state.selected.length !== 0 // We need this to get the width in ems @@ -107,8 +102,9 @@ export class FilesList extends React.Component { rename={() => this.props.onRename(this.selectedFiles)} share={() => this.props.onShare(this.selectedFiles)} download={() => this.props.onDownload(this.selectedFiles)} - inspect={() => this.props.onInspect(this.selectedFiles)} + inspect={() => this.props.onInspect(this.selectedFiles[0].hash)} count={this.state.selected.length} + isMfs={this.props.filesPathInfo.isMfs} downloadProgress={this.props.downloadProgress} size={size} /> ) @@ -226,7 +222,7 @@ export class FilesList extends React.Component { this.listRef.current.forceUpdateGrid() } - move = ([src, dst]) => { + move = (src, dst) => { const selected = this.selectedFiles if (selected.length > 0) { @@ -249,25 +245,25 @@ export class FilesList extends React.Component { } this.toggleAll(false) - toMove.forEach(op => this.props.onMove(op)) + toMove.forEach(op => this.props.onMove(...op)) } else { - this.props.onMove([src, dst]) + this.props.onMove(src, dst) } } sortByIcon = (order) => { - if (this.props.sort.by === order) { - return this.props.sort.asc ? '↑' : '↓' + if (this.props.filesSorting.by === order) { + return this.props.filesSorting.asc ? '↑' : '↓' } return null } changeSort = (order) => () => { - const { sort, updateSorting } = this.props + const { filesSorting, updateSorting } = this.props - if (order === sort.by) { - updateSorting(order, !sort.asc) + if (order === filesSorting.by) { + updateSorting(order, !filesSorting.asc) } else { updateSorting(order, true) } @@ -309,7 +305,7 @@ export class FilesList extends React.Component { } rowRenderer = ({ index, key, style }) => { - const { files, upperDir, isOver, canDrop, onNavigate, onAddFiles } = this.props + const { files, pins, upperDir, filesIsMfs, isOver, canDrop, onNavigate, onAddFiles } = this.props const { selected, focused, isDragging } = this.state if (upperDir) { @@ -318,15 +314,15 @@ export class FilesList extends React.Component { return (
{ this.filesRefs['..'] = r }} + ref={r => { this.filesRefs[upperDir.name] = r }} onNavigate={() => onNavigate(upperDir.path)} onAddFiles={onAddFiles} onMove={this.move} setIsDragging={this.isDragging} handleContextMenuClick={this.props.handleContextMenuClick} + isMfs={filesIsMfs} translucent={isDragging || (isOver && canDrop)} - name='..' - focused={focused === '..'} + focused={focused === upperDir.name} cantDrag cantSelect {...upperDir} /> @@ -341,7 +337,9 @@ export class FilesList extends React.Component {
{ this.filesRefs[files[index].name] = r }} + isMfs={filesIsMfs} name={files[index].name} onSelect={this.toggleOne} onNavigate={() => onNavigate(files[index].path)} @@ -384,7 +382,10 @@ export class FilesList extends React.Component { {t('fileName')} {this.sortByIcon(sorts.BY_NAME)}
-
+
+ { /* Badges */ } +
+
{t('size')} {this.sortByIcon(sorts.BY_SIZE)} @@ -409,7 +410,8 @@ export class FilesList extends React.Component { onRowsRendered={this.onRowsRendered} isScrolling={isScrolling} onScroll={onChildScroll} - scrollTop={scrollTop} /> + scrollTop={scrollTop} + data={files /* NOTE: this is a placebo prop to force the list to re-render */} /> )}
@@ -435,7 +437,8 @@ const dropTarget = { } add() - } + }, + canDrop: props => props.filesIsMfs } const dropCollect = (connect, monitor) => ({ @@ -448,7 +451,10 @@ export const FilesListWithDropTarget = DropTarget(NativeTypes.FILE, dropTarget, export default connect( 'selectNavbarWidth', + 'selectPins', 'selectFilesIsFetching', + 'selectFilesSorting', + 'selectFilesPathInfo', 'selectShowLoadingAnimation', FilesListWithDropTarget ) diff --git a/src/files/files-list/FilesList.stories.js b/src/files/files-list/FilesList.stories.js index a121dd2d8..a84d99661 100644 --- a/src/files/files-list/FilesList.stories.js +++ b/src/files/files-list/FilesList.stories.js @@ -11,7 +11,7 @@ import filesListC from './fixtures/list-with-100-files.json' import filesListE from './fixtures/list-with-1000-files.json' import filesListF from './fixtures/list-with-5000-files.json' -storiesOf('Files List', module) +storiesOf('Files/Files List', module) .addDecorator(i18nDecorator) .addDecorator(DndDecorator) .addDecorator(withKnobs) @@ -19,6 +19,7 @@ storiesOf('Files List', module)
+ filesSorting={{ by: 'name', asc: true }} />
)) .add('List with 100 files', () => (
+ filesSorting={{ by: 'name', asc: true }} />
)) .add('List with 1000 files', () => (
+ filesSorting={{ by: 'name', asc: true }} />
)) .add('List with 5000 files', () => (
+ filesSorting={{ by: 'name', asc: true }} />
)) diff --git a/src/files/header/Header.js b/src/files/header/Header.js index 308f6cfbc..b04513782 100644 --- a/src/files/header/Header.js +++ b/src/files/header/Header.js @@ -1,4 +1,5 @@ import React from 'react' +import SimplifyNumber from 'simplify-number' import { connect } from 'redux-bundler-react' import { translate } from 'react-i18next' // Components @@ -7,53 +8,92 @@ import FileInput from '../file-input/FileInput' import Button from '../../components/button/Button' // Icons import GlyphDots from '../../icons/GlyphDots' -import FolderIcon from '../../icons/StrokeFolder' +import GlyphHome from '../../icons/GlyphHome' + +function BarOption ({ children, title, className = '', ...etc }) { + className += ' tc pa3' + + if (etc.onClick) className += ' pointer' + + return ( +
+ {children} + {title} +
+ ) +} class Header extends React.Component { handleContextMenu = (ev) => { - const dotsPosition = this.dotsWrapper.getBoundingClientRect() - this.props.handleContextMenu(ev, 'LEFT', this.props.files, dotsPosition) + const pos = this.dotsWrapper.getBoundingClientRect() + this.props.handleContextMenu(ev, 'TOP', { + ...this.props.files, + pinned: this.props.pins.includes(this.props.files.hash) + }, pos) } render () { const { files, writeFilesProgress, t, - doFilesNavigateTo + pins, + filesPathInfo, + repoNumObjects, + onNavigate } = this.props return ( -
- +
+
+ +
- { files.type === 'directory' - ? ( +
+ + { repoNumObjects ? SimplifyNumber(repoNumObjects, { decimal: 0 }) : 'N/A' } + + + { onNavigate('/pins') }}> + { pins ? SimplifyNumber(pins.length) : '-' } + + + { onNavigate('/files') }}> + + + +
- - -
- ) : ( -
{ this.dotsWrapper = el }} className='ml-auto' style={{ width: '1.5rem' }}> {/* to render correctly in Firefox */} - + { (files && files.type === 'directory' && filesPathInfo.isMfs) + ? + :
{ this.dotsWrapper = el }}> + +
+ }
- )} +
+
) } } export default connect( - 'doFilesNavigateTo', - 'selectFiles', + 'selectPins', + 'selectRepoSize', + 'selectRepoNumObjects', + 'selectFilesPathInfo', 'selectWriteFilesProgress', translate('files')(Header) ) diff --git a/src/files/info-boxes/InfoBoxes.js b/src/files/info-boxes/InfoBoxes.js new file mode 100644 index 000000000..40d8793ca --- /dev/null +++ b/src/files/info-boxes/InfoBoxes.js @@ -0,0 +1,21 @@ +import React from 'react' +import PropTypes from 'prop-types' +import CompanionInfo from './companion-info/CompanionInfo' +import AddFilesInfo from './add-files-info/AddFilesInfo' +import WelcomeInfo from './welcome-info/WelcomeInfo' + +const InfoBoxes = ({ isRoot, isCompanion, filesExist }) => ( +
+ { isRoot && isCompanion && } + { isRoot && !filesExist && !isCompanion && } + { isRoot && !filesExist && } +
+) + +InfoBoxes.propTypes = { + isRoot: PropTypes.bool.isRequired, + isCompanion: PropTypes.bool.isRequired, + filesExist: PropTypes.bool.isRequired +} + +export default InfoBoxes diff --git a/src/files/info-boxes/InfoBoxes.stories.js b/src/files/info-boxes/InfoBoxes.stories.js new file mode 100644 index 000000000..faaea3207 --- /dev/null +++ b/src/files/info-boxes/InfoBoxes.stories.js @@ -0,0 +1,17 @@ +import React from 'react' +import { storiesOf } from '@storybook/react' +import { withKnobs, boolean } from '@storybook/addon-knobs' +import i18n from '../../i18n-decorator' +import InfoBoxes from './InfoBoxes' + +storiesOf('Files/Info Boxes', module) + .addDecorator(i18n) + .addDecorator(withKnobs) + .add('Info Boxes', () => ( +
+ +
+ )) diff --git a/src/files/info-boxes/AddFilesInfo.js b/src/files/info-boxes/add-files-info/AddFilesInfo.js similarity index 73% rename from src/files/info-boxes/AddFilesInfo.js rename to src/files/info-boxes/add-files-info/AddFilesInfo.js index f55cf4678..c0e320124 100644 --- a/src/files/info-boxes/AddFilesInfo.js +++ b/src/files/info-boxes/add-files-info/AddFilesInfo.js @@ -1,7 +1,6 @@ import React from 'react' -import { connect } from 'redux-bundler-react' import { translate, Trans } from 'react-i18next' -import Box from '../../components/box/Box' +import Box from '../../../components/box/Box' const AddFilesInfo = () => (
@@ -13,4 +12,4 @@ const AddFilesInfo = () => (
) -export default connect(translate('files')(AddFilesInfo)) +export default translate('files')(AddFilesInfo) diff --git a/src/files/info-boxes/add-files-info/AddFilesInfo.stories.js b/src/files/info-boxes/add-files-info/AddFilesInfo.stories.js new file mode 100644 index 000000000..be5451f7c --- /dev/null +++ b/src/files/info-boxes/add-files-info/AddFilesInfo.stories.js @@ -0,0 +1,12 @@ +import React from 'react' +import { storiesOf } from '@storybook/react' +import i18n from '../../../i18n-decorator' +import AddFilesInfo from './AddFilesInfo' + +storiesOf('Files/Info Boxes', module) + .addDecorator(i18n) + .add('Add Files', () => ( +
+ +
+ )) diff --git a/src/files/info-boxes/CompanionInfo.js b/src/files/info-boxes/companion-info/CompanionInfo.js similarity index 74% rename from src/files/info-boxes/CompanionInfo.js rename to src/files/info-boxes/companion-info/CompanionInfo.js index 4dce43506..e1366d052 100644 --- a/src/files/info-boxes/CompanionInfo.js +++ b/src/files/info-boxes/companion-info/CompanionInfo.js @@ -1,7 +1,6 @@ import React from 'react' -import { connect } from 'redux-bundler-react' import { translate, Trans } from 'react-i18next' -import Box from '../../components/box/Box' +import Box from '../../../components/box/Box' const CompanionInfo = () => (
@@ -13,4 +12,4 @@ const CompanionInfo = () => (
) -export default connect(translate('files')(CompanionInfo)) +export default translate('files')(CompanionInfo) diff --git a/src/files/info-boxes/companion-info/CompanionInfo.stories.js b/src/files/info-boxes/companion-info/CompanionInfo.stories.js new file mode 100644 index 000000000..53eb278ec --- /dev/null +++ b/src/files/info-boxes/companion-info/CompanionInfo.stories.js @@ -0,0 +1,12 @@ +import React from 'react' +import { storiesOf } from '@storybook/react' +import i18n from '../../../i18n-decorator' +import CompanionInfo from './CompanionInfo' + +storiesOf('Files/Info Boxes', module) + .addDecorator(i18n) + .add('Companion Info', () => ( +
+ +
+ )) diff --git a/src/files/info-boxes/WelcomeInfo.js b/src/files/info-boxes/welcome-info/WelcomeInfo.js similarity index 90% rename from src/files/info-boxes/WelcomeInfo.js rename to src/files/info-boxes/welcome-info/WelcomeInfo.js index 0e9934f01..f1012760f 100644 --- a/src/files/info-boxes/WelcomeInfo.js +++ b/src/files/info-boxes/welcome-info/WelcomeInfo.js @@ -1,8 +1,7 @@ import React from 'react' -import { connect } from 'redux-bundler-react' import { translate, Trans } from 'react-i18next' -import AboutIpfs from '../../components/about-ipfs/AboutIpfs' -import Box from '../../components/box/Box' +import AboutIpfs from '../../../components/about-ipfs/AboutIpfs' +import Box from '../../../components/box/Box' const WelcomeInfo = ({ t }) => (
@@ -35,4 +34,4 @@ const WelcomeInfo = ({ t }) => (
) -export default connect(translate('files')(WelcomeInfo)) +export default translate('files')(WelcomeInfo) diff --git a/src/files/info-boxes/welcome-info/WelcomeInfo.stories.js b/src/files/info-boxes/welcome-info/WelcomeInfo.stories.js new file mode 100644 index 000000000..f528384f7 --- /dev/null +++ b/src/files/info-boxes/welcome-info/WelcomeInfo.stories.js @@ -0,0 +1,12 @@ +import React from 'react' +import { storiesOf } from '@storybook/react' +import i18n from '../../../i18n-decorator' +import WelcomeInfo from './WelcomeInfo' + +storiesOf('Files/Info Boxes', module) + .addDecorator(i18n) + .add('Welcome Info', () => ( +
+ +
+ )) diff --git a/src/files/modals/Modals.js b/src/files/modals/Modals.js index 1df9a9b7b..3d9581549 100644 --- a/src/files/modals/Modals.js +++ b/src/files/modals/Modals.js @@ -1,37 +1,30 @@ import React from 'react' import PropTypes from 'prop-types' -import { connect } from 'redux-bundler-react' import { join } from 'path' import { translate } from 'react-i18next' import Overlay from '../../components/overlay/Overlay' +// Modals import NewFolderModal from './new-folder-modal/NewFolderModal' import ShareModal from './share-modal/ShareModal' import RenameModal from './rename-modal/RenameModal' import DeleteModal from './delete-modal/DeleteModal' - +import AddByPathModal from './add-by-path-modal/AddByPathModal' +// Constants const NEW_FOLDER = 'new_folder' const SHARE = 'share' const RENAME = 'rename' const DELETE = 'delete' +const ADD_BY_PATH = 'add_by_path' export { NEW_FOLDER, SHARE, RENAME, - DELETE + DELETE, + ADD_BY_PATH } class Modals extends React.Component { - static propTypes = { - t: PropTypes.func, - show: PropTypes.string, - files: PropTypes.array, - doFilesMove: PropTypes.func, - doFilesMakeDir: PropTypes.func, - doFilesShareLink: PropTypes.func, - doFilesDelete: PropTypes.func - } - state = { readyToShow: false, rename: { @@ -47,19 +40,22 @@ class Modals extends React.Component { link: '' } - makeDir = (path) => { - const { doFilesMakeDir, root } = this.props + onAddByPath = (path) => { + this.props.onAddByPath(path) + this.leave() + } - doFilesMakeDir(join(root, path)) + makeDir = (path) => { + this.props.onMakeDir(join(this.props.root, path)) this.leave() } rename = (newName) => { const { filename, path } = this.state.rename - const { doFilesMove } = this.props + const { onMove } = this.props if (newName !== '' && newName !== filename) { - doFilesMove([path, path.replace(filename, newName)]) + onMove(path, path.replace(filename, newName)) } this.leave() @@ -68,7 +64,7 @@ class Modals extends React.Component { delete = () => { const { paths } = this.state.delete - this.props.doFilesDelete(paths) + this.props.onDelete(paths) this.leave() } @@ -78,46 +74,52 @@ class Modals extends React.Component { } componentDidUpdate (prev) { - const { show, files, t, doFilesShareLink } = this.props + const { show, files, t, onShareLink } = this.props if (show === prev.show) { return } - if (show === SHARE) { - this.setState({ - link: t('generating'), - readyToShow: true - }) - - doFilesShareLink(files).then(link => this.setState({ link })) - } else if (show === RENAME) { - const file = files[0] - - this.setState({ - readyToShow: true, - rename: { - folder: file.type === 'directory', - path: file.path, - filename: file.path.split('/').pop() - } - }) - } else if (show === DELETE) { - let filesCount = 0 - let foldersCount = 0 - - files.forEach(file => file.type === 'file' ? filesCount++ : foldersCount++) - - this.setState({ - readyToShow: true, - delete: { - files: filesCount, - folders: foldersCount, - paths: files.map(f => f.path) - } - }) - } else { - this.setState({ readyToShow: true }) + switch (show) { + case SHARE: + this.setState({ + link: t('generating'), + readyToShow: true + }) + + onShareLink(files).then(link => this.setState({ link })) + break + + case RENAME: + const file = files[0] + + this.setState({ + readyToShow: true, + rename: { + folder: file.type === 'directory', + path: file.path, + filename: file.path.split('/').pop() + } + }) + break + + case DELETE: + let filesCount = 0 + let foldersCount = 0 + + files.forEach(file => file.type === 'file' ? filesCount++ : foldersCount++) + + this.setState({ + readyToShow: true, + delete: { + files: filesCount, + folders: foldersCount, + paths: files.map(f => f.path) + } + }) + break + default: + this.setState({ readyToShow: true }) } } @@ -156,15 +158,27 @@ class Modals extends React.Component { onCancel={this.leave} onDelete={this.delete} /> + + + +
) } } -export default connect( - 'doFilesMove', - 'doFilesMakeDir', - 'doFilesShareLink', - 'doFilesDelete', - translate('files')(Modals) -) +Modals.propTypes = { + t: PropTypes.func.isRequired, + show: PropTypes.string, + files: PropTypes.array, + onAddByPath: PropTypes.func.isRequired, + onMove: PropTypes.func.isRequired, + onMakeDir: PropTypes.func.isRequired, + onShareLink: PropTypes.func.isRequired, + onDelete: PropTypes.func.isRequired +} + +export default translate('files')(Modals) diff --git a/src/files/file-input/ByPathModal.js b/src/files/modals/add-by-path-modal/AddByPathModal.js similarity index 91% rename from src/files/file-input/ByPathModal.js rename to src/files/modals/add-by-path-modal/AddByPathModal.js index f820ce07b..b7d477637 100644 --- a/src/files/file-input/ByPathModal.js +++ b/src/files/modals/add-by-path-modal/AddByPathModal.js @@ -2,8 +2,8 @@ import React from 'react' import PropTypes from 'prop-types' import isIPFS from 'is-ipfs' import { translate } from 'react-i18next' -import Icon from '../../icons/StrokeDecentralization' -import TextInputModal from '../../components/text-input-modal/TextInputModal' +import Icon from '../../../icons/StrokeDecentralization' +import TextInputModal from '../../../components/text-input-modal/TextInputModal' function ByPathModal ({ t, tReady, onCancel, onSubmit, className, ...props }) { const validatePath = (p) => { diff --git a/src/files/modals/add-by-path-modal/AddByPathModal.stories.js b/src/files/modals/add-by-path-modal/AddByPathModal.stories.js new file mode 100644 index 000000000..8c5864185 --- /dev/null +++ b/src/files/modals/add-by-path-modal/AddByPathModal.stories.js @@ -0,0 +1,17 @@ +import React from 'react' +import { storiesOf } from '@storybook/react' +import { action } from '@storybook/addon-actions' +import i18n from '../../../i18n-decorator' +import AddByPathModal from './AddByPathModal' + +storiesOf('Files/Modals', module) + .addDecorator(i18n) + .add('Add By Path', () => ( +
+ +
+ )) diff --git a/src/files/modals/delete-modal/DeleteModal.stories.js b/src/files/modals/delete-modal/DeleteModal.stories.js index b8c75dcf4..9939069eb 100644 --- a/src/files/modals/delete-modal/DeleteModal.stories.js +++ b/src/files/modals/delete-modal/DeleteModal.stories.js @@ -4,9 +4,9 @@ import { action } from '@storybook/addon-actions' import i18n from '../../../i18n-decorator' import DeleteModal from './DeleteModal' -storiesOf('Files', module) +storiesOf('Files/Modals', module) .addDecorator(i18n) - .add('Delete Modal', () => ( + .add('Delete', () => (
( +
+ +
+ )) diff --git a/src/files/modals/rename-modal/RenameModal.stories.js b/src/files/modals/rename-modal/RenameModal.stories.js index 4e2ed4b2b..1ac92df7c 100644 --- a/src/files/modals/rename-modal/RenameModal.stories.js +++ b/src/files/modals/rename-modal/RenameModal.stories.js @@ -4,9 +4,9 @@ import { action } from '@storybook/addon-actions' import i18n from '../../../i18n-decorator' import RenameModal from './RenameModal' -storiesOf('Files', module) +storiesOf('Files/Modals', module) .addDecorator(i18n) - .add('Rename Modal', () => ( + .add('Rename', () => (
( + .add('Share', () => (
- +
)) diff --git a/src/files/selected-actions/SelectedActions.js b/src/files/selected-actions/SelectedActions.js index f1683ae3a..b94b6f04d 100644 --- a/src/files/selected-actions/SelectedActions.js +++ b/src/files/selected-actions/SelectedActions.js @@ -31,6 +31,11 @@ const styles = { } } +const classes = { + svg: (v) => v ? 'w3 pointer hover-fill-navy-muted' : 'w3', + action: (v) => v ? 'pointer' : 'disabled o-50' +} + class SelectedActions extends React.Component { static propTypes = { count: PropTypes.number.isRequired, @@ -43,7 +48,8 @@ class SelectedActions extends React.Component { inspect: PropTypes.func.isRequired, downloadProgress: PropTypes.number, t: PropTypes.func.isRequired, - tReady: PropTypes.bool.isRequired + tReady: PropTypes.bool.isRequired, + isMfs: PropTypes.bool.isRequired } static defaultActions = { @@ -80,16 +86,14 @@ class SelectedActions extends React.Component { } render () { - let { t, tReady, count, size, unselect, remove, share, download, downloadProgress, rename, inspect, className, style, ...props } = this.props + let { t, tReady, count, size, unselect, remove, share, download, downloadProgress, rename, inspect, className, style, isMfs, ...props } = this.props + + const isSingle = count === 1 - let singleFileAction = 'disabled o-50' let singleFileTooltip = { title: t('individualFilesOnly') } - let singleSvgClass = 'w3' if (count === 1) { - singleFileAction = 'pointer' singleFileTooltip = {} - singleSvgClass = 'w3 pointer hover-fill-navy-muted' } return ( @@ -115,16 +119,16 @@ class SelectedActions extends React.Component {

{this.downloadText}

-
- +
+

{t('actions.delete')}

-
- +
+

{t('actions.inspect')}

-
- +
+

{t('actions.rename')}

diff --git a/src/icons/GlyphHome.js b/src/icons/GlyphHome.js index 303ea48e8..b55a0709a 100644 --- a/src/icons/GlyphHome.js +++ b/src/icons/GlyphHome.js @@ -1,8 +1,8 @@ import React from 'react' const GlyphHome = props => ( - - + + ) diff --git a/src/icons/GlyphNewFolder.js b/src/icons/GlyphNewFolder.js new file mode 100644 index 000000000..9d28d385f --- /dev/null +++ b/src/icons/GlyphNewFolder.js @@ -0,0 +1,46 @@ +import React from 'react' + +const GlyphNewFolder = props => ( + + + + + + + + + + + + + + + + + + + + + +) + +export default GlyphNewFolder diff --git a/src/icons/GlyphSettings.js b/src/icons/GlyphSettings.js index 9b57a0169..effa2abff 100644 --- a/src/icons/GlyphSettings.js +++ b/src/icons/GlyphSettings.js @@ -2,7 +2,7 @@ import React from 'react' const GlyphSettings = props => ( - + ) diff --git a/src/icons/StrokeCube.js b/src/icons/StrokeCube.js index 5d4a04adc..93b553275 100644 --- a/src/icons/StrokeCube.js +++ b/src/icons/StrokeCube.js @@ -2,7 +2,7 @@ import React from 'react' const StrokeCube = props => ( - + ) diff --git a/src/icons/StrokeIpld.js b/src/icons/StrokeIpld.js index f7ffd0650..c76ad9f74 100644 --- a/src/icons/StrokeIpld.js +++ b/src/icons/StrokeIpld.js @@ -2,7 +2,7 @@ import React from 'react' const StrokeIpld = props => ( - + ) diff --git a/src/icons/StrokeMarketing.js b/src/icons/StrokeMarketing.js index 7ab8c8455..1b2001a9b 100644 --- a/src/icons/StrokeMarketing.js +++ b/src/icons/StrokeMarketing.js @@ -2,8 +2,8 @@ import React from 'react' const StrokeMarketing = props => ( - - + + ) diff --git a/src/icons/StrokeNewFolder.js b/src/icons/StrokeNewFolder.js new file mode 100644 index 000000000..be6da7b2f --- /dev/null +++ b/src/icons/StrokeNewFolder.js @@ -0,0 +1,9 @@ +import React from 'react' + +const StrokeNewFolder = props => ( + + + +) + +export default StrokeNewFolder diff --git a/src/icons/StrokeSettings.js b/src/icons/StrokeSettings.js index 21593eca6..a782f3904 100644 --- a/src/icons/StrokeSettings.js +++ b/src/icons/StrokeSettings.js @@ -2,9 +2,9 @@ import React from 'react' const StrokeSettings = props => ( - - - + + + ) diff --git a/src/navigation/NavBar.js b/src/navigation/NavBar.js index 3d4cb0ac9..c85b82bed 100644 --- a/src/navigation/NavBar.js +++ b/src/navigation/NavBar.js @@ -65,7 +65,7 @@ export const NavBar = ({ t, width, open, onToggle }) => {