1
0
mirror of https://github.com/oceanprotocol/commons.git synced 2023-03-15 18:03:00 +01:00

add default env vars into CONNECTIONS cofig

This commit is contained in:
Max Berman 2020-01-15 12:13:17 +01:00
parent b4fc9883e2
commit ab342b3fa6
9 changed files with 503 additions and 462 deletions

View File

@ -9,28 +9,28 @@ import './styles/global.scss'
import styles from './App.module.scss' import styles from './App.module.scss'
export default class App extends Component { export default class App extends Component {
public render() { public render() {
return ( return (
<div className={styles.app}> <div className={styles.app}>
<Router> <Router>
<> <>
<Header /> <Header />
<main className={styles.main}> <main className={styles.main}>
{this.context.isLoading ? ( {this.context.isLoading ? (
<div className={styles.loader}> <div className={styles.loader}>
<Spinner message={this.context.message} /> <Spinner message={this.context.message} />
</div> </div>
) : ( ) : (
<Routes /> <Routes />
)} )}
</main> </main>
<Footer /> <Footer />
</> </>
</Router> </Router>
</div> </div>
) )
} }
} }
App.contextType = User App.contextType = User

View File

@ -8,34 +8,34 @@ import menu from '../../data/menu'
import meta from '../../data/meta.json' import meta from '../../data/meta.json'
const MenuItem = ({ item }: { item: any }) => ( const MenuItem = ({ item }: { item: any }) => (
<NavLink <NavLink
to={item.link} to={item.link}
className={styles.link} className={styles.link}
activeClassName={styles.linkActive} activeClassName={styles.linkActive}
exact exact
> >
{item.title} {item.title}
</NavLink> </NavLink>
) )
export default class Header extends PureComponent { export default class Header extends PureComponent {
public render() { public render() {
return ( return (
<header className={styles.header}> <header className={styles.header}>
<div className={styles.headerContent}> <div className={styles.headerContent}>
<NavLink to="/" className={styles.headerLogo}> <NavLink to="/" className={styles.headerLogo}>
<Logo className={styles.headerLogoImage} /> <Logo className={styles.headerLogoImage} />
<h1 className={styles.headerTitle}>{meta.title}</h1> <h1 className={styles.headerTitle}>{meta.title}</h1>
</NavLink> </NavLink>
<nav className={styles.headerMenu}> <nav className={styles.headerMenu}>
{menu.map(item => ( {menu.map(item => (
<MenuItem key={item.title} item={item} /> <MenuItem key={item.title} item={item} />
))} ))}
<AccountStatus className={styles.accountStatus} /> <AccountStatus className={styles.accountStatus} />
</nav> </nav>
</div> </div>
</header> </header>
) )
} }
} }

View File

@ -9,151 +9,153 @@ import ReactGA from 'react-ga'
import cleanupContentType from '../../../utils/cleanupContentType' import cleanupContentType from '../../../utils/cleanupContentType'
export const messages: any = { export const messages: any = {
99: 'Decrypting file URL...', 99: 'Decrypting file URL...',
0: '1/3<br />Asking for agreement signature...', 0: '1/3<br />Asking for agreement signature...',
1: '1/3<br />Agreement initialized.', 1: '1/3<br />Agreement initialized.',
2: '2/3<br />Asking for two payment confirmations...', 2: '2/3<br />Asking for two payment confirmations...',
3: '2/3<br />Payment confirmed. Requesting access...', 3: '2/3<br />Payment confirmed. Requesting access...',
4: '3/3<br /> Access granted. Consuming file...' 4: '3/3<br /> Access granted. Consuming file...'
} }
interface AssetFileProps { interface AssetFileProps {
file: File file: File
ddo: DDO ddo: DDO
} }
interface AssetFileState { interface AssetFileState {
isLoading: boolean isLoading: boolean
error: string error: string
step: number step: number
} }
export default class AssetFile extends PureComponent< export default class AssetFile extends PureComponent<
AssetFileProps, AssetFileProps,
AssetFileState AssetFileState
> { > {
public static contextType = User public static contextType = User
public state = { public state = {
isLoading: false,
error: '',
step: 99
}
private resetState = () =>
this.setState({
isLoading: true,
error: '',
step: 99
})
private purchaseAsset = async (ddo: DDO, index: number) => {
this.resetState()
ReactGA.event({
category: 'Purchase',
action: 'purchaseAsset-start ' + ddo.id
})
const { ocean } = this.context
try {
const accounts = await ocean.accounts.list()
const service = ddo.findServiceByType('access')
const agreements = await ocean.keeper.conditions.accessSecretStoreCondition.getGrantedDidByConsumer(
accounts[0].id
)
const agreement = agreements.find((element: any) => {
return element.did === ddo.id
})
let agreementId
if (agreement) {
;({ agreementId } = agreement)
} else {
agreementId = await ocean.assets
.order(ddo.id, service.index, accounts[0])
.next((step: number) => this.setState({ step }))
}
// manually add another step here for better UX
this.setState({ step: 4 })
const path = await ocean.assets.consume(
agreementId,
ddo.id,
service.index,
accounts[0],
'',
index
)
Logger.log('path', path)
ReactGA.event({
category: 'Purchase',
action: 'purchaseAsset-end ' + ddo.id
})
this.setState({ isLoading: false })
} catch (error) {
Logger.error('error', error.message)
this.setState({
isLoading: false, isLoading: false,
error: `${error.message}. Sorry about that, can you try again?` error: '',
}) step: 99
ReactGA.event({
category: 'Purchase',
action: 'purchaseAsset-error ' + error.message
})
} }
}
public render() { private resetState = () =>
const { ddo, file } = this.props this.setState({
const { isLoading, error, step } = this.state isLoading: true,
const { isLogged } = this.context error: '',
const { index, contentType, contentLength } = file step: 99
})
return ( private purchaseAsset = async (ddo: DDO, index: number) => {
<div className={styles.fileWrap}> this.resetState()
<ul key={index} className={styles.file}>
{contentType || contentLength ? (
<>
<li>{cleanupContentType(contentType)}</li>
<li>
{contentLength && contentLength !== '0'
? filesize(contentLength)
: ''}
</li>
{/* <li>{encoding}</li> */}
{/* <li>{compression}</li> */}
</>
) : (
<li className={styles.empty}>No file info available</li>
)}
</ul>
{isLoading ? ( ReactGA.event({
<Spinner message={messages[step]} /> category: 'Purchase',
) : ( action: 'purchaseAsset-start ' + ddo.id
<Market.Consumer> })
{market => (
<Button
primary
className={styles.buttonMain}
// weird 0 hack so TypeScript is happy
onClick={() => this.purchaseAsset(ddo, index || 0)}
disabled={!isLogged || !market.networkMatch}
name="Download"
>
Get file
</Button>
)}
</Market.Consumer>
)}
{error !== '' && <div className={styles.error}>{error}</div>} const { ocean } = this.context
</div>
) try {
} const accounts = await ocean.accounts.list()
const service = ddo.findServiceByType('access')
const agreements = await ocean.keeper.conditions.accessSecretStoreCondition.getGrantedDidByConsumer(
accounts[0].id
)
const agreement = agreements.find((element: any) => {
return element.did === ddo.id
})
let agreementId
if (agreement) {
;({ agreementId } = agreement)
} else {
agreementId = await ocean.assets
.order(ddo.id, service.index, accounts[0])
.next((step: number) => this.setState({ step }))
}
// manually add another step here for better UX
this.setState({ step: 4 })
const path = await ocean.assets.consume(
agreementId,
ddo.id,
service.index,
accounts[0],
'',
index
)
Logger.log('path', path)
ReactGA.event({
category: 'Purchase',
action: 'purchaseAsset-end ' + ddo.id
})
this.setState({ isLoading: false })
} catch (error) {
Logger.error('error', error.message)
this.setState({
isLoading: false,
error: `${error.message}. Sorry about that, can you try again?`
})
ReactGA.event({
category: 'Purchase',
action: 'purchaseAsset-error ' + error.message
})
}
}
public render() {
const { ddo, file } = this.props
const { isLoading, error, step } = this.state
const { isLogged } = this.context
const { index, contentType, contentLength } = file
return (
<div className={styles.fileWrap}>
<ul key={index} className={styles.file}>
{contentType || contentLength ? (
<>
<li>{cleanupContentType(contentType)}</li>
<li>
{contentLength && contentLength !== '0'
? filesize(contentLength)
: ''}
</li>
{/* <li>{encoding}</li> */}
{/* <li>{compression}</li> */}
</>
) : (
<li className={styles.empty}>No file info available</li>
)}
</ul>
{isLoading ? (
<Spinner message={messages[step]} />
) : (
<Market.Consumer>
{market => (
<Button
primary
className={styles.buttonMain}
// weird 0 hack so TypeScript is happy
onClick={() =>
this.purchaseAsset(ddo, index || 0)
}
disabled={!isLogged || !market.networkMatch}
name="Download"
>
Get file
</Button>
)}
</Market.Consumer>
)}
{error !== '' && <div className={styles.error}>{error}</div>}
</div>
)
}
} }

View File

@ -27,27 +27,60 @@ export const ipfsNodeUri =
// //
export const CONNECTIONS = { export const CONNECTIONS = {
pacific: { pacific: {
nodeUri: 'https://pacific.oceanprotocol.com', nodeUri:
aquariusUri: 'https://aquarius.commons.oceanprotocol.com', process.env.REACT_APP_NODE_URI ||
brizoUri: 'https://brizo.commons.oceanprotocol.com', 'https://pacific.oceanprotocol.com',
brizoAddress: '0x008c25ed3594e094db4592f4115d5fa74c4f41ea', aquariusUri:
secretStoreUri: 'https://secret-store.oceanprotocol.com', process.env.REACT_APP_AQUARIUS_URI ||
faucetUri: 'https://faucet.oceanprotocol.com' 'https://aquarius.commons.oceanprotocol.com',
brizoUri:
process.env.REACT_APP_BRIZO_URI ||
'https://brizo.commons.oceanprotocol.com',
brizoAddress:
process.env.REACT_APP_BRIZO_ADDRESS ||
'0x008c25ed3594e094db4592f4115d5fa74c4f41ea',
secretStoreUri:
process.env.REACT_APP_SECRET_STORE_URI ||
'https://secret-store.oceanprotocol.com',
faucetUri:
process.env.REACT_APP_FAUCET_URI ||
'https://faucet.oceanprotocol.com'
}, },
nile: { nile: {
nodeUri: 'https://nile.dev-ocean.com', nodeUri: process.env.REACT_APP_NODE_URI || 'https://nile.dev-ocean.com',
aquariusUri: 'https://aquarius.nile.dev-ocean.com', aquariusUri:
brizoUri: 'https://brizo.nile.dev-ocean.com', process.env.REACT_APP_AQUARIUS_URI ||
brizoAddress: '0x4aaab179035dc57b35e2ce066919048686f82972', 'https://aquarius.nile.dev-ocean.com',
secretStoreUri: 'https://secret-store.nile.dev-ocean.com', brizoUri:
faucetUri: 'https://faucet.nile.dev-ocean.com' process.env.REACT_APP_BRIZO_URI ||
'https://brizo.nile.dev-ocean.com',
brizoAddress:
process.env.REACT_APP_BRIZO_ADDRESS ||
'0x4aaab179035dc57b35e2ce066919048686f82972',
secretStoreUri:
process.env.REACT_APP_SECRET_STORE_URI ||
'https://secret-store.nile.dev-ocean.com',
faucetUri:
process.env.REACT_APP_FAUCET_URI ||
'https://faucet.nile.dev-ocean.com'
}, },
duero: { duero: {
nodeUri: 'https://duero.dev-ocean.com', nodeUri:
aquariusUri: 'https://aquarius.duero.dev-ocean.com', process.env.REACT_APP_NODE_URI || 'https://duero.dev-ocean.com',
brizoUri: 'https://brizo.duero.dev-ocean.com', aquariusUri:
brizoAddress: '0x9d4ed58293f71122ad6a733c1603927a150735d0', process.env.REACT_APP_AQUARIUS_URI ||
secretStoreUri: 'https://secret-store.duero.dev-ocean.com', 'https://aquarius.duero.dev-ocean.com',
faucetUri: 'https://faucet.duero.dev-ocean.com' brizoUri:
process.env.REACT_APP_BRIZO_URI ||
'https://brizo.duero.dev-ocean.com',
brizoAddress:
process.env.REACT_APP_BRIZO_ADDRESS ||
'0x9d4ed58293f71122ad6a733c1603927a150735d0',
secretStoreUri:
process.env.REACT_APP_SECRET_STORE_URI ||
'https://secret-store.duero.dev-ocean.com',
faucetUri:
process.env.REACT_APP_FAUCET_URI ||
'https://faucet.duero.dev-ocean.com'
} }
} }

View File

@ -1,48 +1,48 @@
import Web3 from 'web3' import Web3 from 'web3'
export class MetamaskProvider { export class MetamaskProvider {
private web3: Web3 private web3: Web3
public constructor() { public constructor() {
// Default // Default
this.web3 = null as any this.web3 = null as any
// Modern dapp browsers // Modern dapp browsers
if (window.ethereum) { if (window.ethereum) {
this.web3 = new Web3(window.ethereum) this.web3 = new Web3(window.ethereum)
}
// Legacy dapp browsers
else if (window.web3) {
this.web3 = new Web3(window.web3.currentProvider)
}
} }
// Legacy dapp browsers
else if (window.web3) { public async isAvailable() {
this.web3 = new Web3(window.web3.currentProvider) return this.web3 !== null
} }
}
public async isAvailable() { public async isLogged() {
return this.web3 !== null if (this.web3 === null) return false
} if ((await this.web3.eth.getAccounts()).length > 0) {
return true
public async isLogged() { }
if (this.web3 === null) return false return false
if ((await this.web3.eth.getAccounts()).length > 0) {
return true
} }
return false
}
public async startLogin() { public async startLogin() {
try { try {
await window.ethereum.enable() await window.ethereum.enable()
localStorage.setItem('logType', 'Metamask') localStorage.setItem('logType', 'Metamask')
} catch (error) { } catch (error) {
return false return false
}
} }
}
public async logout() { public async logout() {
localStorage.removeItem('logType') localStorage.removeItem('logType')
// reload page? // reload page?
} }
public getProvider() { public getProvider() {
return this.web3 return this.web3
} }
} }

View File

@ -5,16 +5,16 @@ import App from './App'
import * as serviceWorker from './serviceWorker' import * as serviceWorker from './serviceWorker'
function renderToDOM() { function renderToDOM() {
const root = document.getElementById('root') const root = document.getElementById('root')
if (root !== null) { if (root !== null) {
ReactDOM.render( ReactDOM.render(
<UserProvider> <UserProvider>
<App /> <App />
</UserProvider>, </UserProvider>,
root root
) )
} }
} }
export { renderToDOM } export { renderToDOM }

View File

@ -1,22 +1,22 @@
import { MetaData } from '@oceanprotocol/squid' import { MetaData } from '@oceanprotocol/squid'
const AssetModel: MetaData = { const AssetModel: MetaData = {
// OEP-08 Attributes // OEP-08 Attributes
// https://github.com/oceanprotocol/OEPs/tree/master/8 // https://github.com/oceanprotocol/OEPs/tree/master/8
main: { main: {
type: 'dataset', type: 'dataset',
name: '', name: '',
dateCreated: '', dateCreated: '',
author: '', author: '',
license: '', license: '',
price: '', price: '',
files: [] files: []
}, },
additionalInformation: { additionalInformation: {
description: '', description: '',
copyrightHolder: '', copyrightHolder: '',
categories: [] categories: []
} }
} }
export default AssetModel export default AssetModel

View File

@ -10,48 +10,48 @@ const history = createMemoryHistory()
const location = createLocation('/faucet') const location = createLocation('/faucet')
const setup = () => { const setup = () => {
const utils = render( const utils = render(
<User.Provider value={userMockConnected}> <User.Provider value={userMockConnected}>
<Market.Provider <Market.Provider
value={{ value={{
network: 'pacific', network: 'pacific',
totalAssets: 100, totalAssets: 100,
categories: [''], categories: [''],
networkMatch: true networkMatch: true
}} }}
> >
<MemoryRouter> <MemoryRouter>
<Faucet <Faucet
history={history} history={history}
location={location} location={location}
match={{ params: '', path: '', url: '', isExact: true }} match={{ params: '', path: '', url: '', isExact: true }}
/> />
</MemoryRouter> </MemoryRouter>
</Market.Provider> </Market.Provider>
</User.Provider> </User.Provider>
) )
const button = utils.getByText('Request ETH') const button = utils.getByText('Request ETH')
const { container } = utils const { container } = utils
return { button, container, ...utils } return { button, container, ...utils }
} }
describe('Faucet', () => { describe('Faucet', () => {
it('renders without crashing', () => { it('renders without crashing', () => {
const { container } = setup() const { container } = setup()
expect(container.firstChild).toBeInTheDocument() expect(container.firstChild).toBeInTheDocument()
}) })
it('shows actions when connected', () => { it('shows actions when connected', () => {
const { button } = setup() const { button } = setup()
expect(button).toBeInTheDocument() expect(button).toBeInTheDocument()
expect(button).not.toHaveAttribute('disabled') expect(button).not.toHaveAttribute('disabled')
}) })
it('fires requestFromFaucet', async () => { it('fires requestFromFaucet', async () => {
await act(async () => { await act(async () => {
const { button } = setup() const { button } = setup()
fireEvent.click(button) fireEvent.click(button)
})
expect(userMockConnected.requestFromFaucet).toHaveBeenCalledTimes(1)
}) })
expect(userMockConnected.requestFromFaucet).toHaveBeenCalledTimes(1)
})
}) })

View File

@ -1,9 +1,9 @@
import React, { import React, {
lazy, lazy,
Suspense, Suspense,
FormEvent, FormEvent,
PureComponent, PureComponent,
ChangeEvent ChangeEvent
} from 'react' } from 'react'
import axios from 'axios' import axios from 'axios'
import { Logger, File } from '@oceanprotocol/squid' import { Logger, File } from '@oceanprotocol/squid'
@ -21,196 +21,202 @@ import Spinner from '../../../components/atoms/Spinner'
const Ipfs = lazy(() => import('./Ipfs')) const Ipfs = lazy(() => import('./Ipfs'))
export interface FilePublish extends File { export interface FilePublish extends File {
found: boolean // non-standard found: boolean // non-standard
} }
interface FilesProps { interface FilesProps {
files: File[] files: File[]
placeholder: string placeholder: string
help?: string help?: string
name: string name: string
onChange( onChange(
event: event:
| ChangeEvent<HTMLInputElement> | ChangeEvent<HTMLInputElement>
| FormEvent<HTMLInputElement> | FormEvent<HTMLInputElement>
| ChangeEvent<HTMLSelectElement> | ChangeEvent<HTMLSelectElement>
| ChangeEvent<HTMLTextAreaElement> | ChangeEvent<HTMLTextAreaElement>
): void ): void
} }
interface FilesStates { interface FilesStates {
isFormShown: boolean isFormShown: boolean
isIpfsFormShown: boolean isIpfsFormShown: boolean
} }
const buttons = [ const buttons = [
{ {
id: 'url', id: 'url',
title: '+ From URL', title: '+ From URL',
titleActive: '- Cancel' titleActive: '- Cancel'
}, },
{ {
id: 'ipfs', id: 'ipfs',
title: '+ Add to IPFS', title: '+ Add to IPFS',
titleActive: '- Cancel' titleActive: '- Cancel'
} }
] ]
export default class Files extends PureComponent<FilesProps, FilesStates> { export default class Files extends PureComponent<FilesProps, FilesStates> {
public state: FilesStates = { public state: FilesStates = {
isFormShown: false,
isIpfsFormShown: false
}
// for canceling axios requests
public signal = axios.CancelToken.source()
public componentWillUnmount() {
this.signal.cancel()
}
private toggleForm = (e: Event, form: string) => {
e.preventDefault()
this.setState({
isFormShown: form === 'url' ? !this.state.isFormShown : false,
isIpfsFormShown: form === 'ipfs' ? !this.state.isIpfsFormShown : false
})
}
private async getFile(url: string) {
const file: FilePublish = {
url,
contentType: '',
found: false // non-standard
}
try {
const response = await axios({
method: 'POST',
headers: { 'Content-Type': 'application/json' },
url: `${serviceUri}/api/v1/urlcheck`,
data: { url },
cancelToken: this.signal.token
})
const { contentLength, contentType, found } = response.data.result
if (contentLength) file.contentLength = contentLength
if (contentType) {
file.contentType = contentType
file.compression = cleanupContentType(contentType)
}
file.found = found
return file
} catch (error) {
!axios.isCancel(error) && Logger.error(error.message)
}
}
private addFile = async (url: string) => {
// check for duplicate urls
const duplicateFiles = this.props.files.filter(props =>
url.includes(props.url)
)
if (duplicateFiles.length > 0) {
return this.setState({
isFormShown: false, isFormShown: false,
isIpfsFormShown: false isIpfsFormShown: false
})
} }
const file: FilePublish | undefined = await this.getFile(url) // for canceling axios requests
file && this.props.files.push(file) public signal = axios.CancelToken.source()
const event = { public componentWillUnmount() {
currentTarget: { this.signal.cancel()
name: 'files',
value: this.props.files
}
} }
this.props.onChange(event as any)
this.setState({ private toggleForm = (e: Event, form: string) => {
isFormShown: false, e.preventDefault()
isIpfsFormShown: false
})
this.forceUpdate() this.setState({
} isFormShown: form === 'url' ? !this.state.isFormShown : false,
isIpfsFormShown:
private removeFile = (index: number) => { form === 'ipfs' ? !this.state.isIpfsFormShown : false
this.props.files.splice(index, 1) })
const event = {
currentTarget: {
name: 'files',
value: this.props.files
}
} }
this.props.onChange(event as any)
this.forceUpdate()
}
public render() { private async getFile(url: string) {
const { files, help, placeholder, name, onChange } = this.props const file: FilePublish = {
const { isFormShown, isIpfsFormShown } = this.state url,
contentType: '',
found: false // non-standard
}
return ( try {
<> const response = await axios({
{help && <Help>{help}</Help>} method: 'POST',
headers: { 'Content-Type': 'application/json' },
url: `${serviceUri}/api/v1/urlcheck`,
data: { url },
cancelToken: this.signal.token
})
{/* Use hidden input to collect files */} const { contentLength, contentType, found } = response.data.result
<input
type="hidden"
name={name}
value={JSON.stringify(files)}
onChange={onChange}
data-testid="files"
/>
<div className={styles.newItems}> if (contentLength) file.contentLength = contentLength
{files.length > 0 && ( if (contentType) {
<ul className={styles.itemsList}> file.contentType = contentType
{files.map((item: any, index: number) => ( file.compression = cleanupContentType(contentType)
<Item }
key={shortid.generate()}
item={item} file.found = found
removeFile={() => this.removeFile(index)}
return file
} catch (error) {
!axios.isCancel(error) && Logger.error(error.message)
}
}
private addFile = async (url: string) => {
// check for duplicate urls
const duplicateFiles = this.props.files.filter(props =>
url.includes(props.url)
)
if (duplicateFiles.length > 0) {
return this.setState({
isFormShown: false,
isIpfsFormShown: false
})
}
const file: FilePublish | undefined = await this.getFile(url)
file && this.props.files.push(file)
const event = {
currentTarget: {
name: 'files',
value: this.props.files
}
}
this.props.onChange(event as any)
this.setState({
isFormShown: false,
isIpfsFormShown: false
})
this.forceUpdate()
}
private removeFile = (index: number) => {
this.props.files.splice(index, 1)
const event = {
currentTarget: {
name: 'files',
value: this.props.files
}
}
this.props.onChange(event as any)
this.forceUpdate()
}
public render() {
const { files, help, placeholder, name, onChange } = this.props
const { isFormShown, isIpfsFormShown } = this.state
return (
<>
{help && <Help>{help}</Help>}
{/* Use hidden input to collect files */}
<input
type="hidden"
name={name}
value={JSON.stringify(files)}
onChange={onChange}
data-testid="files"
/> />
))}
</ul>
)}
{buttons.map(button => { <div className={styles.newItems}>
const isActive = {files.length > 0 && (
(button.id === 'url' && isFormShown) || <ul className={styles.itemsList}>
(button.id === 'ipfs' && isIpfsFormShown) {files.map((item: any, index: number) => (
<Item
key={shortid.generate()}
item={item}
removeFile={() => this.removeFile(index)}
/>
))}
</ul>
)}
return ( {buttons.map(button => {
<Button const isActive =
key={shortid.generate()} (button.id === 'url' && isFormShown) ||
link (button.id === 'ipfs' && isIpfsFormShown)
onClick={(e: Event) => this.toggleForm(e, button.id)}
>
{isActive ? button.titleActive : button.title}
</Button>
)
})}
{isFormShown && ( return (
<ItemForm placeholder={placeholder} addFile={this.addFile} /> <Button
)} key={shortid.generate()}
link
onClick={(e: Event) =>
this.toggleForm(e, button.id)
}
>
{isActive ? button.titleActive : button.title}
</Button>
)
})}
{isIpfsFormShown && ( {isFormShown && (
<Suspense fallback={<Spinner message="Loading..." />}> <ItemForm
<Ipfs addFile={this.addFile} /> placeholder={placeholder}
</Suspense> addFile={this.addFile}
)} />
</div> )}
</>
) {isIpfsFormShown && (
} <Suspense fallback={<Spinner message="Loading..." />}>
<Ipfs addFile={this.addFile} />
</Suspense>
)}
</div>
</>
)
}
} }