|
|
@ -1,24 +1,44 @@ |
|
|
|
import React, { useEffect } from 'react'; |
|
|
|
import React, { useEffect, useState } from 'react'; |
|
|
|
import { Box, Typography } from '@material-ui/core'; |
|
|
|
import { Box, Typography, IconButton, CircularProgress } from '@material-ui/core'; |
|
|
|
import LocalOfferIcon from '@material-ui/icons/LocalOffer'; |
|
|
|
import LocalOfferIcon from '@material-ui/icons/LocalOffer'; |
|
|
|
import * as serverApi from '../../api'; |
|
|
|
import * as serverApi from '../../api'; |
|
|
|
import { WindowState } from './Windows'; |
|
|
|
import { WindowState } from './Windows'; |
|
|
|
|
|
|
|
import StoreLinkIcon, { whichStore } from '../common/StoreLinkIcon'; |
|
|
|
|
|
|
|
import EditableText from '../common/EditableText'; |
|
|
|
|
|
|
|
import SubmitChangesButton from '../common/SubmitChangesButton'; |
|
|
|
|
|
|
|
import SongTable, { SongGetters } from '../tables/ResultsTable'; |
|
|
|
|
|
|
|
var _ = require('lodash'); |
|
|
|
|
|
|
|
|
|
|
|
export type TagMetadata = serverApi.TagDetails; |
|
|
|
export interface FullTagMetadata extends serverApi.TagDetails { |
|
|
|
|
|
|
|
fullName: string[], |
|
|
|
|
|
|
|
fullId: number[], |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export type TagMetadata = FullTagMetadata; |
|
|
|
|
|
|
|
export type TagMetadataChanges = serverApi.ModifyTagRequest; |
|
|
|
|
|
|
|
|
|
|
|
export interface TagWindowState extends WindowState { |
|
|
|
export interface TagWindowState extends WindowState { |
|
|
|
tagId: number, |
|
|
|
tagId: number, |
|
|
|
metadata: TagMetadata | null, |
|
|
|
metadata: TagMetadata | null, |
|
|
|
|
|
|
|
pendingChanges: TagMetadataChanges | null, |
|
|
|
|
|
|
|
songsWithTag: any[] | null, |
|
|
|
|
|
|
|
songGetters: SongGetters, |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
export enum TagWindowStateActions { |
|
|
|
export enum TagWindowStateActions { |
|
|
|
SetMetadata = "SetMetadata", |
|
|
|
SetMetadata = "SetMetadata", |
|
|
|
|
|
|
|
SetPendingChanges = "SetPendingChanges", |
|
|
|
|
|
|
|
SetSongs = "SetSongs", |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
export function TagWindowReducer(state: TagWindowState, action: any) { |
|
|
|
export function TagWindowReducer(state: TagWindowState, action: any) { |
|
|
|
switch (action.type) { |
|
|
|
switch (action.type) { |
|
|
|
case TagWindowStateActions.SetMetadata: |
|
|
|
case TagWindowStateActions.SetMetadata: |
|
|
|
return { ...state, metadata: action.value } |
|
|
|
return { ...state, metadata: action.value } |
|
|
|
|
|
|
|
case TagWindowStateActions.SetPendingChanges: |
|
|
|
|
|
|
|
return { ...state, pendingChanges: action.value } |
|
|
|
|
|
|
|
case TagWindowStateActions.SetSongs: |
|
|
|
|
|
|
|
return { ...state, songsWithTag: action.value } |
|
|
|
default: |
|
|
|
default: |
|
|
|
throw new Error("Unimplemented TagWindow state update.") |
|
|
|
throw new Error("Unimplemented TagWindow state update.") |
|
|
|
} |
|
|
|
} |
|
|
@ -61,23 +81,118 @@ export async function getTagMetadata(id: number) { |
|
|
|
const response = await fetch((process.env.REACT_APP_BACKEND || "") + serverApi.QueryEndpoint, requestOpts) |
|
|
|
const response = await fetch((process.env.REACT_APP_BACKEND || "") + serverApi.QueryEndpoint, requestOpts) |
|
|
|
let json: any = await response.json(); |
|
|
|
let json: any = await response.json(); |
|
|
|
let tag = json.tags[0]; |
|
|
|
let tag = json.tags[0]; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Recursively fetch parent tags to build the full metadata.
|
|
|
|
|
|
|
|
if (tag.parentId) { |
|
|
|
|
|
|
|
const parent = await getTagMetadata(tag.parentId); |
|
|
|
|
|
|
|
tag.fullName = [...parent.fullName, tag.name]; |
|
|
|
|
|
|
|
tag.fullId = [...parent.fullId, tag.tagId]; |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
tag.fullName = [tag.name]; |
|
|
|
|
|
|
|
tag.fullId = [tag.tagId]; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return tag; |
|
|
|
return tag; |
|
|
|
})(); |
|
|
|
})(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
export default function TagWindow(props: IProps) { |
|
|
|
export default function TagWindow(props: IProps) { |
|
|
|
let metadata = props.state.metadata; |
|
|
|
let metadata = props.state.metadata; |
|
|
|
|
|
|
|
let pendingChanges = props.state.pendingChanges; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Effect to get the tag's metadata.
|
|
|
|
useEffect(() => { |
|
|
|
useEffect(() => { |
|
|
|
getTagMetadata(props.state.tagId) |
|
|
|
getTagMetadata(props.state.tagId) |
|
|
|
.then((m: TagMetadata) => { |
|
|
|
.then((m: TagMetadata) => { |
|
|
|
console.log("metadata", m); |
|
|
|
|
|
|
|
props.dispatch({ |
|
|
|
props.dispatch({ |
|
|
|
type: TagWindowStateActions.SetMetadata, |
|
|
|
type: TagWindowStateActions.SetMetadata, |
|
|
|
value: m |
|
|
|
value: m |
|
|
|
}); |
|
|
|
}); |
|
|
|
}) |
|
|
|
}) |
|
|
|
}, [props.state.metadata?.name]); |
|
|
|
}, [metadata?.name]); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Effect to get the tag's songs.
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
|
|
|
|
if (props.state.songsWithTag) { return; } |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var q: serverApi.QueryRequest = { |
|
|
|
|
|
|
|
query: { |
|
|
|
|
|
|
|
prop: serverApi.QueryElemProperty.tagId, |
|
|
|
|
|
|
|
propOperator: serverApi.QueryFilterOp.Eq, |
|
|
|
|
|
|
|
propOperand: props.state.tagId, |
|
|
|
|
|
|
|
}, |
|
|
|
|
|
|
|
offsetsLimits: { |
|
|
|
|
|
|
|
songOffset: 0, |
|
|
|
|
|
|
|
songLimit: 100, |
|
|
|
|
|
|
|
}, |
|
|
|
|
|
|
|
ordering: { |
|
|
|
|
|
|
|
orderBy: { |
|
|
|
|
|
|
|
type: serverApi.OrderByType.Name, |
|
|
|
|
|
|
|
}, |
|
|
|
|
|
|
|
ascending: true, |
|
|
|
|
|
|
|
}, |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const requestOpts = { |
|
|
|
|
|
|
|
method: 'POST', |
|
|
|
|
|
|
|
headers: { 'Content-Type': 'application/json' }, |
|
|
|
|
|
|
|
body: JSON.stringify(q), |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
(async () => { |
|
|
|
|
|
|
|
const response = await fetch((process.env.REACT_APP_BACKEND || "") + serverApi.QueryEndpoint, requestOpts) |
|
|
|
|
|
|
|
let json: any = await response.json(); |
|
|
|
|
|
|
|
props.dispatch({ |
|
|
|
|
|
|
|
type: TagWindowStateActions.SetSongs, |
|
|
|
|
|
|
|
value: json.songs, |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
})(); |
|
|
|
|
|
|
|
}, [props.state.songsWithTag]); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const [editingName, setEditingName] = useState<string | null>(null); |
|
|
|
|
|
|
|
const name = <Typography variant="h4"><EditableText |
|
|
|
|
|
|
|
defaultValue={metadata?.name || "(Unknown name)"} |
|
|
|
|
|
|
|
changedValue={pendingChanges?.name || null} |
|
|
|
|
|
|
|
editingValue={editingName} |
|
|
|
|
|
|
|
editingLabel="Name" |
|
|
|
|
|
|
|
onChangeEditingValue={(v: string | null) => setEditingName(v)} |
|
|
|
|
|
|
|
onChangeChangedValue={(v: string | null) => { |
|
|
|
|
|
|
|
let newVal: any = { ...pendingChanges }; |
|
|
|
|
|
|
|
if (v) { newVal.name = v } |
|
|
|
|
|
|
|
else { delete newVal.name } |
|
|
|
|
|
|
|
props.dispatch({ |
|
|
|
|
|
|
|
type: TagWindowStateActions.SetPendingChanges, |
|
|
|
|
|
|
|
value: newVal, |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
}} |
|
|
|
|
|
|
|
/></Typography> |
|
|
|
|
|
|
|
const fullName = <Box display="flex" alignItems="center"> |
|
|
|
|
|
|
|
{metadata?.fullName.map((n: string, i: number) => { |
|
|
|
|
|
|
|
if (metadata?.fullName && i == metadata?.fullName.length - 1) { |
|
|
|
|
|
|
|
return name; |
|
|
|
|
|
|
|
} else if (i >= (metadata?.fullName.length || 0) - 1) { |
|
|
|
|
|
|
|
return undefined; |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
return <Typography variant="h4">{n} / </Typography> |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
})} |
|
|
|
|
|
|
|
</Box> |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const storeLinks = metadata?.storeLinks && metadata?.storeLinks.map((link: string) => { |
|
|
|
|
|
|
|
const store = whichStore(link); |
|
|
|
|
|
|
|
return store && <a |
|
|
|
|
|
|
|
href={link} target="_blank" |
|
|
|
|
|
|
|
> |
|
|
|
|
|
|
|
<IconButton><StoreLinkIcon |
|
|
|
|
|
|
|
whichStore={store} |
|
|
|
|
|
|
|
style={{ height: '40px', width: '40px' }} |
|
|
|
|
|
|
|
/> |
|
|
|
|
|
|
|
</IconButton> |
|
|
|
|
|
|
|
</a> |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const maybeSubmitButton = pendingChanges && Object.keys(pendingChanges).length > 0 && |
|
|
|
|
|
|
|
<SubmitChangesButton /> |
|
|
|
|
|
|
|
|
|
|
|
return <Box width="100%" justifyContent="center" display="flex" flexWrap="wrap"> |
|
|
|
return <Box width="100%" justifyContent="center" display="flex" flexWrap="wrap"> |
|
|
|
<Box |
|
|
|
<Box |
|
|
@ -85,13 +200,42 @@ export default function TagWindow(props: IProps) { |
|
|
|
mt={4} |
|
|
|
mt={4} |
|
|
|
width="80%" |
|
|
|
width="80%" |
|
|
|
> |
|
|
|
> |
|
|
|
<LocalOfferIcon style={{ fontSize: 80 }}/> |
|
|
|
<LocalOfferIcon style={{ fontSize: 80 }} /> |
|
|
|
</Box> |
|
|
|
</Box> |
|
|
|
<Box |
|
|
|
<Box |
|
|
|
m={1} |
|
|
|
m={1} |
|
|
|
width="80%" |
|
|
|
width="80%" |
|
|
|
> |
|
|
|
> |
|
|
|
{metadata && <Typography variant="h4">{metadata.name}</Typography>} |
|
|
|
{metadata && <Box> |
|
|
|
|
|
|
|
<Box m={2}> |
|
|
|
|
|
|
|
{fullName} |
|
|
|
|
|
|
|
</Box> |
|
|
|
|
|
|
|
<Box m={1}> |
|
|
|
|
|
|
|
<Box display="flex" alignItems="center" m={0.5}> |
|
|
|
|
|
|
|
{storeLinks} |
|
|
|
|
|
|
|
</Box> |
|
|
|
|
|
|
|
</Box> |
|
|
|
|
|
|
|
</Box>} |
|
|
|
|
|
|
|
</Box> |
|
|
|
|
|
|
|
<Box |
|
|
|
|
|
|
|
m={1} |
|
|
|
|
|
|
|
width="80%" |
|
|
|
|
|
|
|
> |
|
|
|
|
|
|
|
{maybeSubmitButton} |
|
|
|
|
|
|
|
</Box> |
|
|
|
|
|
|
|
<Box |
|
|
|
|
|
|
|
m={1} |
|
|
|
|
|
|
|
width="80%" |
|
|
|
|
|
|
|
> |
|
|
|
|
|
|
|
<Box display="flex" flexDirection="column" alignItems="left"> |
|
|
|
|
|
|
|
<Typography>Songs with this tag in your library:</Typography> |
|
|
|
|
|
|
|
</Box> |
|
|
|
|
|
|
|
{props.state.songsWithTag && <SongTable |
|
|
|
|
|
|
|
songs={props.state.songsWithTag} |
|
|
|
|
|
|
|
songGetters={props.state.songGetters} |
|
|
|
|
|
|
|
mainDispatch={props.mainDispatch} |
|
|
|
|
|
|
|
/>} |
|
|
|
|
|
|
|
{!props.state.songsWithTag && <CircularProgress />} |
|
|
|
</Box> |
|
|
|
</Box> |
|
|
|
</Box> |
|
|
|
</Box> |
|
|
|
} |
|
|
|
} |