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.0>"
},
"addFilesInfo": "<0>Add files to your local IPFS node by clicking the <1>Add to IPFS1> button above.0>",
- "companionInfo": "<0>As you are using <1>IPFS Companion1>, the files view is limited to files added while using the extension.0>"
+ "companionInfo": "<0>As you are using <1>IPFS Companion1>, the files view is limited to files added while using the extension.0>",
+ "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 (
{children}
diff --git a/src/files/FilesPage.js b/src/files/FilesPage.js
index 91115db90..8b68043ff 100644
--- a/src/files/FilesPage.js
+++ b/src/files/FilesPage.js
@@ -6,13 +6,12 @@ import { connect } from 'redux-bundler-react'
import downloadFile from './download-file'
import { translate } from 'react-i18next'
// Components
-import FilesList from './files-list/FilesList'
-import FilePreview from './file-preview/FilePreview'
import ContextMenu from './context-menu/ContextMenu'
-import AddFilesInfo from './info-boxes/AddFilesInfo'
-import CompanionInfo from './info-boxes/CompanionInfo'
-import WelcomeInfo from './info-boxes/WelcomeInfo'
-import Modals, { DELETE, NEW_FOLDER, SHARE, RENAME } from './modals/Modals'
+import InfoBoxes from './info-boxes/InfoBoxes'
+import FilePreview from './file-preview/FilePreview'
+import FilesList from './files-list/FilesList'
+// Icons
+import Modals, { DELETE, NEW_FOLDER, SHARE, RENAME, ADD_BY_PATH } from './modals/Modals'
import Header from './header/Header'
const defaultState = {
@@ -36,43 +35,22 @@ class FilesPage extends React.Component {
this.contextMenuRef = React.createRef()
}
- static propTypes = {
- ipfsConnected: PropTypes.bool,
- ipfsProvider: PropTypes.string,
- files: PropTypes.object,
- filesErrors: PropTypes.array,
- filesPathFromHash: PropTypes.string,
- filesSorting: PropTypes.object.isRequired,
- gatewayUrl: PropTypes.string.isRequired,
- doUpdateHash: PropTypes.func.isRequired,
- doFilesDelete: PropTypes.func.isRequired,
- doFilesMove: PropTypes.func.isRequired,
- doFilesWrite: PropTypes.func.isRequired,
- doFilesAddPath: PropTypes.func.isRequired,
- doFilesDownloadLink: PropTypes.func.isRequired,
- doFilesMakeDir: PropTypes.func.isRequired,
- doFilesUpdateSorting: PropTypes.func.isRequired,
- t: PropTypes.func.isRequired,
- tReady: PropTypes.bool.isRequired
- }
-
state = defaultState
- resetState = (field) => this.setState({ [field]: defaultState[field] })
-
componentDidMount () {
this.props.doFilesFetch()
+ this.props.doPinsFetch()
}
componentDidUpdate (prev) {
- const { filesPathFromHash } = this.props
+ const { filesPathInfo } = this.props
- if (prev.files === null || !prev.ipfsConnected || filesPathFromHash !== prev.filesPathFromHash) {
+ if (prev.files === null || !prev.ipfsConnected || filesPathInfo.path !== prev.filesPathInfo.path) {
this.props.doFilesFetch()
}
}
- download = async (files) => {
+ onDownload = async (files) => {
const { doFilesDownloadLink } = this.props
const { downloadProgress, downloadAbort } = this.state
@@ -87,52 +65,31 @@ class FilesPage extends React.Component {
this.setState({ downloadAbort: abort })
}
- add = (raw, root = '') => {
- const { files, doFilesWrite } = this.props
-
+ onAddFiles = (raw, root = '') => {
if (root === '') {
- root = files.path
- }
-
- doFilesWrite(root, raw)
- }
-
- addByPath = (path) => {
- const { doFilesAddPath, files } = this.props
- doFilesAddPath(files.path, path)
- }
-
- inspect = (hash) => {
- const { doUpdateHash } = this.props
-
- if (Array.isArray(hash)) {
- hash = hash[0].hash
+ root = this.props.files.path
}
- doUpdateHash(`/explore/ipfs/${hash}`)
- }
-
- showNewFolderModal = () => {
- this.setState({ modals: { show: NEW_FOLDER } })
+ this.props.doFilesWrite(raw, root)
}
- showShareModal = (files) => {
- this.setState({ modals: { show: SHARE, files } })
+ onAddByPath = (path) => {
+ this.props.doFilesAddPath(this.props.files.path, path)
}
- showRenameModal = (files) => {
- this.setState({ modals: { show: RENAME, files } })
+ onInspect = (hash) => {
+ this.props.doUpdateHash(`/explore/ipfs/${hash}`)
}
- showDeleteModal = (files) => {
- this.setState({ modals: { show: DELETE, files } })
+ showModal = (modal, files = null) => {
+ this.setState({ modals: { show: modal, files: files } })
}
- resetModals = () => {
+ hideModal = () => {
this.setState({ modals: { } })
}
- handleContextMenu = (ev, clickType, file, dotsPosition, fromHeader = false) => {
+ handleContextMenu = (ev, clickType, file, pos) => {
// This is needed to disable the native OS right-click menu
// and deal with the clicking on the ContextMenu options
if (ev !== undefined && typeof ev !== 'string') {
@@ -146,17 +103,20 @@ class FilesPage extends React.Component {
let translateX = 0
let translateY = 0
- if (clickType === 'RIGHT') {
- const rightPadding = window.innerWidth - ctxMenu.parentNode.getBoundingClientRect().right
- translateX = (window.innerWidth - ev.clientX) - rightPadding - 20
- translateY = (ctxMenuPosition.y + ctxMenuPosition.height / 2) - ev.clientY - 10
- } else {
- translateX = 1
- translateY = (ctxMenuPosition.y + ctxMenuPosition.height / 2) - (dotsPosition && dotsPosition.y) - 30
- }
-
- if (fromHeader) {
- translateX = -8
+ switch (clickType) {
+ case 'RIGHT':
+ const rightPadding = window.innerWidth - ctxMenu.parentNode.getBoundingClientRect().right
+ translateX = (window.innerWidth - ev.clientX) - rightPadding - 20
+ translateY = (ctxMenuPosition.y + ctxMenuPosition.height / 2) - ev.clientY - 10
+ break
+ case 'TOP':
+ const pagePositions = ctxMenu.parentNode.getBoundingClientRect()
+ translateX = pagePositions.right - pos.right
+ translateY = -(pos.bottom - pagePositions.top + 11)
+ break
+ default:
+ translateX = 1
+ translateY = (ctxMenuPosition.y + ctxMenuPosition.height / 2) - (pos && pos.y) - 30
}
this.setState({
@@ -169,22 +129,65 @@ class FilesPage extends React.Component {
})
}
- render () {
- const {
- ipfsProvider, files, filesSorting: sort, t,
- doFilesMove, doFilesNavigateTo, doFilesUpdateSorting
- } = this.props
+ get mainView () {
+ const { files } = this.props
- const { contextMenu } = this.state
+ if (!files) {
+ return (
)
+ }
- const isCompanion = ipfsProvider === 'window.ipfs'
- const filesExist = files && files.content && files.content.length
- const isRoot = files && files.path === '/'
+ if (files.type !== 'directory') {
+ return (
+
+ )
+ }
+
+ return (
+ this.showModal(SHARE, files)}
+ onRename={(files) => this.showModal(RENAME, files)}
+ onDelete={(files) => this.showModal(DELETE, files)}
+ onInspect={this.onInspect}
+ onDownload={this.onDownload}
+ onAddFiles={this.onAddFiles}
+ onNavigate={this.props.doFilesNavigateTo}
+ onMove={this.props.doFilesMove}
+ handleContextMenuClick={this.handleContextMenu} />
+ )
+ }
+
+ get title () {
+ const { filesPathInfo, t } = this.props
+ const parts = []
+
+ if (filesPathInfo) {
+ parts.push(filesPathInfo.realPath)
+ }
+
+ if (filesPathInfo.isMfs) {
+ parts.push(t('files'))
+ } else if (filesPathInfo.isPins) {
+ parts.push(t('pins'))
+ }
+
+ parts.push('IPFS')
+ return parts.join(' - ')
+ }
+
+ render () {
+ const { files, filesPathInfo } = this.props
+ const { contextMenu } = this.state
return (
- {t('title')} - IPFS
+ {this.title}
this.showShareModal([contextMenu.file])}
- onDelete={() => this.showDeleteModal([contextMenu.file])}
- onRename={() => this.showRenameModal([contextMenu.file])}
- onInspect={() => this.inspect([contextMenu.file])}
- onDownload={() => this.download([contextMenu.file])}
- hash={contextMenu.file && contextMenu.file.hash} />
-
- { files &&
-
-
this.handleContextMenu(...args, true)} />
-
- { isRoot && isCompanion && }
-
- { isRoot && !filesExist && !isCompanion && }
-
- { isRoot && !filesExist && }
-
- { files.type === 'directory'
- ?
- : }
-
- }
-
-
+ hash={contextMenu.file && contextMenu.file.hash}
+ onShare={() => this.showModal(SHARE, [contextMenu.file])}
+ onDelete={() => this.showModal(DELETE, [contextMenu.file])}
+ onRename={() => this.showModal(RENAME, [contextMenu.file])}
+ onInspect={() => this.onInspect(contextMenu.file.hash)}
+ onDownload={() => this.onDownload([contextMenu.file])}
+ onPin={() => this.props.doFilesPin(contextMenu.file.hash)}
+ onUnpin={() => this.props.doFilesUnpin(contextMenu.file.hash)} />
+
+ this.showModal(ADD_BY_PATH, files)}
+ onNewFolder={(files) => this.showModal(NEW_FOLDER, files)}
+ handleContextMenu={(...args) => this.handleContextMenu(...args, true)} />
+
+ { this.mainView }
+
+
+
+
)
}
}
+FilesPage.propTypes = {
+ t: PropTypes.func.isRequired,
+ tReady: PropTypes.bool.isRequired,
+ ipfsConnected: PropTypes.bool,
+ ipfsProvider: PropTypes.string,
+ files: PropTypes.object,
+ filesPathInfo: PropTypes.object,
+ doUpdateHash: PropTypes.func.isRequired,
+ doPinsFetch: PropTypes.func.isRequired,
+ doFilesFetch: PropTypes.func.isRequired,
+ doFilesMove: PropTypes.func.isRequired,
+ doFilesMakeDir: PropTypes.func.isRequired,
+ doFilesShareLink: PropTypes.func.isRequired,
+ doFilesDelete: PropTypes.func.isRequired,
+ doFilesAddPath: PropTypes.func.isRequired,
+ doFilesNavigateTo: PropTypes.func.isRequired,
+ doFilesPin: PropTypes.func.isRequired,
+ doFilesUnpin: PropTypes.func.isRequired,
+ doFilesUpdateSorting: PropTypes.func.isRequired,
+ doFilesWrite: PropTypes.func.isRequired,
+ doFilesDownloadLink: PropTypes.func.isRequired
+}
+
export default connect(
'selectIpfsProvider',
'selectIpfsConnected',
+ 'selectFiles',
+ 'selectFilesPathInfo',
'doUpdateHash',
- 'doFilesDelete',
+ 'doPinsFetch',
+ 'doFilesFetch',
'doFilesMove',
- 'doFilesWrite',
- 'doFilesAddPath',
- 'doFilesDownloadLink',
'doFilesMakeDir',
- 'doFilesFetch',
+ 'doFilesShareLink',
+ 'doFilesDelete',
+ 'doFilesAddPath',
'doFilesNavigateTo',
+ 'doFilesPin',
+ 'doFilesUnpin',
'doFilesUpdateSorting',
- 'selectFiles',
- 'selectGatewayUrl',
- 'selectFilesPathFromHash',
- 'selectFilesSorting',
+ 'doFilesWrite',
+ 'doFilesDownloadLink',
translate('files')(FilesPage)
)
diff --git a/src/files/breadcrumbs/Breadcrumbs.js b/src/files/breadcrumbs/Breadcrumbs.js
index d287c37e3..55f29e2f4 100644
--- a/src/files/breadcrumbs/Breadcrumbs.js
+++ b/src/files/breadcrumbs/Breadcrumbs.js
@@ -2,12 +2,12 @@ import React from 'react'
import PropTypes from 'prop-types'
import { translate } from 'react-i18next'
-function makeBread (root, t) {
+function makeBread (root) {
if (root.endsWith('/')) {
root = root.substring(0, root.length - 1)
}
- let parts = root.split('/').map(part => {
+ const parts = root.split('/').map(part => {
return {
name: part,
path: part
@@ -15,37 +15,91 @@ function makeBread (root, t) {
})
for (let i = 1; i < parts.length; i++) {
+ const name = parts[i].name
+
parts[i] = {
- name: parts[i].name,
+ name: name,
path: parts[i - 1].path + '/' + parts[i].path
}
+
+ if (name.length >= 30) {
+ parts[i].realName = name
+ parts[i].name = `${name.substring(0, 4)}...${name.substring(name.length - 4, name.length)}`
+ }
}
- parts[0].name = t('home')
- parts[0].path = '/'
+ parts.shift()
+ parts[0].disabled = true
+ parts[parts.length - 1].last = true
return parts
}
-function Breadcrumbs ({ t, tReady, path, onClick, className = '', ...props }) {
- const cls = `Breadcrumbs sans-serif ${className}`
- const bread = makeBread(path, t)
-
- const res = bread.map((link, index) => ([
- ,
- /
- ]))
-
- res[res.length - 1].pop()
-
- return (
- {res}
- )
+class Breadcrumbs extends React.Component {
+ state = {
+ overflows: false
+ }
+
+ componentDidUpdate (_, prevState) {
+ const a = this.anchors
+ const overflows = a ? (a.offsetHeight < a.scrollHeight || a.offsetWidth < a.scrollWidth) : false
+
+ if (prevState.overflows !== overflows) {
+ this.setState({ overflows })
+ }
+ }
+
+ render () {
+ const { t, tReady, path, onClick, className = '', ...props } = this.props
+
+ const cls = `Breadcrumbs flex items-center sans-serif overflow-hidden ${className}`
+ const bread = makeBread(path)
+ const root = bread[0]
+
+ if (root.name === 'files' || root.name === 'pins') {
+ bread.shift()
+ }
+
+ const res = bread.map((link, index) => ([
+
+ { link.disabled
+ ? {link.name}
+ /* eslint-disable-next-line jsx-a11y/anchor-is-valid */
+ : onClick(link.path)}>
+ {link.name}
+
+ }
+ ,
+ /* eslint-disable-next-line jsx-a11y/anchor-is-valid */
+ /
+ ]))
+
+ if (res.length === 0) {
+ /* eslint-disable-next-line jsx-a11y/anchor-is-valid */
+ res.push(/ )
+ }
+
+ res.reverse()
+
+ return (
+
+ { (root.name === 'files' || root.name === 'pins') &&
+ /* eslint-disable-next-line jsx-a11y/anchor-is-valid */
+ onClick(root.path)}
+ className='f7 pointer pa1 bg-navy br2 mr2 white'>
+ {t(root.name)}
+
+ }
+
+ { this.anchors = el }} style={{ direction: 'rtl' }}>
+
+ {res}
+
+
+ )
+ }
}
Breadcrumbs.propTypes = {
diff --git a/src/files/breadcrumbs/Breadcrumbs.stories.js b/src/files/breadcrumbs/Breadcrumbs.stories.js
index a0d4b5335..e28d86dd3 100644
--- a/src/files/breadcrumbs/Breadcrumbs.stories.js
+++ b/src/files/breadcrumbs/Breadcrumbs.stories.js
@@ -5,13 +5,17 @@ import { checkA11y } from '@storybook/addon-a11y'
import i18n from '../../i18n-decorator'
import Breadcrumbs from './Breadcrumbs'
-storiesOf('Files', module)
+storiesOf('Files/Header', module)
.addDecorator(checkA11y)
.addDecorator(i18n)
.add('Breadcrumbs', () => (
+
+
))
diff --git a/src/files/context-menu/ContextMenu.js b/src/files/context-menu/ContextMenu.js
index fe6575b88..31165ebab 100644
--- a/src/files/context-menu/ContextMenu.js
+++ b/src/files/context-menu/ContextMenu.js
@@ -10,39 +10,9 @@ import StrokePencil from '../../icons/StrokePencil'
import StrokeIpld from '../../icons/StrokeIpld'
import StrokeTrash from '../../icons/StrokeTrash'
import StrokeDownload from '../../icons/StrokeDownload'
+import StrokePin from '../../icons/StrokePin'
class ContextMenu extends React.Component {
- static propTypes = {
- isOpen: PropTypes.bool,
- isUpperDir: PropTypes.bool,
- handleClick: PropTypes.func,
- translateX: PropTypes.number,
- translateY: PropTypes.number,
- left: PropTypes.number,
- showDots: PropTypes.bool,
- onDelete: PropTypes.func,
- onRename: PropTypes.func,
- onDownload: PropTypes.func,
- onInspect: PropTypes.func,
- onShare: PropTypes.func,
- hash: PropTypes.string,
- className: PropTypes.string,
- t: PropTypes.func.isRequired,
- tReady: PropTypes.bool.isRequired
- }
-
- static defaultProps = {
- isOpen: false,
- isUpperDir: false,
- top: 0,
- left: 0,
- right: 'auto',
- translateX: 0,
- translateY: 0,
- showDots: true,
- className: ''
- }
-
state = {
dropdown: false
}
@@ -53,7 +23,11 @@ class ContextMenu extends React.Component {
}
render () {
- const { t, onRename, onDelete, onDownload, onInspect, onShare, translateX, translateY, className, showDots, isUpperDir } = this.props
+ const {
+ t, onRename, onDelete, onDownload, onInspect, onShare,
+ translateX, translateY, className, showDots,
+ isUpperDir, isMfs, pinned
+ } = this.props
return (
@@ -66,13 +40,13 @@ class ContextMenu extends React.Component {
translateY={-translateY}
open={this.props.isOpen}
onDismiss={this.props.handleClick}>
- { !isUpperDir && onDelete &&
+ { !isUpperDir && isMfs && onDelete &&
{t('actions.delete')}
}
- { !isUpperDir && onRename &&
+ { !isUpperDir && isMfs && onRename &&
{t('actions.rename')}
@@ -90,6 +64,10 @@ class ContextMenu extends React.Component {
{t('actions.inspect')}
}
+
+
+ { pinned ? t('actions.unpin') : t('actions.pin') }
+
@@ -108,4 +86,38 @@ class ContextMenu extends React.Component {
}
}
+ContextMenu.propTypes = {
+ isMfs: PropTypes.bool.isRequired,
+ isOpen: PropTypes.bool.isRequired,
+ hash: PropTypes.string,
+ isUpperDir: PropTypes.bool,
+ pinned: PropTypes.bool,
+ handleClick: PropTypes.func,
+ translateX: PropTypes.number.isRequired,
+ translateY: PropTypes.number.isRequired,
+ left: PropTypes.number.isRequired,
+ showDots: PropTypes.bool,
+ onDelete: PropTypes.func,
+ onRename: PropTypes.func,
+ onDownload: PropTypes.func,
+ onInspect: PropTypes.func,
+ onShare: PropTypes.func,
+ className: PropTypes.string,
+ t: PropTypes.func.isRequired,
+ tReady: PropTypes.bool.isRequired
+}
+
+ContextMenu.defaultProps = {
+ isMfs: false,
+ isOpen: false,
+ isUpperDir: false,
+ top: 0,
+ left: 0,
+ right: 'auto',
+ translateX: 0,
+ translateY: 0,
+ showDots: true,
+ className: ''
+}
+
export default translate('files')(ContextMenu)
diff --git a/src/files/context-menu/ContextMenu.stories.js b/src/files/context-menu/ContextMenu.stories.js
index 46382baf0..ed811c775 100644
--- a/src/files/context-menu/ContextMenu.stories.js
+++ b/src/files/context-menu/ContextMenu.stories.js
@@ -14,6 +14,9 @@ storiesOf('Files', module)
diff --git a/src/files/errors/Errors.js b/src/files/errors/Errors.js
deleted file mode 100644
index d928bc1b1..000000000
--- a/src/files/errors/Errors.js
+++ /dev/null
@@ -1,34 +0,0 @@
-import React from 'react'
-import { actions } from '../../bundles/files'
-import ErrorIcon from '../../icons/GlyphSmallCancel'
-
-const names = {
- [actions.FETCH]: 'fetching',
- [actions.MOVE]: 'moving',
- [actions.COPY]: 'copying',
- [actions.DELETE]: 'deleting',
- [actions.MAKE_DIR]: 'creating a folder',
- [actions.WRITE]: 'adding a file',
- [actions.DOWNLOAD_LINK]: 'downloading a file',
- [actions.ADD_BY_PATH]: 'adding a path'
-}
-
-export default function Errors ({ errors = [], onDismiss }) {
- if (errors.length === 0) {
- return null
- }
-
- return (
-
-
- {errors.map((e, index) => (
-
-
An error occurred while {names[e.type]} .
-
- {e.error.toString()}
-
-
- ))}
-
- )
-}
diff --git a/src/files/explore-form/FilesExploreForm.js b/src/files/explore-form/FilesExploreForm.js
new file mode 100644
index 000000000..ae43f2c93
--- /dev/null
+++ b/src/files/explore-form/FilesExploreForm.js
@@ -0,0 +1,92 @@
+import React from 'react'
+import isIPFS from 'is-ipfs'
+import PropTypes from 'prop-types'
+import { translate } from 'react-i18next'
+import StrokeIpld from '../../icons/StrokeFolder'
+import Button from '../../components/button/Button'
+
+class FilesExploreForm extends React.Component {
+ constructor (props) {
+ super(props)
+ this.state = {
+ path: ''
+ }
+ this.onChange = this.onChange.bind(this)
+ this.onSubmit = this.onSubmit.bind(this)
+ }
+
+ onSubmit (evt) {
+ evt.preventDefault()
+
+ if (this.canBrowse) {
+ let path = this.path
+
+ if (isIPFS.cid(path)) {
+ path = `/ipfs/${path}`
+ }
+
+ this.props.onNavigate(path)
+ this.setState({ path: '' })
+ }
+ }
+
+ onChange (evt) {
+ const path = evt.target.value
+ this.setState({ path })
+ }
+
+ get path () {
+ return this.state.path.trim()
+ }
+
+ get isValid () {
+ return isIPFS.cid(this.path) || isIPFS.path(this.path)
+ }
+
+ get inputClass () {
+ if (this.path === '') {
+ return 'focus-outline'
+ }
+
+ if (this.isValid) {
+ return 'b--green-muted focus-outline-green'
+ } else {
+ return 'b--red-muted focus-outline-red'
+ }
+ }
+
+ get canBrowse () {
+ return this.path !== '' && this.isValid
+ }
+
+ render () {
+ const { t } = this.props
+ 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 (
-
+
- { sending ? `${progress.toFixed(0)}%` : (+ {t('addToIPFS')} ) }
+ { sending ? `${progress.toFixed(0)}%` : (+ {t('addToIPFS')} ) }
{ sending &&
}
@@ -28,17 +27,8 @@ const AddButton = translate('files')(({ progress = null, t, tReady, i18n, lng, .
})
class FileInput extends React.Component {
- static propTypes = {
- onAddFiles: PropTypes.func.isRequired,
- onAddByPath: PropTypes.func.isRequired,
- addProgress: PropTypes.number,
- t: PropTypes.func.isRequired,
- tReady: PropTypes.bool.isRequired
- }
-
state = {
dropdown: false,
- byPathModal: false,
force100: false
}
@@ -46,37 +36,28 @@ class FileInput extends React.Component {
this.setState(s => ({ dropdown: !s.dropdown }))
}
- toggleModal = (which) => () => {
- if (!this.state[`${which}Modal`]) {
- this.toggleDropdown()
- }
-
- this.setState(s => {
- s[`${which}Modal`] = !s[`${which}Modal`]
- return s
- })
- }
+ onAddFolder = async () => {
+ const { isIpfsDesktop, doDesktopSelectDirectory, onAddFiles } = this.props
- handleAddFolder = async () => {
this.toggleDropdown()
- if (!this.props.isIpfsDesktop) {
+ if (!isIpfsDesktop) {
return this.folderInput.click()
}
- const files = await this.props.doDesktopSelectDirectory()
+ const files = await doDesktopSelectDirectory()
if (files) {
- this.props.onAddFiles(files)
+ onAddFiles(files)
}
}
- handleAddFile = async () => {
+ onAddFile = async () => {
this.toggleDropdown()
return this.filesInput.click()
}
componentDidUpdate (prev) {
- if (this.props.addProgress === 100 && prev.addProgress !== 100) {
+ if (this.props.writeFilesProgress === 100 && prev.writeFilesProgress !== 100) {
this.setState({ force100: true })
setTimeout(() => {
this.setState({ force100: false })
@@ -89,9 +70,14 @@ class FileInput extends React.Component {
input.value = null
}
- onAddByPath = (path) => {
- this.props.onAddByPath(path)
- this.toggleModal('byPath')()
+ onAddByPath = () => {
+ this.props.onAddByPath()
+ this.toggleDropdown()
+ }
+
+ onNewFolder = () => {
+ this.props.onNewFolder()
+ this.toggleDropdown()
}
render () {
@@ -103,23 +89,27 @@ class FileInput extends React.Component {
return (
-
+
-
+
{t('addFile')}
-
+
{t('addFolder')}
-
+
{t('addByPath')}
+
+
+ {t('newFolder')}
+
@@ -137,20 +127,25 @@ class FileInput extends React.Component {
webkitdirectory='true'
ref={el => { this.folderInput = el }}
onChange={this.onInputChange(this.folderInput)} />
-
-
-
-
)
}
}
+FileInput.propTypes = {
+ t: PropTypes.func.isRequired,
+ tReady: PropTypes.bool.isRequired,
+ onAddFiles: PropTypes.func.isRequired,
+ onAddByPath: PropTypes.func.isRequired,
+ onNewFolder: PropTypes.func.isRequired,
+ writeFilesProgress: PropTypes.number,
+ isIpfsDesktop: PropTypes.bool.isRequired,
+ doDesktopSelectDirectory: PropTypes.func
+}
+
export default connect(
'selectIsIpfsDesktop',
+ 'selectWriteFilesProgress',
'doDesktopSelectDirectory',
translate('files')(FileInput)
)
diff --git a/src/files/file-preview/FilePreview.js b/src/files/file-preview/FilePreview.js
index f7618156e..e78a1dfa6 100644
--- a/src/files/file-preview/FilePreview.js
+++ b/src/files/file-preview/FilePreview.js
@@ -1,22 +1,12 @@
import React from 'react'
import PropTypes from 'prop-types'
+import { connect } from 'redux-bundler-react'
import isBinary from 'is-binary'
import { Trans, translate } from 'react-i18next'
import typeFromExt from '../type-from-ext'
import ComponentLoader from '../../loader/ComponentLoader.js'
-class FilesPreview extends React.Component {
- static propTypes = {
- hash: PropTypes.string.isRequired,
- name: PropTypes.string.isRequired,
- size: PropTypes.number.isRequired,
- gatewayUrl: PropTypes.string.isRequired,
- read: PropTypes.func.isRequired,
- content: PropTypes.object,
- t: PropTypes.func.isRequired,
- tReady: PropTypes.bool.isRequired
- }
-
+class Preview extends React.Component {
state = {
content: null
}
@@ -27,11 +17,11 @@ class FilesPreview extends React.Component {
}
render () {
- const { t, name, size, hash, gatewayUrl } = this.props
+ const { t, name, hash, size, gatewayUrl } = this.props
const type = typeFromExt(name)
const src = `${gatewayUrl}/ipfs/${hash}`
- const className = 'mw-100 mt3 bg-snow-muted pa2 br2'
+ const className = 'mw-100 mt3 bg-snow-muted pa2 br2 border-box'
switch (type) {
case 'audio':
@@ -89,4 +79,18 @@ class FilesPreview extends React.Component {
}
}
-export default translate('files')(FilesPreview)
+Preview.propTypes = {
+ name: PropTypes.string.isRequired,
+ hash: PropTypes.string.isRequired,
+ size: PropTypes.number.isRequired,
+ gatewayUrl: PropTypes.string.isRequired,
+ read: PropTypes.func.isRequired,
+ content: PropTypes.object,
+ t: PropTypes.func.isRequired,
+ tReady: PropTypes.bool.isRequired
+}
+
+export default connect(
+ 'selectGatewayUrl',
+ translate('files')(Preview)
+)
diff --git a/src/files/file/File.js b/src/files/file/File.js
index 9a630160e..c05d28e48 100644
--- a/src/files/file/File.js
+++ b/src/files/file/File.js
@@ -2,6 +2,7 @@ import React from 'react'
import PropTypes from 'prop-types'
import { join, basename } from 'path'
import filesize from 'filesize'
+import { translate } from 'react-i18next'
import classnames from 'classnames'
import { filesToStreams } from '../../lib/files'
// React DnD
@@ -9,6 +10,7 @@ import { DropTarget, DragSource } from 'react-dnd'
import { NativeTypes } from 'react-dnd-html5-backend'
// Components
import GlyphDots from '../../icons/GlyphDots'
+import GlyphPin from '../../icons/GlyphPin'
import Tooltip from '../../components/tooltip/Tooltip'
import Checkbox from '../../components/checkbox/Checkbox'
import FileIcon from '../file-icon/FileIcon'
@@ -18,9 +20,8 @@ class File extends React.Component {
name: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
path: PropTypes.string.isRequired,
- size: PropTypes.number.isRequired,
- cumulativeSize: PropTypes.number,
- hash: PropTypes.string.isRequired,
+ size: PropTypes.number,
+ hash: PropTypes.string,
selected: PropTypes.bool,
focused: PropTypes.bool,
onSelect: PropTypes.func,
@@ -30,6 +31,8 @@ class File extends React.Component {
coloured: PropTypes.bool,
translucent: PropTypes.bool,
handleContextMenuClick: PropTypes.func,
+ pinned: PropTypes.bool,
+ isMfs: PropTypes.bool,
// Injected by DragSource and DropTarget
isOver: PropTypes.bool.isRequired,
canDrop: PropTypes.bool.isRequired,
@@ -44,19 +47,19 @@ class File extends React.Component {
}
handleCtxLeftClick = (ev) => {
- const { name, type, size, hash, path } = this.props
- const dotsPosition = this.dotsWrapper.getBoundingClientRect()
- this.props.handleContextMenuClick(ev, 'LEFT', { name, size, type, hash, path }, dotsPosition)
+ const { name, type, size, hash, path, pinned } = this.props
+ const pos = this.dotsWrapper.getBoundingClientRect()
+ this.props.handleContextMenuClick(ev, 'LEFT', { name, size, type, hash, path, pinned }, pos)
}
handleCtxRightClick = (ev) => {
- const { name, type, size, hash, path } = this.props
- this.props.handleContextMenuClick(ev, 'RIGHT', { name, size, type, hash, path })
+ const { name, type, size, hash, path, pinned } = this.props
+ this.props.handleContextMenuClick(ev, 'RIGHT', { name, size, type, hash, path, pinned })
}
render () {
let {
- selected, focused, translucent, coloured, hash, name, type, size, cumulativeSize, onSelect, onNavigate,
+ t, selected, focused, translucent, coloured, hash, name, type, size, pinned, onSelect, onNavigate,
isOver, canDrop, cantDrag, cantSelect, connectDropTarget, connectDragPreview, connectDragSource,
styles = {}
} = this.props
@@ -83,9 +86,8 @@ class File extends React.Component {
styles.height = 55
styles.overflow = 'hidden'
- size = (type === 'directory' && !cumulativeSize)
- ? '―'
- : filesize(cumulativeSize || size, { round: 0 })
+ size = size ? filesize(size, { round: 0 }) : '-'
+ hash = hash || t('hashUnavailable')
const select = (select) => onSelect(name, select)
@@ -114,7 +116,12 @@ class File extends React.Component {
)}
-
+
+
{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') }}>
+
+
+
+
-
-
- {t('newFolder')}
-
-
-
- ) : (
-
{ this.dotsWrapper = el }} className='ml-auto' style={{ width: '1.5rem' }}> {/* to render correctly in Firefox */}
-
+ { (files && files.type === 'directory' && filesPathInfo.isMfs)
+ ?
+ :
{ this.dotsWrapper = el }}>
+
+
+ { t('more') }
+
+
+ }
- )}
+
+
)
}
}
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}
-
-
+
-
-
+
-
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 }) => {
{t('status:title')}
- {t('files:title')}
+ {t('files:title')}
{t('explore:tabName')}
{t('peers:title')}
{t('settings:title')}
diff --git a/src/status/StatusConnected.js b/src/status/StatusConnected.js
index a4907e48d..139e2254c 100644
--- a/src/status/StatusConnected.js
+++ b/src/status/StatusConnected.js
@@ -4,7 +4,7 @@ import { connect } from 'redux-bundler-react'
import filesize from 'filesize'
export const StatusConnected = ({ peersCount, repoSize }) => {
- const humanRepoSize = filesize(repoSize || 0, { round: 0 })
+ const humanRepoSize = filesize(repoSize || 0, { round: 1 })
return (
diff --git a/test/e2e/navigation.test.js b/test/e2e/navigation.test.js
index 9d4fe0590..dff33821d 100644
--- a/test/e2e/navigation.test.js
+++ b/test/e2e/navigation.test.js
@@ -25,8 +25,8 @@ it('Navigation test: node running', async () => {
await page.goto(appUrl)
await waitForTitle('Status - IPFS')
- await page.click('nav a[href="#/files/"]')
- await waitForTitle('Files - IPFS')
+ await page.click('nav a[href="#/files"]')
+ await waitForTitle('/ - Files - IPFS')
await page.click('nav a[href="#/explore"]')
await waitForTitle('Explore - IPLD')