diff --git a/package-lock.json b/package-lock.json index dec4864ec..7ad8719b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39173,9 +39173,9 @@ } }, "ipfs-css": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/ipfs-css/-/ipfs-css-1.0.0.tgz", - "integrity": "sha512-R82wX2bliiQBR1nKZqw8LWTCvvk1um94SvIq+9ATpf1bIzvXN0Xs0rnXpUPVU4Nu6kKz6VIiLfLUkm3smcDzhA==" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ipfs-css/-/ipfs-css-1.1.0.tgz", + "integrity": "sha512-MXADP4wVdPP4jubuFR6MeJJUEuuSEmBnx0ST0vGhUdhN5v0R4PN5XL6Cdm9M3WwwAyFjV2FOyJAnP/WQToKRTg==" }, "ipfs-geoip": { "version": "4.0.0", @@ -46496,6 +46496,15 @@ "throttle-debounce": "^2.1.0" } }, + "react-spring": { + "version": "8.0.27", + "resolved": "https://registry.npmjs.org/react-spring/-/react-spring-8.0.27.tgz", + "integrity": "sha512-nDpWBe3ZVezukNRandTeLSPcwwTMjNVu1IDq9qA/AMiUqHuRN4BeSWvKr3eIxxg1vtiYiOLy4FqdfCP5IoP77g==", + "requires": { + "@babel/runtime": "^7.3.1", + "prop-types": "^15.5.8" + } + }, "react-syntax-highlighter": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-8.1.0.tgz", diff --git a/package.json b/package.json index 6302b5d4a..337764fda 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "i18next-xhr-backend": "^3.2.2", "internal-nav-helper": "^3.1.0", "ip": "^1.1.5", - "ipfs-css": "^1.0.0", + "ipfs-css": "^1.1.0", "ipfs-geoip": "^4.0.0", "ipfs-redux-bundle": "^7.0.0", "ipld-explorer-components": "^1.5.1", @@ -75,6 +75,7 @@ "react-overlays": "^2.1.0", "react-router-dom": "^5.1.2", "react-scripts": "3.3.0", + "react-spring": "^8.0.27", "react-test-renderer": "^16.12.0", "react-virtualized": "^9.21.2", "redux-bundler": "^26.0.0", diff --git a/public/locales/en/files.json b/public/locales/en/files.json index d3ecafc33..321fd9204 100644 --- a/public/locales/en/files.json +++ b/public/locales/en/files.json @@ -75,6 +75,13 @@ "filesList": { "noFiles": "<0>No files in this directory. Click the “Add” button to add some." }, + "filesImportStatus": { + "imported": "{count, plural, one {Imported 1 item} other {Imported {count} items}}", + "importing": "{count, plural, one {Importing 1 item} other {Importing {count} items}}", + "toggleDropdown": "Toggle dropdown", + "closeDropdown": "Close dropdown", + "count": "{count} of {count}" + }, "addFilesInfo": "<0>Add files to your local IPFS node by clicking the <1>Add to IPFS button above.", "companionInfo": "<0>As you are using <1>IPFS Companion, the files view is limited to files added while using the extension.", "tour": { diff --git a/src/bundles/files/actions.js b/src/bundles/files/actions.js index 442ae54c7..897b68da5 100644 --- a/src/bundles/files/actions.js +++ b/src/bundles/files/actions.js @@ -179,8 +179,17 @@ export default () => ({ } }) + const paths = files.map(f => ({ path: f.path, size: f.size })) + const updateProgress = (sent) => { - dispatch({ type: 'FILES_WRITE_UPDATED', payload: { id: id, progress: sent / totalSize * 100 } }) + dispatch({ + type: 'FILES_WRITE_UPDATED', + payload: { + id, + paths, + progress: sent / totalSize * 100 + } + }) } updateProgress(0) @@ -273,6 +282,8 @@ export default () => ({ dispatch({ type: 'FILES_UPDATE_SORT', payload: { by, asc } }) }, + doFilesClear: () => async ({ dispatch }) => dispatch({ type: 'FILES_CLEAR_ALL' }), + doFilesSizeGet: make(ACTIONS.FILES_SIZE_GET, async (ipfs) => { const stat = await ipfs.files.stat('/') return { size: stat.cumulativeSize } diff --git a/src/bundles/files/index.js b/src/bundles/files/index.js index 719db2ae2..b93623045 100644 --- a/src/bundles/files/index.js +++ b/src/bundles/files/index.js @@ -23,6 +23,15 @@ export default () => { } } + if (action.type === 'FILES_CLEAR_ALL') { + return { + ...state, + failed: [], + finished: [], + pending: [] + } + } + if (action.type === 'FILES_UPDATE_SORT') { const pageContent = state.pageContent @@ -65,7 +74,10 @@ export default () => { ...state.pending.filter(a => a.id !== id), { ...pendingAction, - data: data + data: { + ...data, + hasError: true + } } ] } diff --git a/src/bundles/files/selectors.js b/src/bundles/files/selectors.js index a33071119..8af9e00d0 100644 --- a/src/bundles/files/selectors.js +++ b/src/bundles/files/selectors.js @@ -18,16 +18,9 @@ export default () => ({ selectFilesSorting: (state) => state.files.sorting, - selectWriteFilesProgress: (state) => { - const writes = state.files.pending.filter(s => s.type === ACTIONS.WRITE && s.data.progress) + selectFilesPending: (state) => 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 - }, + selectFilesFinished: (state) => state.files.finished.filter(s => s.type === ACTIONS.WRITE), selectFilesHasError: (state) => state.files.failed.length > 0, diff --git a/src/bundles/files/utils.js b/src/bundles/files/utils.js index 878132712..bb886e1af 100644 --- a/src/bundles/files/utils.js +++ b/src/bundles/files/utils.js @@ -18,7 +18,17 @@ export const make = (basename, action, options = {}) => (...args) => async (args try { data = await action(getIpfs(), ...args, id, args2) - dispatch({ type: `FILES_${basename}_FINISHED`, payload: { id, ...data } }) + + const paths = args[0] ? args[0].flat() : [] + + dispatch({ + type: `FILES_${basename}_FINISHED`, + payload: { + id, + ...data, + paths + } + }) // Rename specific logic if (basename === ACTIONS.MOVE) { diff --git a/src/components/notify/Toast.js b/src/components/notify/Toast.js index 3680b07ab..25485da10 100644 --- a/src/components/notify/Toast.js +++ b/src/components/notify/Toast.js @@ -1,5 +1,5 @@ import React from 'react' -import CancelIcon from '../../icons/GlyphCancel' +import CancelIcon from '../../icons/GlyphSmallCancel' const Toast = ({ error, children, onDismiss }) => { const bg = error ? 'bg-yellow' : 'bg-green' @@ -9,7 +9,7 @@ const Toast = ({ error, children, onDismiss }) => { {children} diff --git a/src/files/FilesPage.js b/src/files/FilesPage.js index a99a4a0a5..30c93a25a 100644 --- a/src/files/FilesPage.js +++ b/src/files/FilesPage.js @@ -19,6 +19,7 @@ import { getJoyrideLocales } from '../helpers/i8n' // Icons import Modals, { DELETE, NEW_FOLDER, SHARE, RENAME, ADD_BY_PATH } from './modals/Modals' import Header from './header/Header' +import FileImportStatus from './file-import-status/FileImportStatus' const defaultState = { downloadAbort: null, @@ -258,6 +259,8 @@ class FilesPage extends React.Component { onAddByPath={this.onAddByPath} { ...this.state.modals } /> + + { + const pathsByFolder = paths.reduce((prev, currentPath) => { + const isFolder = currentPath.path.includes('/') + if (!isFolder) { + return [...prev, currentPath] + } + + const baseFolder = currentPath.path.split('/')[0] + + const alreadyExistentBaseFolder = prev.find(previousPath => previousPath.path.startsWith(`${baseFolder}/`)) + + if (alreadyExistentBaseFolder) { + alreadyExistentBaseFolder.count = alreadyExistentBaseFolder.count + 1 + + return prev + } + + return [...prev, { ...currentPath, name: baseFolder, count: 1 }] + }, []) + + return pathsByFolder.map(({ count, name, path, size, progress }) => ( +
  • + { count ? : } + { name || path } + | + { count && ( { t('filesImportStatus.count', { count }) } | ) } + { filesize(size) } + + { hasError ? : } +
  • + )) +} + +const LoadingIndicator = ({ complete }) => ( + <> +
    +
    +
    + { complete && } + +) + +const FileImportStatus = ({ filesFinished, filesPending, filesErrors, doFilesClear, t }) => { + const sortedFilesFinished = useMemo(() => filesFinished.sort((fileA, fileB) => fileB.start - fileA.start), [filesFinished]) + const [expanded, setExpanded] = useState(true) + + const handleImportStatusClose = useCallback((ev) => { + doFilesClear() + ev.stopPropagation() // Prevent setExpanded from being called + }, [doFilesClear]) + + if (!filesFinished.length && !filesPending.length && !filesErrors.length) { + return null + } + + const numberOfImportedFiles = !filesFinished.length ? 0 : filesFinished.reduce((prev, finishedFile) => prev + finishedFile?.data?.paths?.length, 0) + + return ( +
    +
    +
    setExpanded(!expanded)} aria-expanded={expanded} aria-label={ t('filesImportStatus.toggleDropdown') } role="button"> + { filesPending.length + ? t('filesImportStatus.importing', { count: filesPending.length }) + : t('filesImportStatus.imported', { count: numberOfImportedFiles }) + } + +
    + +
    +
    +
      + { filesPending.map(file => File(file.data, t)) } + { sortedFilesFinished.map(file => File(file.data, t)) } + { filesErrors.map(file => File(file.data, t)) } +
    +
    +
    + ) +} + +FileImportStatus.propTypes = { + filesFinished: PropTypes.array, + filesPending: PropTypes.array, + filesErrors: PropTypes.array, + doFilesClear: PropTypes.func +} + +FileImportStatus.defaultProps = { + filesFinished: [], + filesPending: [], + filesErrors: [] +} + +export default connect( + 'selectFilesFinished', + 'selectFilesPending', + 'selectFilesErrors', + 'doFilesClear', + withTranslation('files')(FileImportStatus) +) diff --git a/src/files/file-input/FileInput.js b/src/files/file-input/FileInput.js index 166ab98e8..2e70978dd 100644 --- a/src/files/file-input/FileInput.js +++ b/src/files/file-input/FileInput.js @@ -12,24 +12,17 @@ import DecentralizationIcon from '../../icons/StrokeDecentralization' import { Dropdown, DropdownMenu, Option } from '../dropdown/Dropdown' import Button from '../../components/button/Button' -const AddButton = withTranslation('files')(({ progress = null, disabled, t, tReady, i18n, lng, ...props }) => { - const sending = progress !== null - - return ( - ) -}) +) class FileInput extends React.Component { state = { - dropdown: false, - force100: false + dropdown: false } toggleDropdown = () => { @@ -56,15 +49,6 @@ class FileInput extends React.Component { return this.filesInput.click() } - componentDidUpdate (prev) { - if (this.props.writeFilesProgress === 100 && prev.writeFilesProgress !== 100) { - this.setState({ force100: true }) - setTimeout(() => { - this.setState({ force100: false }) - }, 2000) - } - } - onInputChange = (input) => async () => { this.props.onAddFiles(await filesToStreams(input.files)) input.value = null @@ -81,15 +65,12 @@ class FileInput extends React.Component { } render () { - let { progress, t } = this.props - if (this.state.force100) { - progress = 100 - } + const { t } = this.props return (
    - + this.props.onDelete(this.selectedFiles)} diff --git a/src/files/header/Header.js b/src/files/header/Header.js index 89507b3a7..07a294306 100644 --- a/src/files/header/Header.js +++ b/src/files/header/Header.js @@ -42,7 +42,8 @@ class Header extends React.Component { render () { const { - files, writeFilesProgress, t, + files, + t, pins, filesPathInfo, filesSize, @@ -80,8 +81,7 @@ class Header extends React.Component { ? + onAddByPath={this.props.onAddByPath} /> :
    { this.dotsWrapper = el }}>
    - + {t('actions.unselectAll')} {t('actions.clear')} - +
    diff --git a/src/icons/GlyphSmallArrow.js b/src/icons/GlyphSmallArrow.js new file mode 100644 index 000000000..097d53148 --- /dev/null +++ b/src/icons/GlyphSmallArrow.js @@ -0,0 +1,10 @@ +import React from 'react' + +const GlyphSmallArrows = props => ( + + + + +) + +export default GlyphSmallArrows