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 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'
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
if (slug) {
|
||||||
|
xhr.open('PUT', `/api/tracks/${slug}`)
|
||||||
|
} else {
|
||||||
xhr.open('POST', '/api/tracks')
|
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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue