frontend: Replace tracks with new file through upload in edit page
This commit is contained in:
parent
40882549f7
commit
cc4679d048
64
frontend/src/components/FileUploadField.tsx
Normal file
64
frontend/src/components/FileUploadField.tsx
Normal file
|
@ -0,0 +1,64 @@
|
|||
import React from 'react'
|
||||
import {Icon, Segment, Header, Button} from 'semantic-ui-react'
|
||||
|
||||
import {FileDrop} from 'components'
|
||||
|
||||
export default function FileUploadField({onSelect: onSelect_, multiple}) {
|
||||
const labelRef = React.useRef()
|
||||
const [labelRefState, setLabelRefState] = React.useState()
|
||||
|
||||
const onSelect = multiple ? onSelect_ : (files) => onSelect_(files?.[0])
|
||||
|
||||
React.useLayoutEffect(
|
||||
() => {
|
||||
setLabelRefState(labelRef.current)
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[labelRef.current]
|
||||
)
|
||||
|
||||
function onChangeField(e) {
|
||||
if (e.target.files && e.target.files.length) {
|
||||
onSelect(e.target.files)
|
||||
}
|
||||
e.target.value = '' // reset the form field for uploading again
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<input
|
||||
type="file"
|
||||
id="upload-field"
|
||||
style={{width: 0, height: 0, position: 'fixed', left: -1000, top: -1000, opacity: 0.001}}
|
||||
multiple={multiple}
|
||||
accept=".csv"
|
||||
onChange={onChangeField}
|
||||
/>
|
||||
<label htmlFor="upload-field" ref={labelRef}>
|
||||
{labelRefState && (
|
||||
<FileDrop onDrop={onSelect} frame={labelRefState}>
|
||||
{({draggingOverFrame, draggingOverTarget, onDragOver, onDragLeave, onDrop, onClick}) => (
|
||||
<Segment
|
||||
placeholder
|
||||
{...{onDragOver, onDragLeave, onDrop}}
|
||||
style={{
|
||||
background: draggingOverTarget || draggingOverFrame ? '#E0E0EE' : null,
|
||||
transition: 'background 0.2s',
|
||||
}}
|
||||
>
|
||||
<Header icon>
|
||||
<Icon name="cloud upload" />
|
||||
Drop file{multiple ? 's' : ''} here or click to select {multiple ? 'them' : 'one'} for upload
|
||||
</Header>
|
||||
|
||||
<Button primary as="span">
|
||||
Upload file{multiple ? 's' : ''}
|
||||
</Button>
|
||||
</Segment>
|
||||
)}
|
||||
</FileDrop>
|
||||
)}
|
||||
</label>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
export {default as FileDrop} from './FileDrop'
|
||||
export {default as FileUploadField} from './FileUploadField'
|
||||
export {default as FormattedDate} from './FormattedDate'
|
||||
export {default as LoginButton} from './LoginButton'
|
||||
export {default as Map} from './Map'
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import React from 'react'
|
||||
import _ from 'lodash'
|
||||
import {connect} from 'react-redux'
|
||||
import {Confirm, Grid, Button, Icon, Popup, Form, Ref, TextArea, Checkbox} from 'semantic-ui-react'
|
||||
import {useHistory, useParams} from 'react-router-dom'
|
||||
import {Divider, Message, Confirm, Grid, Button, Icon, Popup, Form, Ref, TextArea, Checkbox} from 'semantic-ui-react'
|
||||
import {useHistory, useParams, Link} from 'react-router-dom'
|
||||
import {concat, of, from} from 'rxjs'
|
||||
import {pluck, distinctUntilChanged, map, switchMap} from 'rxjs/operators'
|
||||
import {useObservable} from 'rxjs-hooks'
|
||||
|
@ -10,9 +10,32 @@ import {findInput} from 'utils'
|
|||
import {useForm, Controller} from 'react-hook-form'
|
||||
|
||||
import api from 'api'
|
||||
import {Page} from 'components'
|
||||
import {Page, FileUploadField} from 'components'
|
||||
import type {Track} from 'types'
|
||||
|
||||
import {FileUploadStatus} from 'pages/UploadPage'
|
||||
|
||||
function ReplaceTrackData({slug}) {
|
||||
const [file, setFile] = React.useState(null)
|
||||
const [result, setResult] = React.useState(null)
|
||||
const onComplete = React.useCallback((_id, r) => setResult(r), [setResult])
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2>Replace track data</h2>
|
||||
{!file ? (
|
||||
<FileUploadField onSelect={setFile} />
|
||||
) : result ? (
|
||||
<Message>
|
||||
Upload complete. <Link to={`/tracks/${slug}`}>Show track</Link>
|
||||
</Message>
|
||||
) : (
|
||||
<FileUploadStatus {...{file, onComplete, slug}} />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const TrackEditor = connect((state) => ({login: state.login}))(function TrackEditor({login}) {
|
||||
const [busy, setBusy] = React.useState(false)
|
||||
const {register, control, handleSubmit} = useForm()
|
||||
|
@ -75,7 +98,7 @@ const TrackEditor = connect((state) => ({login: state.login}))(function TrackEdi
|
|||
<Grid centered relaxed divided>
|
||||
<Grid.Row>
|
||||
<Grid.Column width={10}>
|
||||
<h2>Edit {track ? (track.title || 'Unnamed track') : 'track'}</h2>
|
||||
<h2>Edit {track ? track.title || 'Unnamed track' : 'track'}</h2>
|
||||
<Form loading={loading} key={track?.slug} onSubmit={onSubmit}>
|
||||
<Ref innerRef={findInput(register)}>
|
||||
<Form.Input label="Title" name="title" defaultValue={track?.title} style={{fontSize: '120%'}} />
|
||||
|
@ -136,13 +159,19 @@ const TrackEditor = connect((state) => ({login: state.login}))(function TrackEdi
|
|||
</Form>
|
||||
</Grid.Column>
|
||||
<Grid.Column width={6}>
|
||||
<ReplaceTrackData slug={slug} />
|
||||
|
||||
<Divider />
|
||||
|
||||
<h2>Danger zone</h2>
|
||||
<p>
|
||||
You can remove this track from your account and the portal if you like. However, if at any point you have
|
||||
published this track, we cannot guarantee that there are no versions of it in the public data repository,
|
||||
or any copy thereof.
|
||||
</p>
|
||||
<Button color="red" onClick={() => setConfirmDelete(true)}>Delete</Button>
|
||||
<Button color="red" onClick={() => setConfirmDelete(true)}>
|
||||
Delete
|
||||
</Button>
|
||||
<Confirm open={confirmDelete} onCancel={() => setConfirmDelete(false)} onConfirm={onDelete} />
|
||||
</Grid.Column>
|
||||
</Grid.Row>
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import _ from 'lodash'
|
||||
import React from 'react'
|
||||
import {List, Loader, Table, Icon, Segment, Header, Button} from 'semantic-ui-react'
|
||||
import {List, Loader, Table, Icon} from 'semantic-ui-react'
|
||||
import {Link} from 'react-router-dom'
|
||||
|
||||
import {FileDrop, Page} from 'components'
|
||||
import {FileUploadField, Page} from 'components'
|
||||
import type {Track} from 'types'
|
||||
import api from 'api'
|
||||
|
||||
|
@ -40,14 +40,16 @@ type FileUploadResult =
|
|||
errors: Record<string, string>
|
||||
}
|
||||
|
||||
function FileUploadStatus({
|
||||
export function FileUploadStatus({
|
||||
id,
|
||||
file,
|
||||
onComplete,
|
||||
slug,
|
||||
}: {
|
||||
id: string
|
||||
file: File
|
||||
onComplete: (result: FileUploadResult) => void
|
||||
slug?: string
|
||||
}) {
|
||||
const [progress, setProgress] = React.useState(0)
|
||||
|
||||
|
@ -70,7 +72,11 @@ function FileUploadStatus({
|
|||
xhr.responseType = 'json'
|
||||
xhr.onload = onLoad
|
||||
xhr.upload.onprogress = onProgress
|
||||
if (slug) {
|
||||
xhr.open('PUT', `/api/tracks/${slug}`)
|
||||
} else {
|
||||
xhr.open('POST', '/api/tracks')
|
||||
}
|
||||
|
||||
api.getValidAccessToken().then((accessToken) => {
|
||||
xhr.setRequestHeader('Authorization', accessToken)
|
||||
|
@ -85,7 +91,8 @@ function FileUploadStatus({
|
|||
|
||||
return (
|
||||
<span>
|
||||
<Loader inline size="mini" active /> {progress < 1 ? `Uploading ${(progress * 100).toFixed(0)}%` : 'Processing...'}
|
||||
<Loader inline size="mini" active />{' '}
|
||||
{progress < 1 ? `Uploading ${(progress * 100).toFixed(0)}%` : 'Processing...'}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
@ -99,9 +106,6 @@ type FileEntry = {
|
|||
}
|
||||
|
||||
export default function UploadPage() {
|
||||
const labelRef = React.useRef()
|
||||
const [labelRefState, setLabelRefState] = React.useState()
|
||||
|
||||
const [files, setFiles] = React.useState<FileEntry[]>([])
|
||||
|
||||
const onCompleteFileUpload = React.useCallback(
|
||||
|
@ -111,14 +115,6 @@ export default function UploadPage() {
|
|||
[setFiles]
|
||||
)
|
||||
|
||||
React.useLayoutEffect(
|
||||
() => {
|
||||
setLabelRefState(labelRef.current)
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[labelRef.current]
|
||||
)
|
||||
|
||||
function onSelectFiles(fileList) {
|
||||
const newFiles = Array.from(fileList).map((file) => ({
|
||||
id: 'file-' + String(Math.floor(Math.random() * 1000000)),
|
||||
|
@ -129,18 +125,6 @@ export default function UploadPage() {
|
|||
setFiles(files.filter((a) => !newFiles.some((b) => isSameFile(a, b))).concat(newFiles))
|
||||
}
|
||||
|
||||
function onChangeField(e) {
|
||||
if (e.target.files && e.target.files.length) {
|
||||
onSelectFiles(e.target.files)
|
||||
}
|
||||
e.target.value = '' // reset the form field for uploading again
|
||||
}
|
||||
|
||||
async function onDeleteTrack(slug: string) {
|
||||
await api.delete(`/tracks/${slug}`)
|
||||
setFiles((files) => files.filter((t) => t.result?.track?.slug !== slug))
|
||||
}
|
||||
|
||||
return (
|
||||
<Page>
|
||||
{files.length ? (
|
||||
|
@ -192,39 +176,7 @@ export default function UploadPage() {
|
|||
</Table>
|
||||
) : null}
|
||||
|
||||
<input
|
||||
type="file"
|
||||
id="upload-field"
|
||||
style={{width: 0, height: 0, position: 'fixed', left: -1000, top: -1000, opacity: 0.001}}
|
||||
multiple
|
||||
accept=".csv"
|
||||
onChange={onChangeField}
|
||||
/>
|
||||
<label htmlFor="upload-field" ref={labelRef}>
|
||||
{labelRefState && (
|
||||
<FileDrop onDrop={onSelectFiles} frame={labelRefState}>
|
||||
{({draggingOverFrame, draggingOverTarget, onDragOver, onDragLeave, onDrop, onClick}) => (
|
||||
<Segment
|
||||
placeholder
|
||||
{...{onDragOver, onDragLeave, onDrop}}
|
||||
style={{
|
||||
background: draggingOverTarget || draggingOverFrame ? '#E0E0EE' : null,
|
||||
transition: 'background 0.2s',
|
||||
}}
|
||||
>
|
||||
<Header icon>
|
||||
<Icon name="cloud upload" />
|
||||
Drop files here or click to select them for upload
|
||||
</Header>
|
||||
|
||||
<Button primary as="span">
|
||||
Upload files
|
||||
</Button>
|
||||
</Segment>
|
||||
)}
|
||||
</FileDrop>
|
||||
)}
|
||||
</label>
|
||||
<FileUploadField onSelect={onSelectFiles} multiple />
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue