Skip to content

Commit

Permalink
feat: add pinning to files page (#1678)
Browse files Browse the repository at this point in the history
* chore: refactor files page

* feat: add pinning services mock to files page

* chore: update remotePin dimensions

Co-authored-by: Jessica Schilling <jessica@protocol.ai>

* chore: update local pin icon dimension

Co-authored-by: Jessica Schilling <jessica@protocol.ai>

* chore: change pin icon fill color

Co-authored-by: Jessica Schilling <jessica@protocol.ai>

* chore: change remote pin icon fill color

Co-authored-by: Jessica Schilling <jessica@protocol.ai>

* chore: update pinning modal text size

Co-authored-by: Jessica Schilling <jessica@protocol.ai>

* chore: update pinning modal text size

Co-authored-by: Jessica Schilling <jessica@protocol.ai>

* chore: pinning modal secondary text changes

Co-authored-by: Jessica Schilling <jessica@protocol.ai>

* chore: update pinning modal image size

Co-authored-by: Jessica Schilling <jessica@protocol.ai>

* chore: update pinning modal pin icon

Co-authored-by: Jessica Schilling <jessica@protocol.ai>

* chore: add pin status column to fileslist

* chore: fix modals icons

* chore: fix linting error

* chore: fix files sorting

Co-authored-by: Jessica Schilling <jessica@protocol.ai>
  • Loading branch information
rafaelramalho19 and jessicaschilling committed Nov 3, 2020
1 parent d28b8ac commit bc60e46
Show file tree
Hide file tree
Showing 37 changed files with 4,178 additions and 694 deletions.
3,996 changes: 3,636 additions & 360 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions public/locales/en/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"save": "Save",
"saving": "Saving…",
"selectAll": "Select all",
"setPinning": "Set pinning",
"submit": "Submit",
"unpin": "Unpin",
"unselectAll": "Unselect all"
Expand Down Expand Up @@ -68,6 +69,7 @@
"peers": "Peers",
"pinNoun": "Pin",
"pins": "Pins",
"pinStatus": "Pin Status",
"publicKey": "Public key",
"rateIn": "Rate in",
"rateOut": "Rate out",
Expand Down
5 changes: 5 additions & 0 deletions public/locales/en/files.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@
"descriptionFolder": "{count, plural, one {Are you sure you want to delete this folder? This action is permanent and cannot be reversed.} other {Are you sure you want to delete these {count} folders? This action is permanent and cannot be reversed.}}",
"descriptionFile": "{count, plural, one {Are you sure you want to delete this file? This action is permanent and cannot be reversed.} other {Are you sure you want to delete these {count} files? This action is permanent and cannot be reversed.}}"
},
"pinningModal": {
"title": "Select the services with which you wish to pin these files.",
"footer": "Need to add or configure a pinning service? Go to <1>Settings</1>.",
"localNode": "Local node"
},
"addByPathModal": {
"title": "Import from IPFS",
"description": "Insert an IPFS path (CID) to import.",
Expand Down
10 changes: 0 additions & 10 deletions public/locales/en/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,16 +59,6 @@
"DISABLED": "Disabled",
"PINS_ONLY": "Pins only"
},
"localPinning": "Local Pinning",
"service": "Service",
"files": "Files",
"bandwidthUsed": "Bandwidth Used",
"autoUpload": "Auto Upload",
"autoUploadKeys": {
"ALL_FILES": "All files",
"DISABLED": "Disabled",
"PINS_ONLY": "Pins only"
},
"fetchingSettings": "Fetching settings...",
"configApiNotAvailable": "The IPFS config API is not available. Please disable the \"IPFS Companion\" Web Extension and try again.",
"ipfsDaemonOffline": "The IPFS daemon is offline. Please turn it on and try again.",
Expand Down
4 changes: 2 additions & 2 deletions src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,9 @@ export class App extends Component {

return connectDropTarget(
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
<div className='sans-serif h-100' onClick={getNavHelper(this.props.doUpdateUrl)}>
<div className='sans-serif h-100 relative' onClick={getNavHelper(this.props.doUpdateUrl)}>
{/* Tinted overlay that appears when dragging and dropping an item */}
{ canDrop && isOver && <div className='w-100 h-100 top-0 left-0 absolute' style={{ background: 'rgba(99, 202, 210, 0.2)' }} /> }
{ canDrop && isOver && <div className='h-100 top-0 right-0 fixed appOverlay' style={{ background: 'rgba(99, 202, 210, 0.2)' }} /> }
<div className='flex flex-row-reverse-l flex-column-reverse justify-end justify-start-l' style={{ minHeight: '100vh' }}>
<div className='flex-auto-l'>
<div className='flex items-center ph3 ph4-l' style={{ WebkitAppRegion: 'drag', height: 75, background: '#F0F6FA', paddingTop: '20px', paddingBottom: '15px' }}>
Expand Down
29 changes: 18 additions & 11 deletions src/bundles/files/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -462,21 +462,28 @@ const actions = () => ({
doFilesDismissErrors: () => send({ type: ACTIONS.DISMISS_ERRORS }),

/**
* @param {string} path
*/
doFilesNavigateTo: (path) =>
* @param {Object} fileArgs
* @param {string} fileArgs.path
* @param {string|CID} fileArgs.cid
*/
doFilesNavigateTo: ({ path, cid }) =>
/**
* @param {Context} context
*/
async ({ store }) => {
const link = path.split('/').map(p => encodeURIComponent(p)).join('/')
const files = store.selectFiles()
const url = store.selectFilesPathInfo()

if (files && files.path === link && url) {
await store.doFilesFetch()
} else {
await store.doUpdateHash(link)
try {
const link = path.split('/').map(p => encodeURIComponent(p)).join('/')
const files = store.selectFiles()
const url = store.selectFilesPathInfo()

if (files && files.path === link && url) {
await store.doFilesFetch()
} else {
await store.doUpdateHash(link)
}
} catch (e) {
console.error(e)
store.doUpdateHash(`/ipfs/${cid}`)
}
},

Expand Down
3 changes: 2 additions & 1 deletion src/bundles/files/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,8 @@ export default () => {
pageContent: {
...pageContent,
content
}
},
sorting: action.payload
}
} else {
return state
Expand Down
59 changes: 58 additions & 1 deletion src/bundles/pinning.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,63 @@
// @ts-check
/**
* TODO: This might change, current version from: https://github.com/ipfs/go-ipfs/blob/petar/pincli/core/commands/remotepin.go#L53
* @typedef {Object} RemotePin
* @property {string} id
* @property {string} name
* @property {('queued'|'pinning'|'pinned'|'failed')} status
* @property {string} cid
* @property {Array<string>} [delegates] e.g. ["/dnsaddr/pin-service.example.com"]
*/
export default {
name: 'pinning',
reducer: (state = {}) => state,
reducer: (state = {
remotePins: []
}, action) => {
if (action.type === 'SET_REMOTE_PINS') {
return { ...state, remotePins: action.payload }
}
return state
},

doFetchRemotePins: () => async ({ dispatch, store }) => {
const pinningServices = store.selectPinningServices()

if (!pinningServices?.length) return

// TODO: unmock this (e.g. const pins = ipfs.pin.remote.ls ...)
const response = [
{
id: 'Pinata:UniqueIdOfPinRequest',
status: 'queued',
cid: 'QmQsUbcVx6Vu8vtL858FdxD3sVBE6m8uP3bjFoTzrGubmX',
name: '26_remote.png',
delegates: ['/dnsaddr/pin-service.example.com']
}
]

// TODO: get type of item?

const remotePins = response.map(item => ({
...item,
isRemotePin: true,
type: item.type || 'unknown',
size: Math.random() * 1000// TODO: files.stat in the future
}))

// TODO: handle different status (queued = async fetch in batches to update ui?)

dispatch({ type: 'SET_REMOTE_PINS', payload: remotePins })
},

selectRemotePins: (state) => state.pinning.remotePins || [],

doSelectRemotePinsForFile: (file) => ({ store }) => {
const pinningServicesNames = store.selectPinningServices().map(remote => remote.name)
const remotePinForFile = store.selectRemotePins().filter(pin => pin.cid === file.cid.string)
const servicesBeingUsed = remotePinForFile.map(pin => pin.id.split(':')[0]).filter(pinId => pinningServicesNames.includes(pinId))

return servicesBeingUsed
},

// selectPinningServices: state => state.pinning
// TODO: unmock this
Expand Down
2 changes: 1 addition & 1 deletion src/components/cli-tutor-mode/CliTutorMode.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export const CliTutorialModal = ({ command, t, onLeave, className, downloadConfi

return (
<Modal {...props} className={className} onCancel={onLeave} style={{ maxWidth: '40em' }}>
<ModalBody icon={StrokeCode}>
<ModalBody Icon={StrokeCode}>
<p className='charcoal w-80 center' style={{ lineHeight: '1.3' }}>
{t('app:cliModal.description')}
</p>
Expand Down
6 changes: 3 additions & 3 deletions src/components/text-input-modal/TextInputModal.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class TextInputModal extends React.Component {
onChange: PropTypes.func,
onInputChange: PropTypes.func,
title: PropTypes.string.isRequired,
icon: PropTypes.func.isRequired,
Icon: PropTypes.func.isRequired,
description: PropTypes.node,
submitText: PropTypes.string,
validate: PropTypes.func,
Expand Down Expand Up @@ -98,7 +98,7 @@ class TextInputModal extends React.Component {
mustBeDifferent,
onSubmit,
className,
icon,
Icon,
submitText,
validate,
defaultValue,
Expand All @@ -111,7 +111,7 @@ class TextInputModal extends React.Component {

return (
<Modal {...props} className={className} onCancel={onCancel}>
<ModalBody title={title} icon={icon}>
<ModalBody title={title} Icon={Icon}>
{ description && typeof description === 'object' && description }

{ description && typeof description === 'string' &&
Expand Down
20 changes: 11 additions & 9 deletions src/files/FilesPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import FilesList from './files-list/FilesList'
import { getJoyrideLocales } from '../helpers/i8n'

// Icons
import Modals, { DELETE, NEW_FOLDER, SHARE, RENAME, ADD_BY_PATH, CLI_TUTOR_MODE } from './modals/Modals'
import Modals, { DELETE, NEW_FOLDER, SHARE, RENAME, ADD_BY_PATH, CLI_TUTOR_MODE, PINNING } from './modals/Modals'
import Header from './header/Header'
import FileImportStatus from './file-import-status/FileImportStatus'

Expand Down Expand Up @@ -48,6 +48,7 @@ class FilesPage extends React.Component {
this.props.doFilesFetch()
this.props.doPinsFetch()
this.props.doFilesSizeGet()
this.props.doFetchRemotePins()
}

componentDidUpdate (prev) {
Expand Down Expand Up @@ -141,7 +142,7 @@ class FilesPage extends React.Component {
}

get mainView () {
const { t, files, doExploreUserProvidedPath } = this.props
const { t, files, remotePins, doExploreUserProvidedPath } = this.props

if (!files) {
return (<div/>)
Expand Down Expand Up @@ -170,15 +171,17 @@ class FilesPage extends React.Component {
return (
<FilesList
key={window.encodeURIComponent(files.path)}
root={files.path}
updateSorting={this.props.doFilesUpdateSorting}
files={files.content}
remotePins={remotePins}
upperDir={files.upper}
downloadProgress={this.state.downloadProgress}
onShare={(files) => this.showModal(SHARE, files)}
onRename={(files) => this.showModal(RENAME, files)}
onDelete={(files) => this.showModal(DELETE, files)}
onSetPinning={(files) => this.showModal(PINNING, files)}
onInspect={this.onInspect}
onRemotePinClick={this.onRemotePinClick}
onDownload={this.onDownload}
onAddFiles={this.onAddFiles}
onNavigate={this.props.doFilesNavigateTo}
Expand Down Expand Up @@ -234,8 +237,7 @@ class FilesPage extends React.Component {
onRename={() => this.showModal(RENAME, [contextMenu.file])}
onInspect={() => this.onInspect(contextMenu.file.cid)}
onDownload={() => this.onDownload([contextMenu.file])}
onPin={() => this.props.doFilesPin(contextMenu.file.cid)}
onUnpin={() => this.props.doFilesUnpin(contextMenu.file.cid)}
onPinning={() => this.showModal(PINNING, [contextMenu.file])}
isCliTutorModeEnabled={isCliTutorModeEnabled}
onCliTutorMode={() => this.showModal(CLI_TUTOR_MODE, [contextMenu.file])}
doSetCliOptions={doSetCliOptions}
Expand Down Expand Up @@ -265,6 +267,7 @@ class FilesPage extends React.Component {
onShareLink={this.props.doFilesShareLink}
onDelete={this.props.doFilesDelete}
onAddByPath={this.onAddByPath}
onPinningSet={this.props.doSetPinning}
cliOptions={cliOptions}
{ ...this.state.modals } />

Expand Down Expand Up @@ -300,8 +303,6 @@ FilesPage.propTypes = {
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
Expand All @@ -311,18 +312,18 @@ export default connect(
'selectIpfsProvider',
'selectIpfsConnected',
'selectFiles',
'selectRemotePins',
'selectFilesPathInfo',
'doUpdateHash',
'doPinsFetch',
'doFetchRemotePins',
'doFilesFetch',
'doFilesMove',
'doFilesMakeDir',
'doFilesShareLink',
'doFilesDelete',
'doFilesAddPath',
'doFilesNavigateTo',
'doFilesPin',
'doFilesUnpin',
'doFilesUpdateSorting',
'selectFilesSorting',
'selectToursEnabled',
Expand All @@ -335,5 +336,6 @@ export default connect(
'doOpenCliTutorModal',
'doSetCliOptions',
'selectCliOptions',
'doSetPinning',
withTour(withTranslation('files')(FilesPage))
)
2 changes: 1 addition & 1 deletion src/files/breadcrumbs/Breadcrumbs.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ const DropableBreadcrumb = ({ index, link, immutable, onAddFiles, onMove, onClic
index === 0 && (immutable ? 'bg-charcoal-muted white' : 'bg-navy white'),
immutable && (link.last || index === 0) && 'no-events',
link.last && 'b', isOver && 'dragging')}
onClick={() => onClick(link.path)} onContextMenu={(ev) => index !== 0 && handleOnContextMenuHandle(ev)}>
onClick={() => onClick({ path: link.path })} onContextMenu={(ev) => index !== 0 && handleOnContextMenuHandle(ev)}>
{link.name}
</button>
</span>
Expand Down
6 changes: 3 additions & 3 deletions src/files/context-menu/ContextMenu.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ class ContextMenu extends React.Component {
render () {
const {
t, onRename, onDelete, onDownload, onInspect, onShare,
translateX, translateY, className, isMfs, isUnknown, pinned, isCliTutorModeEnabled
translateX, translateY, className, isMfs, isUnknown, isCliTutorModeEnabled
} = this.props
return (
<Dropdown className={className}>
Expand Down Expand Up @@ -75,10 +75,10 @@ class ContextMenu extends React.Component {
{t('app:actions.inspect')}
</Option>
}
<Option onClick={this.wrap(pinned ? 'onUnpin' : 'onPin')} isCliTutorModeEnabled={isCliTutorModeEnabled}
<Option onClick={this.wrap('onPinning')} isCliTutorModeEnabled={isCliTutorModeEnabled}
onCliTutorMode={this.wrap('onCliTutorMode', cliCmdKeys.PIN_OBJECT)}>
<StrokePin className='w2 mr2 fill-aqua' />
{ pinned ? t('app:actions.unpin') : t('app:actions.pinVerb') }
{ t('app:actions.setPinning') }
</Option>
{ !isUnknown && onDownload &&
<Option onClick={this.wrap('onDownload')} isCliTutorModeEnabled={isCliTutorModeEnabled}
Expand Down
2 changes: 1 addition & 1 deletion src/files/explore-form/FilesExploreForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class FilesExploreForm extends React.Component {
path = `/ipfs/${path}`
}

this.props.onBrowse(path)
this.props.onBrowse({ path })
this.setState({ path: '' })
}
}
Expand Down
6 changes: 3 additions & 3 deletions src/files/file-import-status/FileImportStatus.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@ import GlyphCancel from '../../icons/GlyphCancel'
import GlyphSmallCancel from '../../icons/GlyphSmallCancel'

const File = (job, t) => {
const pathsByFolder = job.message.entries.reduce((prev, currentEntry) => {
const pathsByFolder = job.message?.entries?.reduce((prev, currentEntry) => {
const isFolder = currentEntry.path.includes('/')
if (!isFolder) {
return [...prev, currentEntry]
}

const baseFolder = currentEntry.path.split('/')[0]

const alreadyExistentBaseFolder = prev.find(previousPath => previousPath.path.startsWith(`${baseFolder}/`))
const alreadyExistentBaseFolder = prev.find(previousPath => previousPath.path.startsWith(baseFolder + '/'))

if (alreadyExistentBaseFolder) {
alreadyExistentBaseFolder.count = alreadyExistentBaseFolder.count + 1
Expand All @@ -33,7 +33,7 @@ const File = (job, t) => {
return [...prev, { ...currentEntry, name: baseFolder, count: 1 }]
}, [])

return pathsByFolder.map(({ count, name, path, size, progress }) => (
return pathsByFolder?.map(({ count, name, path, size, progress }) => (
<li className="flex w-100 bb b--light-gray items-center f6 charcoal" key={ path || name }>
{ count ? <FolderIcon className='fileImportStatusIcon fill-aqua pa1'/> : <DocumentIcon className='fileImportStatusIcon fill-aqua pa1'/> }
<span className="fileImportStatusName truncate">{ name || path }</span>
Expand Down
Loading

0 comments on commit bc60e46

Please sign in to comment.