Can replace artist. Cannot save yet and options are fixed.

expand_detailspages
Sander Vocke 5 years ago
parent 04e47349dd
commit 3f68b7664c
  1. 36
      client/src/components/common/AutocompleteSelect.tsx
  2. 8
      client/src/components/querybuilder/QBAddElemMenu.tsx
  3. 202
      client/src/components/windows/SongWindow.tsx
  4. 10
      client/src/lib/algorithm/UniqueByField.tsx
  5. 1459
      package-lock.json

@ -4,15 +4,17 @@ import Autocomplete from '@material-ui/lab/Autocomplete';
import CircularProgress from '@material-ui/core/CircularProgress';
interface IProps {
getNewOptions: (textInput: string) => Promise<string[]>,
getNewOptions: (textInput: string) => Promise<any[]>,
getOptionLabel: (option: any) => string,
label: string,
onSubmit: (s: string, exactMatch: boolean) => void,
onSubmit: (e: { input: string, option?: any }, exactMatch: boolean) => void,
onlySubmitExact: boolean,
}
// Autocompleted combo box which can make asynchronous requests
// to get new options.
// Based on Material UI example: https://material-ui.com/components/autocomplete/
export default function QBSelectWithRequest(props: IProps & any) {
export default function AutocompleteSelect(props: IProps & any) {
interface OptionsFor {
forInput: string,
options: string[],
@ -43,26 +45,16 @@ export default function QBSelectWithRequest(props: IProps & any) {
})();
};
// // Ensure a new request is made whenever the loading option is enabled.
// useEffect(() => {
// startRequest(input);
// }, []);
// Ensure options are cleared whenever the element is closed.
// useEffect(() => {
// if (!open) {
// setOptions(null);
// }
// }, [open]);
useEffect(() => {
startRequest(input);
}, [input]);
const onInputChange = (e: any, val: any, reason: any) => {
if (reason === 'reset') {
const matches = options ? options.options.filter((o:any) => props.getOptionLabel(o) === val) : [];
const match = (matches && matches.length > 0) ? matches[0] : undefined;
// User selected a preset option.
props.onSubmit(val, true);
props.onSubmit(val, match);
} else {
// User changed text, start a new request.
setInput(val);
@ -80,7 +72,7 @@ export default function QBSelectWithRequest(props: IProps & any) {
setOpen(false);
}}
getOptionSelected={(option, value) => option === value}
getOptionLabel={(option) => option}
getOptionLabel={props.getOptionLabel}
options={options ? options.options : null}
loading={loading}
freeSolo={true}
@ -92,7 +84,15 @@ export default function QBSelectWithRequest(props: IProps & any) {
e.stopPropagation();
if (e.key === 'Enter') {
// User submitted free-form value.
props.onSubmit(input, options && options.options.includes(input));
const matches = options ? options.options.filter((o:any) => props.getOptionLabel(o) === input) : [];
const match = (matches && matches.length > 0) ? matches[0] : undefined;
if(!match && props.onlySubmitExact) {
// Don't allow submitting a non-exact match.
return;
}
props.onSubmit(input, match);
}
}}
renderInput={(params) => (

@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react';
import { Menu, MenuItem } from '@material-ui/core';
import NestedMenuItem from "material-ui-nested-menu-item";
import { QueryElem, QueryLeafBy, QueryLeafOp, TagQueryInfo } from '../../lib/query/Query';
import QBSelectWithRequest from './QBSelectWithRequest';
import AutocompleteSelect from '../common/AutocompleteSelect';
import { Requests } from './QueryBuilder';
export interface MenuProps {
@ -115,7 +115,7 @@ export function QBAddElemMenu(props: MenuProps) {
label="Song"
parentMenuOpen={Boolean(anchorEl)}
>
<QBSelectWithRequest
<AutocompleteSelect
label="Title"
getNewOptions={props.requestFunctions.getSongTitles}
onSubmit={(s: string, exact: boolean) => {
@ -133,7 +133,7 @@ export function QBAddElemMenu(props: MenuProps) {
label="Artist"
parentMenuOpen={Boolean(anchorEl)}
>
<QBSelectWithRequest
<AutocompleteSelect
label="Name"
getNewOptions={props.requestFunctions.getArtists}
onSubmit={(s: string, exact: boolean) => {
@ -151,7 +151,7 @@ export function QBAddElemMenu(props: MenuProps) {
label="Album"
parentMenuOpen={Boolean(anchorEl)}
>
<QBSelectWithRequest
<AutocompleteSelect
label="Name"
getNewOptions={props.requestFunctions.getAlbums}
onSubmit={(s: string, exact: boolean) => {

@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react';
import { Box, Typography, IconButton, Button, CircularProgress } from '@material-ui/core';
import { Box, Typography, IconButton, Button, CircularProgress, Menu, MenuItem } from '@material-ui/core';
import AudiotrackIcon from '@material-ui/icons/Audiotrack';
import PersonIcon from '@material-ui/icons/Person';
import AlbumIcon from '@material-ui/icons/Album';
@ -11,9 +11,18 @@ import StoreLinkIcon, { whichStore } from '../common/StoreLinkIcon';
import EditableText from '../common/EditableText';
import SubmitChangesButton from '../common/SubmitChangesButton';
import { saveSongChanges } from '../../lib/saveChanges';
import { uniqueByField } from '../../lib/algorithm/UniqueByField';
import NestedMenuItem from 'material-ui-nested-menu-item';
import AutocompleteSelect from '../common/AutocompleteSelect';
export type SongMetadata = serverApi.SongDetails;
export type SongMetadataChanges = serverApi.ModifySongRequest;
export type SongMetadataChanges = {
title?: string,
artists?: any[],
albums?: any[],
tags?: any[],
storeLinks?: string[],
}
export interface SongWindowState extends WindowState {
songId: number,
@ -81,6 +90,55 @@ export async function getSongMetadata(id: number) {
})();
}
export function EditArtistMenu(props: {
artist: any,
pos: number[] | undefined,
onClose: () => void,
onRemove: () => void,
onReplace: (newArtist: any) => void,
}) {
const name = props.artist ? props.artist.name : "unknown";
const open = Boolean(props.pos);
return <Menu
anchorReference='anchorPosition'
anchorPosition={props.pos && { left: props.pos[0], top: props.pos[1] }}
keepMounted
open={open}
onClose={props.onClose}
>
<MenuItem onClick={() => { props.onRemove(); props.onClose(); }}>Remove "{name}"</MenuItem>
<NestedMenuItem
parentMenuOpen={open}
label="Replace..."
>
<AutocompleteSelect
label="Artist"
getNewOptions={async () => {
return [
{
name: "Dude",
artistId: 900,
},
{
name: "Sweet",
artistId: 901,
}
];
}}
onSubmit={(input: string, artist: any) => {
console.log("Submitted", artist)
props.onReplace(artist);
props.onClose();
}}
style={{ width: 300 }}
getOptionLabel={(artist: any) => artist.name}
onlySubmitExact={true}
/>
</NestedMenuItem>
</Menu>
}
export default function SongWindow(props: IProps) {
let metadata = props.state.metadata;
let pendingChanges = props.state.pendingChanges;
@ -95,6 +153,11 @@ export default function SongWindow(props: IProps) {
})
}, [metadata?.title]);
const [artistMenuState, setArtistMenuState] = useState<{
menuPos: number[],
artist: any,
} | undefined>(undefined);
const [editingTitle, setEditingTitle] = useState<string | null>(null);
const title = <Typography variant="h4"><EditableText
defaultValue={metadata?.title || "(Unknown title)"}
@ -113,10 +176,21 @@ export default function SongWindow(props: IProps) {
}}
/></Typography>
const artists = metadata?.artists && metadata?.artists.map((artist: ArtistMetadata) => {
return <Typography>
{artist.name}
</Typography>
const originalArtists = (metadata?.artists) || [];
const newArtists = (pendingChanges?.artists) || [];
const allArtists = uniqueByField([...originalArtists, ...newArtists], 'artistId');
const artists = allArtists.map((artist: any) => {
const id = artist.artistId;
const deleted = pendingChanges && pendingChanges.artists && pendingChanges.artists.filter((aa: any) => aa.artistId === id).length === 0;
return <Button
style={{ textTransform: 'none' }}
onClick={(e: any) => setArtistMenuState({ menuPos: [e.pageX, e.pageY], artist: artist })}
>
<Typography>
{deleted ? <del>{artist.name}</del> : artist.name}
</Typography>
</Button>
});
const albums = metadata?.albums && metadata?.albums.map((album: AlbumMetadata) => {
@ -154,50 +228,86 @@ export default function SongWindow(props: IProps) {
{applying && <CircularProgress />}
</Box>
return <Box width="100%" justifyContent="center" display="flex" flexWrap="wrap">
<Box
m={1}
mt={4}
width="80%"
>
<AudiotrackIcon style={{ fontSize: 80 }} />
</Box>
<Box
m={1}
width="80%"
>
{metadata && <Box>
<Box m={2}>
{title}
</Box>
<Box m={0.5}>
<Box display="flex" alignItems="center" m={0.5}>
<PersonIcon />
<Box m={0.5}>
{artists}
return <>
<Box width="100%" justifyContent="center" display="flex" flexWrap="wrap">
<Box
m={1}
mt={4}
width="80%"
>
<AudiotrackIcon style={{ fontSize: 80 }} />
</Box>
<Box
m={1}
width="80%"
>
{metadata && <Box>
<Box m={2}>
{title}
</Box>
<Box m={0.5}>
<Box display="flex" alignItems="center" m={0.5}>
<PersonIcon />
<Box m={0.5}>
{artists}
</Box>
</Box>
</Box>
</Box>
<Box m={0.5}>
<Box display="flex" alignItems="center" m={0.5}>
<AlbumIcon />
<Box m={0.5}>
{albums}
<Box m={0.5}>
<Box display="flex" alignItems="center" m={0.5}>
<AlbumIcon />
<Box m={0.5}>
{albums}
</Box>
</Box>
</Box>
</Box>
<Box m={1}>
<Box display="flex" alignItems="center" m={0.5}>
{storeLinks}
<Box m={1}>
<Box display="flex" alignItems="center" m={0.5}>
{storeLinks}
</Box>
</Box>
</Box>
</Box>}
</Box>
<Box
m={1}
width="80%"
>
{maybeSubmitButton}
</Box>}
</Box>
<Box
m={1}
width="80%"
>
{maybeSubmitButton}
</Box>
</Box>
</Box>
<EditArtistMenu
pos={artistMenuState?.menuPos}
artist={artistMenuState?.artist}
onClose={() => setArtistMenuState(undefined)}
onRemove={() => {
const oldArtists = props.state.pendingChanges?.artists ?
props.state.pendingChanges.artists :
(props.state.metadata?.artists || []);
const newArtists = [...oldArtists]
.filter((a: any) => (a.artistId != artistMenuState?.artist.artistId));
props.dispatch({
type: SongWindowStateActions.SetPendingChanges,
value: {
...props.state.pendingChanges,
artists: newArtists,
}
})
}}
onReplace={(newArtist: any) => {
const oldArtists = props.state.pendingChanges?.artists ?
props.state.pendingChanges.artists :
(props.state.metadata?.artists || []);
const withoutReplaced = [...oldArtists]
.filter((a: any) => (a.artistId != artistMenuState?.artist.artistId));
const withNew = [ ...withoutReplaced, newArtist ];
props.dispatch({
type: SongWindowStateActions.SetPendingChanges,
value: {
...props.state.pendingChanges,
artists: withNew,
}
})
}}
/>
</>
}

@ -0,0 +1,10 @@
// Takes
export function uniqueByField(
list: any[],
field: string,
) {
const uniqueFields = new Set(list.map((l: any) => l[field]));
return [...uniqueFields].map((f: string) => {
return list.filter((e: any) => e[field] === f)[0];
})
}

1459
package-lock.json generated

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save