frontend: Replace tracks with new file through upload in edit page

This commit is contained in:
Paul Bienkowski 2021-02-26 23:10:10 +01:00
parent 40882549f7
commit cc4679d048
4 changed files with 112 additions and 66 deletions

View 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>
</>
)
}

View file

@ -1,4 +1,5 @@
export {default as FileDrop} from './FileDrop' export {default as FileDrop} from './FileDrop'
export {default as FileUploadField} from './FileUploadField'
export {default as FormattedDate} from './FormattedDate' export {default as FormattedDate} from './FormattedDate'
export {default as LoginButton} from './LoginButton' export {default as LoginButton} from './LoginButton'
export {default as Map} from './Map' export {default as Map} from './Map'

View file

@ -1,8 +1,8 @@
import React from 'react' import React from 'react'
import _ from 'lodash' import _ from 'lodash'
import {connect} from 'react-redux' import {connect} from 'react-redux'
import {Confirm, Grid, Button, Icon, Popup, Form, Ref, TextArea, Checkbox} from 'semantic-ui-react' import {Divider, Message, Confirm, Grid, Button, Icon, Popup, Form, Ref, TextArea, Checkbox} from 'semantic-ui-react'
import {useHistory, useParams} from 'react-router-dom' import {useHistory, useParams, Link} from 'react-router-dom'
import {concat, of, from} from 'rxjs' import {concat, of, from} from 'rxjs'
import {pluck, distinctUntilChanged, map, switchMap} from 'rxjs/operators' import {pluck, distinctUntilChanged, map, switchMap} from 'rxjs/operators'
import {useObservable} from 'rxjs-hooks' import {useObservable} from 'rxjs-hooks'
@ -10,9 +10,32 @@ import {findInput} from 'utils'
import {useForm, Controller} from 'react-hook-form' import {useForm, Controller} from 'react-hook-form'
import api from 'api' import api from 'api'
import {Page} from 'components' import {Page, FileUploadField} from 'components'
import type {Track} from 'types' 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 TrackEditor = connect((state) => ({login: state.login}))(function TrackEditor({login}) {
const [busy, setBusy] = React.useState(false) const [busy, setBusy] = React.useState(false)
const {register, control, handleSubmit} = useForm() const {register, control, handleSubmit} = useForm()
@ -75,7 +98,7 @@ const TrackEditor = connect((state) => ({login: state.login}))(function TrackEdi
<Grid centered relaxed divided> <Grid centered relaxed divided>
<Grid.Row> <Grid.Row>
<Grid.Column width={10}> <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}> <Form loading={loading} key={track?.slug} onSubmit={onSubmit}>
<Ref innerRef={findInput(register)}> <Ref innerRef={findInput(register)}>
<Form.Input label="Title" name="title" defaultValue={track?.title} style={{fontSize: '120%'}} /> <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> </Form>
</Grid.Column> </Grid.Column>
<Grid.Column width={6}> <Grid.Column width={6}>
<ReplaceTrackData slug={slug} />
<Divider />
<h2>Danger zone</h2> <h2>Danger zone</h2>
<p> <p>
You can remove this track from your account and the portal if you like. However, if at any point you have 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, published this track, we cannot guarantee that there are no versions of it in the public data repository,
or any copy thereof. or any copy thereof.
</p> </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} /> <Confirm open={confirmDelete} onCancel={() => setConfirmDelete(false)} onConfirm={onDelete} />
</Grid.Column> </Grid.Column>
</Grid.Row> </Grid.Row>

View file

@ -1,9 +1,9 @@
import _ from 'lodash' import _ from 'lodash'
import React from 'react' 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 {Link} from 'react-router-dom'
import {FileDrop, Page} from 'components' import {FileUploadField, Page} from 'components'
import type {Track} from 'types' import type {Track} from 'types'
import api from 'api' import api from 'api'
@ -40,14 +40,16 @@ type FileUploadResult =
errors: Record<string, string> errors: Record<string, string>
} }
function FileUploadStatus({ export function FileUploadStatus({
id, id,
file, file,
onComplete, onComplete,
slug,
}: { }: {
id: string id: string
file: File file: File
onComplete: (result: FileUploadResult) => void onComplete: (result: FileUploadResult) => void
slug?: string
}) { }) {
const [progress, setProgress] = React.useState(0) const [progress, setProgress] = React.useState(0)
@ -70,7 +72,11 @@ function FileUploadStatus({
xhr.responseType = 'json' xhr.responseType = 'json'
xhr.onload = onLoad xhr.onload = onLoad
xhr.upload.onprogress = onProgress xhr.upload.onprogress = onProgress
xhr.open('POST', '/api/tracks') if (slug) {
xhr.open('PUT', `/api/tracks/${slug}`)
} else {
xhr.open('POST', '/api/tracks')
}
api.getValidAccessToken().then((accessToken) => { api.getValidAccessToken().then((accessToken) => {
xhr.setRequestHeader('Authorization', accessToken) xhr.setRequestHeader('Authorization', accessToken)
@ -85,7 +91,8 @@ function FileUploadStatus({
return ( return (
<span> <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> </span>
) )
} }
@ -99,9 +106,6 @@ type FileEntry = {
} }
export default function UploadPage() { export default function UploadPage() {
const labelRef = React.useRef()
const [labelRefState, setLabelRefState] = React.useState()
const [files, setFiles] = React.useState<FileEntry[]>([]) const [files, setFiles] = React.useState<FileEntry[]>([])
const onCompleteFileUpload = React.useCallback( const onCompleteFileUpload = React.useCallback(
@ -111,14 +115,6 @@ export default function UploadPage() {
[setFiles] [setFiles]
) )
React.useLayoutEffect(
() => {
setLabelRefState(labelRef.current)
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[labelRef.current]
)
function onSelectFiles(fileList) { function onSelectFiles(fileList) {
const newFiles = Array.from(fileList).map((file) => ({ const newFiles = Array.from(fileList).map((file) => ({
id: 'file-' + String(Math.floor(Math.random() * 1000000)), 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)) 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 ( return (
<Page> <Page>
{files.length ? ( {files.length ? (
@ -192,39 +176,7 @@ export default function UploadPage() {
</Table> </Table>
) : null} ) : null}
<input <FileUploadField onSelect={onSelectFiles} multiple />
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>
</Page> </Page>
) )
} }