frontend: Add download button for original file
This commit is contained in:
parent
31af59819e
commit
817de8fae5
5
frontend/package-lock.json
generated
5
frontend/package-lock.json
generated
|
@ -5107,6 +5107,11 @@
|
|||
"resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz",
|
||||
"integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA=="
|
||||
},
|
||||
"downloadjs": {
|
||||
"version": "1.4.7",
|
||||
"resolved": "https://registry.npmjs.org/downloadjs/-/downloadjs-1.4.7.tgz",
|
||||
"integrity": "sha1-9p+W+UDg0FU9rCkROYZaPNAQHjw="
|
||||
},
|
||||
"duplexer": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz",
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
"@types/react": "^17.0.1",
|
||||
"@types/react-dom": "^17.0.0",
|
||||
"classnames": "^2.2.6",
|
||||
"downloadjs": "^1.4.7",
|
||||
"luxon": "^1.25.0",
|
||||
"node-sass": "^4.14.1",
|
||||
"ol": "^6.5.0",
|
||||
|
|
|
@ -4,6 +4,20 @@ import {setAuth, invalidateAccessToken, resetAuth} from 'reducers/auth'
|
|||
import {setLogin} from 'reducers/login'
|
||||
import config from 'config.json'
|
||||
import {create as createPkce} from 'pkce'
|
||||
import download from 'downloadjs'
|
||||
|
||||
function getFileNameFromContentDispostionHeader(contentDisposition: string): string | undefined {
|
||||
const standardPattern = /filename=(["']?)(.+)\1/i
|
||||
const wrongPattern = /filename=([^"'][^;"'\n]+)/i
|
||||
|
||||
if (standardPattern.test(contentDisposition)) {
|
||||
return contentDisposition.match(standardPattern)[2]
|
||||
}
|
||||
|
||||
if (wrongPattern.test(contentDisposition)) {
|
||||
return contentDisposition.match(wrongPattern)[1]
|
||||
}
|
||||
}
|
||||
|
||||
class RequestError extends Error {
|
||||
constructor(message, errors) {
|
||||
|
@ -122,9 +136,9 @@ class API {
|
|||
}
|
||||
|
||||
async exchangeAuthorizationCode(code) {
|
||||
const codeVerifier = localStorage.getItem('codeVerifier');
|
||||
const codeVerifier = localStorage.getItem('codeVerifier')
|
||||
if (!codeVerifier) {
|
||||
throw new Error("No code verifier found");
|
||||
throw new Error('No code verifier found')
|
||||
}
|
||||
|
||||
const {tokenEndpoint} = await this.getAuthorizationServerMetadata()
|
||||
|
@ -154,7 +168,7 @@ class API {
|
|||
const {authorizationEndpoint} = await this.getAuthorizationServerMetadata()
|
||||
|
||||
const {codeVerifier, codeChallenge} = createPkce()
|
||||
localStorage.setItem("codeVerifier", codeVerifier);
|
||||
localStorage.setItem('codeVerifier', codeVerifier)
|
||||
|
||||
const loginUrl = new URL(authorizationEndpoint)
|
||||
loginUrl.searchParams.append('client_id', config.auth.clientId)
|
||||
|
@ -172,10 +186,12 @@ class API {
|
|||
async fetch(url, options = {}) {
|
||||
const accessToken = await this.getValidAccessToken()
|
||||
|
||||
const {returnResponse = false, ...fetchOptions} = options
|
||||
|
||||
const response = await window.fetch(config.apiUrl + '/api' + url, {
|
||||
...options,
|
||||
...fetchOptions,
|
||||
headers: {
|
||||
...(options.headers || {}),
|
||||
...(fetchOptions.headers || {}),
|
||||
Authorization: accessToken,
|
||||
},
|
||||
})
|
||||
|
@ -189,12 +205,22 @@ class API {
|
|||
throw new Error('401 Unauthorized')
|
||||
}
|
||||
|
||||
let json
|
||||
try {
|
||||
json = await response.json()
|
||||
} catch (err) {
|
||||
json = null
|
||||
if (returnResponse) {
|
||||
if (response.status === 200) {
|
||||
return response
|
||||
} else if (response.status === 204) {
|
||||
return null
|
||||
} else {
|
||||
throw new RequestError('Error code ' + response.status)
|
||||
}
|
||||
}
|
||||
|
||||
let json
|
||||
try {
|
||||
json = await response.json()
|
||||
} catch (err) {
|
||||
json = null
|
||||
}
|
||||
|
||||
if (response.status === 200) {
|
||||
return json
|
||||
|
@ -244,6 +270,18 @@ class API {
|
|||
scope: tokenResponse.scope,
|
||||
}
|
||||
}
|
||||
|
||||
async downloadFile(url, options = {}) {
|
||||
const res = await this.fetch(url, {returnResponse: true, ...options})
|
||||
const blob = await res.blob()
|
||||
const filename = getFileNameFromContentDispostionHeader(res.headers.get('content-disposition'))
|
||||
const contentType = res.headers.get('content-type')
|
||||
|
||||
// Apparently this workaround is needed for some browsers
|
||||
const newBlob = new Blob([blob], {type: contentType})
|
||||
|
||||
download(newBlob, filename, contentType)
|
||||
}
|
||||
}
|
||||
|
||||
const api = new API(globalStore)
|
||||
|
|
|
@ -1,13 +1,42 @@
|
|||
import React from 'react'
|
||||
import {Link} from 'react-router-dom'
|
||||
import {Button} from 'semantic-ui-react'
|
||||
import {Icon, Popup, Button, Dropdown} from 'semantic-ui-react'
|
||||
|
||||
export default function TrackActions({slug}) {
|
||||
export default function TrackActions({slug, isAuthor, onDownloadOriginal}) {
|
||||
return (
|
||||
<Button.Group vertical>
|
||||
<Link to={`/tracks/${slug}/edit`}>
|
||||
<Button primary>Edit track</Button>
|
||||
</Link>
|
||||
</Button.Group>
|
||||
<>
|
||||
{isAuthor ? (
|
||||
<Dropdown text="Download" button>
|
||||
<Dropdown.Menu>
|
||||
<Popup
|
||||
content={
|
||||
<>
|
||||
<p>Only you, the author of this track, can download the original file.</p>
|
||||
<p>
|
||||
This is the file as it was uploaded to the server, without modifications, and it can be used with
|
||||
other tools. Exporting to other formats, and downloading modified files, will be implemented soon.
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
trigger={<Dropdown.Item text="Original" onClick={onDownloadOriginal} />}
|
||||
/>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
) : (
|
||||
<>
|
||||
<Button disabled>Download</Button>
|
||||
<Popup
|
||||
content={<p>Only the author of this track can download the original file.</p>}
|
||||
trigger={<Icon name="info circle" />}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{isAuthor && (
|
||||
<Link to={`/tracks/${slug}/edit`}>
|
||||
<Button primary>Edit track</Button>
|
||||
</Link>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -20,6 +20,11 @@ export default function TrackDetails({track, isAuthor}) {
|
|||
|
||||
{track.originalFileName != null && (
|
||||
<List.Item>
|
||||
{isAuthor && (
|
||||
<div style={{float: 'right'}}>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<List.Header>Original Filename</List.Header>
|
||||
<code>{track.originalFileName}</code>
|
||||
</List.Item>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react'
|
||||
import {connect} from 'react-redux'
|
||||
import {Button, Table, Checkbox, Segment, Dimmer, Grid, Loader, Header, Message} from 'semantic-ui-react'
|
||||
import {Table, Checkbox, Segment, Dimmer, Grid, Loader, Header, Message} from 'semantic-ui-react'
|
||||
import {useParams, useHistory} from 'react-router-dom'
|
||||
import {concat, combineLatest, of, from, Subject} from 'rxjs'
|
||||
import {pluck, distinctUntilChanged, map, switchMap, startWith, catchError} from 'rxjs/operators'
|
||||
|
@ -106,6 +106,13 @@ const TrackPage = connect((state) => ({login: state.login}))(function TrackPage(
|
|||
[slug, reloadComments]
|
||||
)
|
||||
|
||||
const onDownloadOriginal = React.useCallback(
|
||||
() => {
|
||||
api.downloadFile(`/tracks/${slug}/download/original.csv`)
|
||||
},
|
||||
[slug]
|
||||
)
|
||||
|
||||
const isAuthor = login?.username === data?.track?.author?.username
|
||||
|
||||
const {track, trackData, comments} = data || {}
|
||||
|
@ -147,7 +154,7 @@ const TrackPage = connect((state) => ({login: state.login}))(function TrackPage(
|
|||
<>
|
||||
<Header as="h1">{track.title || 'Unnamed track'}</Header>
|
||||
<TrackDetails {...{track, isAuthor}} />
|
||||
{isAuthor && <TrackActions {...{slug}} />}
|
||||
<TrackActions {...{isAuthor, onDownloadOriginal, slug}} />
|
||||
</>
|
||||
)}
|
||||
</Segment>
|
||||
|
|
Loading…
Reference in a new issue