Skip to content

Commit 830a30d

Browse files
MattWong-calidelsemantic-release-botgithub-actions[bot]SgtPooki
authored
feat: bulk CIDs import (#2307)
* Start bulk CID import * Get functional MVP working with csv file * Get it working with txt file * WIP: bulk import modal UX * WIP, need to debug modal UX * Modal UX working, need to clean up code * Remove comments * Clean code * Add validation to file select * Use localization * Add correct type, remove progress tracking * Refactor code * Add storybook file * Remove comments * Update Add menu test * Remove prop-types * Use functional component * Use useTranslation * Update to TS file * Update import path * fix(explore): browsing chunked files and inspecting via context menu (#2305) * fix(explore): chunked files This includes latest ipld-explorer-components with fix from ipfs/ipld-explorer-components#462 also bumped kubo and caniuse and non-breaking audit suggestions * fix(files): Inspect via context menu Closes #2306 * chore(ci): set cluster pin timeout to 30m https://github.com/ipfs/ipfs-webui/actions/workflows/ci.yml?page=4&query=is%3Asuccess are usually under 10-20 minutes if something takes longer it will likely take ages and then fail, so better to fail faster, allowing user to retry release * chore(ci): use repo in offline mode no need to start node and open outgoing connections github CI may be punishing us by throttling egress * Get functional MVP working with csv file * Get it working with txt file * WIP: bulk import modal UX * WIP, need to debug modal UX * Modal UX working, need to clean up code * Remove comments * Clean code * Add validation to file select * Use localization * Add correct type, remove progress tracking * Refactor code * Add storybook file * Remove comments * Update Add menu test * chore(release): 4.4.1 [skip ci] ## [4.4.1](v4.4.0...v4.4.1) (2024-11-30) CID `bafybeiatztgdllxnp5p6zu7bdwhjmozsmd7jprff4bdjqjljxtylitvss4` --- ### Bug Fixes * add lithuanian to languages.json ([#2293](#2293)) ([40c512b](40c512b)) * analyze script doesn't persist stats.json ([#2290](#2290)) ([dbbdd70](dbbdd70)) * **explore:** browsing chunked files and inspecting via context menu ([#2305](#2305)) ([0412970](0412970)), closes [#2306](#2306) ### Trivial Changes * **ci:** add CAR file directly to cluster ([#2304](#2304)) ([e2ae110](e2ae110)) * **ci:** no replication factor when pinning - use cluster's default ([#2302](#2302)) ([81b8f29](81b8f29)) * **ci:** set cluster pin timeout to 30m ([4b8fc00](4b8fc00)) * **ci:** udpate artifact actions to v4 ([#2292](#2292)) ([305908f](305908f)) * **ci:** use repo in offline mode ([eaf63ed](eaf63ed)) * pull new translations ([#2291](#2291)) ([bfe7e40](bfe7e40)) * pull transifex translations ([#2296](#2296)) ([502abd4](502abd4)) * pull transifex translations ([#2303](#2303)) ([89c094b](89c094b)) * size-related labels in Files screen ([#2295](#2295)) ([49019d4](49019d4)) * chore: pull new translations (#2308) Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: lidel <[email protected]> * Remove prop-types * Use functional component * Use useTranslation * Update to TS file * Update import path --------- Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Marcin Rataj <[email protected]> Co-authored-by: semantic-release-bot <[email protected]> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: lidel <[email protected]> Co-authored-by: Russell Dempsey <[email protected]>
1 parent d751fc6 commit 830a30d

File tree

12 files changed

+229
-14
lines changed

12 files changed

+229
-14
lines changed

public/locales/en/files.json

+9
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"openWithLocalAndPublicGateway": "Try opening it instead with your <1>local gateway</1> or <3>public gateway</3>.",
1515
"cantBePreviewed": "Sorry, this file can’t be previewed",
1616
"addByPath": "From IPFS",
17+
"bulkImport": "Bulk import",
1718
"newFolder": "New folder",
1819
"generating": "Generating…",
1920
"actions": {
@@ -59,6 +60,14 @@
5960
"namePlaceholder": "Name (optional)",
6061
"examples": "Examples:"
6162
},
63+
"bulkImportModal": {
64+
"title": "Bulk import with text file",
65+
"description": "Upload a text file with a list of CIDs (names are optional). Example:",
66+
"select": "Select file",
67+
"selectedFile": "Selected file",
68+
"invalidCids": "*Invalid CID(s) found",
69+
"failedToReadFile": "*Failed to read file contents"
70+
},
6271
"newFolderModal": {
6372
"title": "New folder",
6473
"description": "Insert the name of the folder you want to create."

src/bundles/files/actions.js

+47
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,53 @@ const actions = () => ({
398398
}
399399
}),
400400

401+
/**
402+
* Reads a text file containing CIDs and adds each one to IPFS at the given root path.
403+
* @param {FileStream[]} source - The text file containing CIDs
404+
* @param {string} root - Destination directory in IPFS
405+
*/
406+
doFilesBulkCidImport: (source, root) => perform(ACTIONS.BULK_CID_IMPORT, async function (ipfs, { store }) {
407+
ensureMFS(store)
408+
409+
if (!source?.[0]?.content) {
410+
console.error('Invalid file format provided to doFilesBulkCidImport')
411+
return
412+
}
413+
414+
try {
415+
const file = source[0]
416+
const content = await new Response(file.content).text()
417+
const lines = content.split('\n').map(line => line.trim()).filter(Boolean)
418+
419+
const cidObjects = lines.map((line) => {
420+
let actualCid = line
421+
let name = line
422+
const cidParts = line.split(' ')
423+
if (cidParts.length > 1) {
424+
actualCid = cidParts[0]
425+
name = cidParts.slice(1).join(' ')
426+
}
427+
return {
428+
name,
429+
cid: actualCid
430+
}
431+
})
432+
433+
for (const { cid, name } of cidObjects) {
434+
try {
435+
const src = `/ipfs/${cid}`
436+
const dst = realMfsPath(join(root || '/files', name || cid))
437+
438+
await ipfs.files.cp(src, dst)
439+
} catch (err) {
440+
console.error(`Failed to add CID ${cid}:`, err)
441+
}
442+
}
443+
} finally {
444+
await store.doFilesFetch()
445+
}
446+
}),
447+
401448
/**
402449
* Creates a download link for the provided files.
403450
* @param {FileStat[]} files

src/bundles/files/consts.js

+2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ export const ACTIONS = {
2323
SHARE_LINK: ('FILES_SHARE_LINK'),
2424
/** @type {'FILES_ADDBYPATH'} */
2525
ADD_BY_PATH: ('FILES_ADDBYPATH'),
26+
/** @type {'FILES_BULK_CID_IMPORT'} */
27+
BULK_CID_IMPORT: ('FILES_BULK_CID_IMPORT'),
2628
/** @type {'FILES_PIN_ADD'} */
2729
PIN_ADD: ('FILES_PIN_ADD'),
2830
/** @type {'FILES_PIN_REMOVE'} */

src/bundles/files/protocol.ts

+2
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ export type Message =
6363
| Move
6464
| Write
6565
| AddByPath
66+
| BulkCidImport
6667
| DownloadLink
6768
| Perform<'FILES_SHARE_LINK', Error, string, void>
6869
| Perform<'FILES_COPY', Error, void, void>
@@ -76,6 +77,7 @@ export type MakeDir = Perform<'FILES_MAKEDIR', Error, void, void>
7677
export type WriteProgress = { paths: string[], progress: number }
7778
export type Write = Spawn<'FILES_WRITE', WriteProgress, Error, void, void>
7879
export type AddByPath = Perform<'FILES_ADDBYPATH', Error, void, void>
80+
export type BulkCidImport = Perform<'FILES_BULK_CID_IMPORT', Error, void, void>
7981
export type Move = Perform<'FILES_MOVE', Error, void, void>
8082
export type Delete = Perform<'FILES_DELETE', Error, void, void>
8183
export type DownloadLink = Perform<'FILES_DOWNLOADLINK', Error, FileDownload, void>

src/components/modal/Modal.js

+2-11
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import React from 'react'
22
import PropTypes from 'prop-types'
33
import CancelIcon from '../../icons/GlyphSmallCancel.js'
44

5-
export const ModalActions = ({ justify, className, children, ...props }) => (
5+
export const ModalActions = ({ justify = 'between', className = '', children, ...props }) => (
66
<div className={`flex justify-${justify} pa2 ${className}`} style={{ backgroundColor: '#f4f6f8' }} {...props}>
77
{ children }
88
</div>
@@ -13,12 +13,7 @@ ModalActions.propTypes = {
1313
className: PropTypes.string
1414
}
1515

16-
ModalActions.defaultProps = {
17-
justify: 'between',
18-
className: ''
19-
}
20-
21-
export const ModalBody = ({ className, Icon, title, children, ...props }) => (
16+
export const ModalBody = ({ className = '', Icon, title, children, ...props }) => (
2217
<div className={`ph4 pv3 tc ${className}`} {...props}>
2318
{ Icon && (
2419
<div className='center bg-snow br-100 flex justify-center items-center' style={{ width: '80px', height: '80px' }}>
@@ -40,10 +35,6 @@ ModalBody.propTypes = {
4035
])
4136
}
4237

43-
ModalBody.defaultProps = {
44-
className: ''
45-
}
46-
4738
export const Modal = ({ onCancel, children, className, ...props }) => {
4839
return (
4940
<div className={`${className} bg-white w-80 shadow-4 sans-serif relative`} style={{ maxWidth: '34em' }} {...props}>

src/files/FilesPage.js

+11-2
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,13 @@ import FilesList from './files-list/FilesList.js'
1515
import { getJoyrideLocales } from '../helpers/i8n.js'
1616

1717
// Icons
18-
import Modals, { DELETE, NEW_FOLDER, SHARE, RENAME, ADD_BY_PATH, CLI_TUTOR_MODE, PINNING, PUBLISH } from './modals/Modals.js'
18+
import Modals, { DELETE, NEW_FOLDER, SHARE, RENAME, ADD_BY_PATH, BULK_CID_IMPORT, CLI_TUTOR_MODE, PINNING, PUBLISH } from './modals/Modals.js'
1919
import Header from './header/Header.js'
2020
import FileImportStatus from './file-import-status/FileImportStatus.js'
2121
import { useExplore } from 'ipld-explorer-components/providers'
2222

2323
const FilesPage = ({
24-
doFetchPinningServices, doFilesFetch, doPinsFetch, doFilesSizeGet, doFilesDownloadLink, doFilesDownloadCarLink, doFilesWrite, doFilesAddPath, doUpdateHash,
24+
doFetchPinningServices, doFilesFetch, doPinsFetch, doFilesSizeGet, doFilesDownloadLink, doFilesDownloadCarLink, doFilesWrite, doFilesBulkCidImport, doFilesAddPath, doUpdateHash,
2525
doFilesUpdateSorting, doFilesNavigateTo, doFilesMove, doSetCliOptions, doFetchRemotePins, remotePins, pendingPins, failedPins,
2626
ipfsProvider, ipfsConnected, doFilesMakeDir, doFilesShareLink, doFilesDelete, doSetPinning, onRemotePinClick, doPublishIpnsKey,
2727
files, filesPathInfo, pinningServices, toursEnabled, handleJoyrideCallback, isCliTutorModeEnabled, cliOptions, t
@@ -72,6 +72,12 @@ const FilesPage = ({
7272
doFilesWrite(raw, root)
7373
}
7474

75+
const onBulkCidImport = (raw, root = '') => {
76+
if (root === '') root = files.path
77+
78+
doFilesBulkCidImport(raw, root)
79+
}
80+
7581
const onAddByPath = (path, name) => doFilesAddPath(files.path, path, name)
7682
const onInspect = (cid) => doUpdateHash(`/explore/${cid}`)
7783
const showModal = (modal, files = null) => setModals({ show: modal, files })
@@ -206,6 +212,7 @@ const FilesPage = ({
206212
onAddFiles={onAddFiles}
207213
onMove={doFilesMove}
208214
onAddByPath={(files) => showModal(ADD_BY_PATH, files)}
215+
onBulkCidImport={(files) => showModal(BULK_CID_IMPORT, files)}
209216
onNewFolder={(files) => showModal(NEW_FOLDER, files)}
210217
onCliTutorMode={() => showModal(CLI_TUTOR_MODE)}
211218
handleContextMenu={(...args) => handleContextMenu(...args, true)} />
@@ -226,6 +233,7 @@ const FilesPage = ({
226233
onShareLink={doFilesShareLink}
227234
onRemove={doFilesDelete}
228235
onAddByPath={onAddByPath}
236+
onBulkCidImport={onBulkCidImport}
229237
onPinningSet={doSetPinning}
230238
onPublish={doPublishIpnsKey}
231239
cliOptions={cliOptions}
@@ -277,6 +285,7 @@ export default connect(
277285
'selectFilesSorting',
278286
'selectToursEnabled',
279287
'doFilesWrite',
288+
'doFilesBulkCidImport',
280289
'doFilesDownloadLink',
281290
'doFilesDownloadCarLink',
282291
'doFilesSizeGet',

src/files/file-input/FileInput.js

+11-1
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,11 @@ class FileInput extends React.Component {
5050
this.toggleDropdown()
5151
}
5252

53+
onBulkCidImport = () => {
54+
this.props.onBulkCidImport()
55+
this.toggleDropdown()
56+
}
57+
5358
onNewFolder = () => {
5459
this.props.onNewFolder()
5560
this.toggleDropdown()
@@ -92,6 +97,10 @@ class FileInput extends React.Component {
9297
<NewFolderIcon className='fill-aqua w2 h2 mr1' />
9398
{t('newFolder')}
9499
</Option>
100+
<Option onClick={this.onBulkCidImport} id='bulk-cid-import'>
101+
<DocumentIcon className='fill-aqua w2 mr1' />
102+
{t('bulkImport')}
103+
</Option>
95104
</DropdownMenu>
96105
</Dropdown>
97106

@@ -120,7 +129,8 @@ FileInput.propTypes = {
120129
t: PropTypes.func.isRequired,
121130
onAddFiles: PropTypes.func.isRequired,
122131
onAddByPath: PropTypes.func.isRequired,
123-
onNewFolder: PropTypes.func.isRequired
132+
onNewFolder: PropTypes.func.isRequired,
133+
onBulkCidImport: PropTypes.func.isRequired
124134
}
125135

126136
export default connect(

src/files/header/Header.js

+1
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ class Header extends React.Component {
9393
onNewFolder={this.props.onNewFolder}
9494
onAddFiles={this.props.onAddFiles}
9595
onAddByPath={this.props.onAddByPath}
96+
onBulkCidImport={this.props.onBulkCidImport}
9697
onCliTutorMode={this.props.onCliTutorMode}
9798
/>
9899
: <div ref={el => { this.dotsWrapper = el }}>

src/files/modals/Modals.js

+18
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import RenameModal from './rename-modal/RenameModal.js'
1010
import PinningModal from './pinning-modal/PinningModal.js'
1111
import RemoveModal from './remove-modal/RemoveModal.js'
1212
import AddByPathModal from './add-by-path-modal/AddByPathModal.js'
13+
import BulkImportModal from './bulk-import-modal/bulk-import-modal.tsx'
1314
import PublishModal from './publish-modal/PublishModal.js'
1415
import CliTutorMode from '../../components/cli-tutor-mode/CliTutorMode.js'
1516
import { cliCommandList, cliCmdKeys } from '../../bundles/files/consts.js'
@@ -20,6 +21,7 @@ const SHARE = 'share'
2021
const RENAME = 'rename'
2122
const DELETE = 'delete'
2223
const ADD_BY_PATH = 'add_by_path'
24+
const BULK_CID_IMPORT = 'bulk_cid_import'
2325
const CLI_TUTOR_MODE = 'cli_tutor_mode'
2426
const PINNING = 'pinning'
2527
const PUBLISH = 'publish'
@@ -30,6 +32,7 @@ export {
3032
RENAME,
3133
DELETE,
3234
ADD_BY_PATH,
35+
BULK_CID_IMPORT,
3336
CLI_TUTOR_MODE,
3437
PINNING,
3538
PUBLISH
@@ -63,6 +66,11 @@ class Modals extends React.Component {
6366
this.leave()
6467
}
6568

69+
onBulkCidImport = (files, root) => {
70+
this.props.onBulkCidImport(files, root)
71+
this.leave()
72+
}
73+
6674
makeDir = (path) => {
6775
this.props.onMakeDir(join(this.props.root, path))
6876
this.leave()
@@ -152,6 +160,9 @@ class Modals extends React.Component {
152160
case ADD_BY_PATH:
153161
this.setState({ readyToShow: true })
154162
break
163+
case BULK_CID_IMPORT:
164+
this.setState({ readyToShow: true })
165+
break
155166
case CLI_TUTOR_MODE:
156167
this.setState({ command: this.cliCommand(cliOptions, files, root) }, () => {
157168
this.setState({ readyToShow: true })
@@ -254,6 +265,13 @@ class Modals extends React.Component {
254265
onCancel={this.leave} />
255266
</Overlay>
256267

268+
<Overlay show={show === BULK_CID_IMPORT && readyToShow} onLeave={this.leave}>
269+
<BulkImportModal
270+
className='outline-0'
271+
onBulkCidImport={this.onBulkCidImport}
272+
onCancel={this.leave} />
273+
</Overlay>
274+
257275
<Overlay show={show === CLI_TUTOR_MODE && readyToShow} onLeave={this.leave}>
258276
<CliTutorMode onLeave={this.leave} filesPage={true} command={command} t={t}/>
259277
</Overlay>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import React from 'react'
2+
import { action } from '@storybook/addon-actions'
3+
import i18n from '../../../i18n-decorator.js'
4+
import BulkImportModal from './bulk-import-modal.tsx'
5+
6+
/**
7+
* @type {import('@storybook/react').Meta}
8+
*/
9+
export default {
10+
title: 'Files/Modals',
11+
decorators: [i18n]
12+
}
13+
14+
/**
15+
* @type {import('@storybook/react').StoryObj}
16+
*/
17+
export const BulkImport = () => (
18+
<div className="ma3">
19+
<BulkImportModal onCancel={action('Cancel')} onBulkCidImport={action('Bulk CID Import')} />
20+
</div>
21+
)

0 commit comments

Comments
 (0)