Back to simple declarative routing. (#28)

Fix routing.
Boy-scout: alphabetic ordering in the tags window

Reviewed-on: #28
pull/34/head
Sander Vocke 5 years ago
parent a689613a45
commit 87af6e18a4
  1. 18
      client/src/App.tsx
  2. 181
      client/src/components/MainWindow.tsx
  3. 40
      client/src/components/appbar/AddTabMenu.tsx
  4. 93
      client/src/components/appbar/AppBar.tsx
  5. 65
      client/src/components/tables/ResultsTable.tsx
  6. 33
      client/src/components/windows/Windows.tsx
  7. 39
      client/src/components/windows/album/AlbumWindow.tsx
  8. 34
      client/src/components/windows/artist/ArtistWindow.tsx
  9. 4
      client/src/components/windows/manage_tags/ManageTagMenu.tsx
  10. 83
      client/src/components/windows/manage_tags/ManageTagsWindow.tsx
  11. 25
      client/src/components/windows/query/QueryWindow.tsx
  12. 27
      client/src/components/windows/song/SongWindow.tsx
  13. 37
      client/src/components/windows/tag/TagWindow.tsx

@ -3,25 +3,13 @@ import React from 'react';
import { DndProvider } from 'react-dnd'; import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend'; import { HTML5Backend } from 'react-dnd-html5-backend';
import {
HashRouter as Router,
Switch,
Route
} from "react-router-dom";
import MainWindow from './components/MainWindow'; import MainWindow from './components/MainWindow';
function App() { function App() {
return ( return (
<Router> <DndProvider backend={HTML5Backend}>
<DndProvider backend={HTML5Backend}> <MainWindow />
<Switch> </DndProvider>
<Route path="/">
<MainWindow/>
</Route>
</Switch>
</DndProvider>
</Router>
); );
} }

@ -1,15 +1,15 @@
import React, { useReducer, Reducer } from 'react'; import React, { useReducer, Reducer, useContext } from 'react';
import { ThemeProvider, CssBaseline, createMuiTheme } from '@material-ui/core'; import { ThemeProvider, CssBaseline, createMuiTheme } from '@material-ui/core';
import { grey } from '@material-ui/core/colors'; import { grey } from '@material-ui/core/colors';
import AppBar from './appbar/AppBar'; import AppBar, { AppBarTab } from './appbar/AppBar';
import QueryWindow from './windows/query/QueryWindow'; import QueryWindow, { QueryWindowReducer } from './windows/query/QueryWindow';
import { NewTabProps } from './appbar/AddTabMenu';
import { newWindowState, newWindowReducer, WindowType } from './windows/Windows'; import { newWindowState, newWindowReducer, WindowType } from './windows/Windows';
import ArtistWindow from './windows/artist/ArtistWindow'; import ArtistWindow from './windows/artist/ArtistWindow';
import AlbumWindow from './windows/album/AlbumWindow'; import AlbumWindow from './windows/album/AlbumWindow';
import TagWindow from './windows/tag/TagWindow'; import TagWindow from './windows/tag/TagWindow';
import SongWindow from './windows/song/SongWindow'; import SongWindow from './windows/song/SongWindow';
import ManageTagsWindow from './windows/manage_tags/ManageTagsWindow'; import ManageTagsWindow from './windows/manage_tags/ManageTagsWindow';
import { BrowserRouter, Switch, Route, useParams, Redirect } from 'react-router-dom';
var _ = require('lodash'); var _ = require('lodash');
const darkTheme = createMuiTheme({ const darkTheme = createMuiTheme({
@ -21,150 +21,39 @@ const darkTheme = createMuiTheme({
}, },
}); });
export interface MainWindowState {
tabStates: any[],
tabReducers: Reducer<any, any>[],
tabTypes: WindowType[],
activeTab: number,
}
export enum MainWindowStateActions {
SetActiveTab = "setActiveTab",
DispatchToTab = "dispatchToTab",
CloseTab = "closeTab",
AddTab = "addTab",
}
export function MainWindowReducer(state: MainWindowState, action: any) {
switch (action.type) {
case MainWindowStateActions.SetActiveTab:
return { ...state, activeTab: action.value }
case MainWindowStateActions.CloseTab:
const newSize = state.tabStates.length - 1;
return {
...state,
tabStates: state.tabStates.filter((i: any, idx: number) => idx != action.idx),
tabReducers: state.tabReducers.filter((i: any, idx: number) => idx != action.idx),
tabTypes: state.tabTypes.filter((i: any, idx: number) => idx != action.idx),
activeTab: state.activeTab >= (newSize - 1) ? (newSize - 1) : state.activeTab,
}
case MainWindowStateActions.AddTab:
return {
...state,
tabStates: [...state.tabStates, action.tabState],
tabReducers: [...state.tabReducers, action.tabReducer],
tabTypes: [...state.tabTypes, action.tabType],
}
case MainWindowStateActions.DispatchToTab:
return {
...state,
tabStates: state.tabStates.map((item: any, i: number) => {
return i === action.idx ?
state.tabReducers[i](item, action.tabAction) :
item;
})
}
default:
throw new Error("Unimplemented MainWindow state update.")
}
}
export default function MainWindow(props: any) { export default function MainWindow(props: any) {
const [state, dispatch] = useReducer(MainWindowReducer, {
tabStates: [
newWindowState[WindowType.Query](),
newWindowState[WindowType.Song](),
newWindowState[WindowType.Album](),
newWindowState[WindowType.Artist](),
newWindowState[WindowType.Tag](),
newWindowState[WindowType.ManageTags](),
],
tabReducers: [
newWindowReducer[WindowType.Query],
newWindowReducer[WindowType.Song],
newWindowReducer[WindowType.Album],
newWindowReducer[WindowType.Artist],
newWindowReducer[WindowType.Tag],
newWindowReducer[WindowType.ManageTags],
],
tabTypes: [
WindowType.Query,
WindowType.Song,
WindowType.Album,
WindowType.Artist,
WindowType.Tag,
WindowType.ManageTags,
],
activeTab: 0
})
const windows = state.tabStates.map((tabState: any, i: number) => {
const tabDispatch = (action: any) => {
dispatch({
type: MainWindowStateActions.DispatchToTab,
tabAction: action,
idx: i
});
}
switch (state.tabTypes[i]) {
case WindowType.Query:
return <QueryWindow
state={tabState}
dispatch={tabDispatch}
mainDispatch={dispatch}
/>
case WindowType.Artist:
return <ArtistWindow
state={tabState}
dispatch={tabDispatch}
mainDispatch={dispatch}
/>
case WindowType.Album:
return <AlbumWindow
state={tabState}
dispatch={tabDispatch}
mainDispatch={dispatch}
/>
case WindowType.Tag:
return <TagWindow
state={tabState}
dispatch={tabDispatch}
mainDispatch={dispatch}
/>
case WindowType.Song:
return <SongWindow
state={tabState}
dispatch={tabDispatch}
mainDispatch={dispatch}
/>
case WindowType.ManageTags:
return <ManageTagsWindow
state={tabState}
dispatch={tabDispatch}
mainDispatch={dispatch}
/>
default:
throw new Error("Unimplemented window type");
}
});
return <ThemeProvider theme={darkTheme}> return <ThemeProvider theme={darkTheme}>
<CssBaseline /> <CssBaseline />
<AppBar <BrowserRouter>
tabLabels={state.tabStates.map((s: any) => s.tabLabel)} <Switch>
selectedTab={state.activeTab} <Route exact path="/">
setSelectedTab={(t: number) => dispatch({ type: MainWindowStateActions.SetActiveTab, value: t })} <Redirect to={"/query"} />
onCloseTab={(t: number) => dispatch({ type: MainWindowStateActions.CloseTab, idx: t })} </Route>
onAddTab={(w: NewTabProps) => { <Route path="/query">
dispatch({ <AppBar selectedTab={AppBarTab.Query} />
type: MainWindowStateActions.AddTab, <QueryWindow/>
tabState: newWindowState[w.windowType](), </Route>
tabReducer: newWindowReducer[w.windowType], <Route path="/artist/:id">
tabType: w.windowType, <AppBar selectedTab={null} />
}) <ArtistWindow/>
}} </Route>
/> <Route path="/tag/:id">
{windows[state.activeTab]} <AppBar selectedTab={null} />
<TagWindow/>
</Route>
<Route path="/album/:id">
<AppBar selectedTab={null} />
<AlbumWindow/>
</Route>
<Route path="/song/:id">
<AppBar selectedTab={null} />
<SongWindow/>
</Route>
<Route path="/tags">
<AppBar selectedTab={AppBarTab.Tags} />
<ManageTagsWindow/>
</Route>
</Switch>
</BrowserRouter>
</ThemeProvider> </ThemeProvider>
} }

@ -1,40 +0,0 @@
import React from 'react';
import { WindowType } from '../windows/Windows';
import { Menu, MenuItem } from '@material-ui/core';
export interface NewTabProps {
windowType: WindowType,
}
export interface IProps {
anchorEl: null | HTMLElement,
onClose: () => void,
onCreateTab: (q: NewTabProps) => void,
}
export default function AddTabMenu(props: IProps) {
return <Menu
anchorEl={props.anchorEl}
keepMounted
open={Boolean(props.anchorEl)}
onClose={props.onClose}
>
<MenuItem disabled={true}>New Tab</MenuItem>
<MenuItem
onClick={() => {
props.onClose();
props.onCreateTab({
windowType: WindowType.Query,
})
}}
>{WindowType.Query}</MenuItem>
<MenuItem
onClick={() => {
props.onClose();
props.onCreateTab({
windowType: WindowType.ManageTags,
})
}}
>Manage Tags</MenuItem>
</Menu>
}

@ -1,85 +1,52 @@
import React from 'react'; import React from 'react';
import { AppBar as MuiAppBar, Box, Tab as MuiTab, Tabs, IconButton } from '@material-ui/core'; import { AppBar as MuiAppBar, Box, Tab as MuiTab, Tabs, IconButton, Typography } from '@material-ui/core';
import CloseIcon from '@material-ui/icons/Close'; import CloseIcon from '@material-ui/icons/Close';
import AddIcon from '@material-ui/icons/Add'; import SearchIcon from '@material-ui/icons/Search';
import AddTabMenu, { NewTabProps } from './AddTabMenu'; import LocalOfferIcon from '@material-ui/icons/LocalOffer';
import { Link, useHistory } from 'react-router-dom';
export interface IProps { import { WindowType } from '../windows/Windows';
tabLabels: string[],
selectedTab: number, export enum AppBarTab {
setSelectedTab: (n: number) => void, Query = 0,
onCloseTab: (idx: number) => void, Tags,
onAddTab: (w: NewTabProps) => void,
}
export interface TabProps {
onClose: () => void,
} }
export function Tab(props: any) { export const appBarTabProps: Record<any, any> = {
const { onClose, label, ...restProps } = props; [AppBarTab.Query]: {
label: <Box display="flex"><SearchIcon/><Typography variant="button">Query</Typography></Box>,
const labelElem = <Box path: "/query",
display="flex" },
alignItems="center" [AppBarTab.Tags]: {
justifyContent="center" label: <Box display="flex"><LocalOfferIcon/><Typography variant="button">Tags</Typography></Box>,
> path: "/tags",
{label} },
<Box ml={1}>
<IconButton
size="small"
color="inherit"
onClick={onClose}
>
<CloseIcon />
</IconButton>
</Box>
</Box>;
return <MuiTab
label={labelElem}
{...restProps}
/>
} }
export default function AppBar(props: IProps) { export default function AppBar(props: {
const [addMenuAnchorEl, setAddMenuAnchorEl] = React.useState<null | HTMLElement>(null); selectedTab: AppBarTab | null
}) {
const onOpenAddMenu = (event: any) => { const history = useHistory();
setAddMenuAnchorEl(event.currentTarget);
};
const onCloseAddMenu = () => {
setAddMenuAnchorEl(null);
};
const onAddTab = (w: NewTabProps) => {
props.onAddTab(w);
};
return <> return <>
<MuiAppBar position="static" style={{ background: 'grey' }}> <MuiAppBar position="static" style={{ background: 'grey' }}>
<Box display="flex" alignItems="center"> <Box display="flex" alignItems="center">
<Box m={0.5} display="flex" alignItems="center"> <Link to="/">
<img height="30px" src={process.env.PUBLIC_URL + "/logo.svg"} alt="error"></img> <Box m={0.5} display="flex" alignItems="center">
</Box> <img height="30px" src={process.env.PUBLIC_URL + "/logo.svg"} alt="error"></img>
</Box>
</Link>
<Tabs <Tabs
value={props.selectedTab} value={props.selectedTab}
onChange={(e: any, v: number) => props.setSelectedTab(v)} onChange={(e: any, val: AppBarTab) => history.push(appBarTabProps[val].path)}
variant="scrollable" variant="scrollable"
scrollButtons="auto" scrollButtons="auto"
> >
{props.tabLabels.map((l: string, idx: number) => <Tab {Object.keys(appBarTabProps).map((tab: any, idx: number) => <MuiTab
label={l} label={appBarTabProps[tab].label}
value={idx} value={idx}
onClose={() => props.onCloseTab(idx)}
/>)} />)}
</Tabs> </Tabs>
<IconButton color="inherit" onClick={onOpenAddMenu}><AddIcon /></IconButton>
</Box> </Box>
</MuiAppBar> </MuiAppBar>
<AddTabMenu
anchorEl={addMenuAnchorEl}
onClose={onCloseAddMenu}
onCreateTab={onAddTab}
/>
</> </>
} }

@ -1,13 +1,7 @@
import React from 'react'; import React from 'react';
import { TableContainer, Table, TableHead, TableRow, TableCell, Paper, makeStyles, TableBody, Chip, Box, Button } from '@material-ui/core'; import { TableContainer, Table, TableHead, TableRow, TableCell, Paper, makeStyles, TableBody, Chip, Box, Button } from '@material-ui/core';
import stringifyList from '../../lib/stringifyList'; import stringifyList from '../../lib/stringifyList';
import { MainWindowStateActions } from '../MainWindow'; import { useHistory } from 'react-router';
import { newWindowReducer, WindowType } from '../windows/Windows';
import PersonIcon from '@material-ui/icons/Person';
import AlbumIcon from '@material-ui/icons/Album';
import AudiotrackIcon from '@material-ui/icons/Audiotrack';
import LocalOfferIcon from '@material-ui/icons/LocalOffer';
import { songGetters } from '../../lib/songGetters';
export interface SongGetters { export interface SongGetters {
getTitle: (song: any) => string, getTitle: (song: any) => string,
@ -20,13 +14,12 @@ export interface SongGetters {
getTagIds: (song: any) => number[][], // Each tag is represented as a series of ids. getTagIds: (song: any) => number[][], // Each tag is represented as a series of ids.
} }
export interface IProps { export default function SongTable(props: {
songs: any[], songs: any[],
songGetters: SongGetters, songGetters: SongGetters,
mainDispatch: (action: any) => void, }) {
} const history = useHistory();
export default function SongTable(props: IProps) {
const classes = makeStyles({ const classes = makeStyles({
button: { button: {
textTransform: "none", textTransform: "none",
@ -66,61 +59,19 @@ export default function SongTable(props: IProps) {
const tagIds = props.songGetters.getTagIds(song); const tagIds = props.songGetters.getTagIds(song);
const onClickArtist = () => { const onClickArtist = () => {
props.mainDispatch({ history.push('/artist/' + mainArtistId);
type: MainWindowStateActions.AddTab,
tabState: {
tabLabel: <><PersonIcon />{mainArtistName}</>,
artistId: mainArtistId,
metadata: null,
songGetters: songGetters,
songsByArtist: null,
},
tabReducer: newWindowReducer[WindowType.Artist],
tabType: WindowType.Artist,
})
} }
const onClickAlbum = () => { const onClickAlbum = () => {
props.mainDispatch({ history.push('/album/' + mainAlbumId);
type: MainWindowStateActions.AddTab,
tabState: {
tabLabel: <><AlbumIcon />{mainAlbumName}</>,
albumId: mainAlbumId,
metadata: null,
songGetters: songGetters,
songsOnAlbum: null,
},
tabReducer: newWindowReducer[WindowType.Album],
tabType: WindowType.Album,
})
} }
const onClickSong = () => { const onClickSong = () => {
props.mainDispatch({ history.push('/song/' + songId);
type: MainWindowStateActions.AddTab,
tabState: {
tabLabel: <><AudiotrackIcon />{title}</>,
songId: songId,
metadata: null,
},
tabReducer: newWindowReducer[WindowType.Song],
tabType: WindowType.Song,
})
} }
const onClickTag = (id: number, name: string) => { const onClickTag = (id: number, name: string) => {
props.mainDispatch({ history.push('/tag/' + id);
type: MainWindowStateActions.AddTab,
tabState: {
tabLabel: <><LocalOfferIcon />{name}</>,
tagId: id,
metadata: null,
songGetters: songGetters,
songsWithTag: null,
},
tabReducer: newWindowReducer[WindowType.Tag],
tabType: WindowType.Tag,
})
} }
const tags = props.songGetters.getTagNames(song).map((tag: string[], i: number) => { const tags = props.songGetters.getTagNames(song).map((tag: string[], i: number) => {

@ -1,17 +1,17 @@
import React from 'react'; import React, { useReducer } from 'react';
import { QueryWindowReducer } from "./query/QueryWindow"; import QueryWindow, { QueryWindowReducer } from "./query/QueryWindow";
import { ArtistWindowReducer } from "./artist/ArtistWindow"; import ArtistWindow, { ArtistWindowReducer } from "./artist/ArtistWindow";
import SearchIcon from '@material-ui/icons/Search'; import SearchIcon from '@material-ui/icons/Search';
import PersonIcon from '@material-ui/icons/Person'; import PersonIcon from '@material-ui/icons/Person';
import AlbumIcon from '@material-ui/icons/Album'; import AlbumIcon from '@material-ui/icons/Album';
import LocalOfferIcon from '@material-ui/icons/LocalOffer'; import LocalOfferIcon from '@material-ui/icons/LocalOffer';
import AudiotrackIcon from '@material-ui/icons/Audiotrack'; import AudiotrackIcon from '@material-ui/icons/Audiotrack';
import LoyaltyIcon from '@material-ui/icons/Loyalty'; import LoyaltyIcon from '@material-ui/icons/Loyalty';
import { SongWindowReducer } from './song/SongWindow'; import SongWindow, { SongWindowReducer } from './song/SongWindow';
import { AlbumWindowReducer } from './album/AlbumWindow'; import AlbumWindow, { AlbumWindowReducer } from './album/AlbumWindow';
import { TagWindowReducer } from './tag/TagWindow'; import TagWindow, { TagWindowReducer } from './tag/TagWindow';
import { songGetters } from '../../lib/songGetters'; import { songGetters } from '../../lib/songGetters';
import { ManageTagsWindowReducer } from './manage_tags/ManageTagsWindow'; import ManageTagsWindow, { ManageTagsWindowReducer } from './manage_tags/ManageTagsWindow';
export enum WindowType { export enum WindowType {
Query = "Query", Query = "Query",
@ -22,9 +22,7 @@ export enum WindowType {
ManageTags = "ManageTags", ManageTags = "ManageTags",
} }
export interface WindowState { export interface WindowState { }
tabLabel: string,
}
export const newWindowReducer = { export const newWindowReducer = {
[WindowType.Query]: QueryWindowReducer, [WindowType.Query]: QueryWindowReducer,
@ -38,7 +36,6 @@ export const newWindowReducer = {
export const newWindowState = { export const newWindowState = {
[WindowType.Query]: () => { [WindowType.Query]: () => {
return { return {
tabLabel: <><SearchIcon/>Query</>,
editingQuery: false, editingQuery: false,
query: null, query: null,
resultsForQuery: null, resultsForQuery: null,
@ -46,8 +43,7 @@ export const newWindowState = {
}, },
[WindowType.Artist]: () => { [WindowType.Artist]: () => {
return { return {
tabLabel: <><PersonIcon/>Artist 1</>, id: 1,
artistId: 1,
metadata: null, metadata: null,
pendingChanges: null, pendingChanges: null,
songGetters: songGetters, songGetters: songGetters,
@ -56,8 +52,7 @@ export const newWindowState = {
}, },
[WindowType.Album]: () => { [WindowType.Album]: () => {
return { return {
tabLabel: <><AlbumIcon/>Album 1</>, id: 1,
albumId: 1,
metadata: null, metadata: null,
pendingChanges: null, pendingChanges: null,
songGetters: songGetters, songGetters: songGetters,
@ -66,16 +61,14 @@ export const newWindowState = {
}, },
[WindowType.Song]: () => { [WindowType.Song]: () => {
return { return {
tabLabel: <><AudiotrackIcon/>Song 1</>, id: 1,
songId: 1,
metadata: null, metadata: null,
pendingChanges: null, pendingChanges: null,
} }
}, },
[WindowType.Tag]: () => { [WindowType.Tag]: () => {
return { return {
tabLabel: <><LocalOfferIcon/>Tag 1</>, id: 1,
tagId: 1,
metadata: null, metadata: null,
pendingChanges: null, pendingChanges: null,
songGetters: songGetters, songGetters: songGetters,
@ -84,8 +77,8 @@ export const newWindowState = {
}, },
[WindowType.ManageTags]: () => { [WindowType.ManageTags]: () => {
return { return {
tabLabel: <><LoyaltyIcon/>Manage Tags</>,
fetchedTags: null, fetchedTags: null,
alert: null,
pendingChanges: [], pendingChanges: [],
} }
} }

@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState, useReducer } from 'react';
import { Box, Typography, IconButton, CircularProgress } from '@material-ui/core'; import { Box, Typography, IconButton, CircularProgress } from '@material-ui/core';
import AlbumIcon from '@material-ui/icons/Album'; import AlbumIcon from '@material-ui/icons/Album';
import * as serverApi from '../../../api'; import * as serverApi from '../../../api';
@ -10,13 +10,15 @@ import SongTable, { SongGetters } from '../../tables/ResultsTable';
import { saveAlbumChanges } from '../../../lib/saveChanges'; import { saveAlbumChanges } from '../../../lib/saveChanges';
import { QueryLeafBy, QueryLeafOp } from '../../../lib/query/Query'; import { QueryLeafBy, QueryLeafOp } from '../../../lib/query/Query';
import { queryAlbums, querySongs } from '../../../lib/backend/queries'; import { queryAlbums, querySongs } from '../../../lib/backend/queries';
import { songGetters } from '../../../lib/songGetters';
import { useParams } from 'react-router';
var _ = require('lodash'); var _ = require('lodash');
export type AlbumMetadata = serverApi.AlbumDetails; export type AlbumMetadata = serverApi.AlbumDetails;
export type AlbumMetadataChanges = serverApi.ModifyAlbumRequest; export type AlbumMetadataChanges = serverApi.ModifyAlbumRequest;
export interface AlbumWindowState extends WindowState { export interface AlbumWindowState extends WindowState {
albumId: number, id: number,
metadata: AlbumMetadata | null, metadata: AlbumMetadata | null,
pendingChanges: AlbumMetadataChanges | null, pendingChanges: AlbumMetadataChanges | null,
songsOnAlbum: any[] | null, songsOnAlbum: any[] | null,
@ -45,12 +47,6 @@ export function AlbumWindowReducer(state: AlbumWindowState, action: any) {
} }
} }
export interface IProps {
state: AlbumWindowState,
dispatch: (action: any) => void,
mainDispatch: (action: any) => void,
}
export async function getAlbumMetadata(id: number) { export async function getAlbumMetadata(id: number) {
return (await queryAlbums({ return (await queryAlbums({
query: { query: {
@ -63,13 +59,29 @@ export async function getAlbumMetadata(id: number) {
}))[0]; }))[0];
} }
export default function AlbumWindow(props: IProps) { export default function AlbumWindow(props: {}) {
const { id } = useParams();
const [state, dispatch] = useReducer(AlbumWindowReducer, {
id: id,
metadata: null,
pendingChanges: null,
songGetters: songGetters,
songsOnAlbum: null,
});
return <AlbumWindowControlled state={state} dispatch={dispatch} />
}
export function AlbumWindowControlled(props: {
state: AlbumWindowState,
dispatch: (action: any) => void,
}) {
let metadata = props.state.metadata; let metadata = props.state.metadata;
let pendingChanges = props.state.pendingChanges; let pendingChanges = props.state.pendingChanges;
// Effect to get the album's metadata. // Effect to get the album's metadata.
useEffect(() => { useEffect(() => {
getAlbumMetadata(props.state.albumId) getAlbumMetadata(props.state.id)
.then((m: AlbumMetadata) => { .then((m: AlbumMetadata) => {
props.dispatch({ props.dispatch({
type: AlbumWindowStateActions.SetMetadata, type: AlbumWindowStateActions.SetMetadata,
@ -86,7 +98,7 @@ export default function AlbumWindow(props: IProps) {
const songs = await querySongs({ const songs = await querySongs({
query: { query: {
a: QueryLeafBy.AlbumId, a: QueryLeafBy.AlbumId,
b: props.state.albumId, b: props.state.id,
leafOp: QueryLeafOp.Equals, leafOp: QueryLeafOp.Equals,
}, },
offset: 0, offset: 0,
@ -95,7 +107,7 @@ export default function AlbumWindow(props: IProps) {
props.dispatch({ props.dispatch({
type: AlbumWindowStateActions.SetSongs, type: AlbumWindowStateActions.SetSongs,
value: songs, value: songs,
}); });
})(); })();
}, [props.state.songsOnAlbum]); }, [props.state.songsOnAlbum]);
@ -135,7 +147,7 @@ export default function AlbumWindow(props: IProps) {
<Box> <Box>
<SubmitChangesButton onClick={() => { <SubmitChangesButton onClick={() => {
setApplying(true); setApplying(true);
saveAlbumChanges(props.state.albumId, pendingChanges || {}) saveAlbumChanges(props.state.id, pendingChanges || {})
.then(() => { .then(() => {
setApplying(false); setApplying(false);
props.dispatch({ props.dispatch({
@ -185,7 +197,6 @@ export default function AlbumWindow(props: IProps) {
{props.state.songsOnAlbum && <SongTable {props.state.songsOnAlbum && <SongTable
songs={props.state.songsOnAlbum} songs={props.state.songsOnAlbum}
songGetters={props.state.songGetters} songGetters={props.state.songGetters}
mainDispatch={props.mainDispatch}
/>} />}
{!props.state.songsOnAlbum && <CircularProgress />} {!props.state.songsOnAlbum && <CircularProgress />}
</Box> </Box>

@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState, useReducer } from 'react';
import { Box, Typography, IconButton, Button, CircularProgress } from '@material-ui/core'; import { Box, Typography, IconButton, Button, CircularProgress } from '@material-ui/core';
import PersonIcon from '@material-ui/icons/Person'; import PersonIcon from '@material-ui/icons/Person';
import * as serverApi from '../../../api'; import * as serverApi from '../../../api';
@ -10,13 +10,15 @@ import SongTable, { SongGetters } from '../../tables/ResultsTable';
import { saveArtistChanges } from '../../../lib/saveChanges'; import { saveArtistChanges } from '../../../lib/saveChanges';
import { QueryLeafBy, QueryLeafOp } from '../../../lib/query/Query'; import { QueryLeafBy, QueryLeafOp } from '../../../lib/query/Query';
import { queryArtists, querySongs } from '../../../lib/backend/queries'; import { queryArtists, querySongs } from '../../../lib/backend/queries';
import { songGetters } from '../../../lib/songGetters';
import { useParams } from 'react-router';
var _ = require('lodash'); var _ = require('lodash');
export type ArtistMetadata = serverApi.ArtistDetails; export type ArtistMetadata = serverApi.ArtistDetails;
export type ArtistMetadataChanges = serverApi.ModifyArtistRequest; export type ArtistMetadataChanges = serverApi.ModifyArtistRequest;
export interface ArtistWindowState extends WindowState { export interface ArtistWindowState extends WindowState {
artistId: number, id: number,
metadata: ArtistMetadata | null, metadata: ArtistMetadata | null,
pendingChanges: ArtistMetadataChanges | null, pendingChanges: ArtistMetadataChanges | null,
songsByArtist: any[] | null, songsByArtist: any[] | null,
@ -48,7 +50,6 @@ export function ArtistWindowReducer(state: ArtistWindowState, action: any) {
export interface IProps { export interface IProps {
state: ArtistWindowState, state: ArtistWindowState,
dispatch: (action: any) => void, dispatch: (action: any) => void,
mainDispatch: (action: any) => void,
} }
export async function getArtistMetadata(id: number) { export async function getArtistMetadata(id: number) {
@ -63,13 +64,29 @@ export async function getArtistMetadata(id: number) {
}))[0]; }))[0];
} }
export default function ArtistWindow(props: IProps) { export default function ArtistWindow(props: {}) {
const { id } = useParams();
const [state, dispatch] = useReducer(ArtistWindowReducer, {
id: id,
metadata: null,
pendingChanges: null,
songGetters: songGetters,
songsByArtist: null,
});
return <ArtistWindowControlled state={state} dispatch={dispatch} />
}
export function ArtistWindowControlled(props: {
state: ArtistWindowState,
dispatch: (action: any) => void,
}) {
let metadata = props.state.metadata; let metadata = props.state.metadata;
let pendingChanges = props.state.pendingChanges; let pendingChanges = props.state.pendingChanges;
// Effect to get the artist's metadata. // Effect to get the artist's metadata.
useEffect(() => { useEffect(() => {
getArtistMetadata(props.state.artistId) getArtistMetadata(props.state.id)
.then((m: ArtistMetadata) => { .then((m: ArtistMetadata) => {
props.dispatch({ props.dispatch({
type: ArtistWindowStateActions.SetMetadata, type: ArtistWindowStateActions.SetMetadata,
@ -86,7 +103,7 @@ export default function ArtistWindow(props: IProps) {
const songs = await querySongs({ const songs = await querySongs({
query: { query: {
a: QueryLeafBy.ArtistId, a: QueryLeafBy.ArtistId,
b: props.state.artistId, b: props.state.id,
leafOp: QueryLeafOp.Equals, leafOp: QueryLeafOp.Equals,
}, },
offset: 0, offset: 0,
@ -95,7 +112,7 @@ export default function ArtistWindow(props: IProps) {
props.dispatch({ props.dispatch({
type: ArtistWindowStateActions.SetSongs, type: ArtistWindowStateActions.SetSongs,
value: songs, value: songs,
}); });
})(); })();
}, [props.state.songsByArtist]); }, [props.state.songsByArtist]);
@ -135,7 +152,7 @@ export default function ArtistWindow(props: IProps) {
<Box> <Box>
<SubmitChangesButton onClick={() => { <SubmitChangesButton onClick={() => {
setApplying(true); setApplying(true);
saveArtistChanges(props.state.artistId, pendingChanges || {}) saveArtistChanges(props.state.id, pendingChanges || {})
.then(() => { .then(() => {
setApplying(false); setApplying(false);
props.dispatch({ props.dispatch({
@ -185,7 +202,6 @@ export default function ArtistWindow(props: IProps) {
{props.state.songsByArtist && <SongTable {props.state.songsByArtist && <SongTable
songs={props.state.songsByArtist} songs={props.state.songsByArtist}
songGetters={props.state.songGetters} songGetters={props.state.songGetters}
mainDispatch={props.mainDispatch}
/>} />}
{!props.state.songsByArtist && <CircularProgress />} {!props.state.songsByArtist && <CircularProgress />}
</Box> </Box>

@ -35,7 +35,7 @@ export default function ManageTagMenu(props: {
onDelete: () => void, onDelete: () => void,
onMove: (to: string | null) => void, onMove: (to: string | null) => void,
onMergeInto: (to: string) => void, onMergeInto: (to: string) => void,
onOpenInTab: () => void, onOpenTag: () => void,
tag: any, tag: any,
changedTags: any[], // Tags organized hierarchically with "children" fields changedTags: any[], // Tags organized hierarchically with "children" fields
}) { }) {
@ -53,7 +53,7 @@ export default function ManageTagMenu(props: {
<MenuItem <MenuItem
onClick={() => { onClick={() => {
props.onClose(); props.onClose();
props.onOpenInTab(); props.onOpenTag();
}} }}
>Browse</MenuItem> >Browse</MenuItem>
<MenuItem <MenuItem

@ -1,4 +1,4 @@
import React, { useEffect, useState, ReactFragment } from 'react'; import React, { useEffect, useState, ReactFragment, useReducer } from 'react';
import { WindowState, newWindowReducer, WindowType } from '../Windows'; import { WindowState, newWindowReducer, WindowType } from '../Windows';
import { Box, Typography, Chip, IconButton, useTheme, Button } from '@material-ui/core'; import { Box, Typography, Chip, IconButton, useTheme, Button } from '@material-ui/core';
import LoyaltyIcon from '@material-ui/icons/Loyalty'; import LoyaltyIcon from '@material-ui/icons/Loyalty';
@ -9,10 +9,10 @@ import ControlTagChanges, { TagChange, TagChangeType, submitTagChanges } from '.
import { queryTags } from '../../../lib/backend/queries'; import { queryTags } from '../../../lib/backend/queries';
import NewTagMenu from './NewTagMenu'; import NewTagMenu from './NewTagMenu';
import { v4 as genUuid } from 'uuid'; import { v4 as genUuid } from 'uuid';
import { MainWindowStateActions } from '../../MainWindow';
import LocalOfferIcon from '@material-ui/icons/LocalOffer'; import LocalOfferIcon from '@material-ui/icons/LocalOffer';
import { songGetters } from '../../../lib/songGetters'; import { songGetters } from '../../../lib/songGetters';
import Alert from '@material-ui/lab/Alert'; import Alert from '@material-ui/lab/Alert';
import { useHistory } from 'react-router';
var _ = require('lodash'); var _ = require('lodash');
export interface ManageTagsWindowState extends WindowState { export interface ManageTagsWindowState extends WindowState {
@ -120,7 +120,6 @@ export function SingleTag(props: {
tag: any, tag: any,
prependElems: any[], prependElems: any[],
dispatch: (action: any) => void, dispatch: (action: any) => void,
mainDispatch: (action: any) => void,
state: ManageTagsWindowState, state: ManageTagsWindowState,
changedTags: any[], changedTags: any[],
}) { }) {
@ -128,9 +127,11 @@ export function SingleTag(props: {
const hasChildren = 'children' in tag && tag.children.length > 0; const hasChildren = 'children' in tag && tag.children.length > 0;
const [menuPos, setMenuPos] = React.useState<null | number[]>(null); const [menuPos, setMenuPos] = React.useState<null | number[]>(null);
const [expanded, setExpanded] = useState<boolean>(false); const [expanded, setExpanded] = useState<boolean>(true);
const theme = useTheme(); const theme = useTheme();
const history = useHistory();
const onOpenMenu = (e: any) => { const onOpenMenu = (e: any) => {
setMenuPos([e.clientX, e.clientY]) setMenuPos([e.clientX, e.clientY])
}; };
@ -163,33 +164,23 @@ export function SingleTag(props: {
{props.prependElems} {props.prependElems}
<TagChip transparent={tag.proposeDelete} label={tagLabel} /> <TagChip transparent={tag.proposeDelete} label={tagLabel} />
</Box> </Box>
{hasChildren && expanded && tag.children.map((child: any) => <SingleTag {hasChildren && expanded && tag.children
tag={child} .sort((a: any, b: any) => a.name.localeCompare(b.name))
prependElems={[...props.prependElems, .map((child: any) => <SingleTag
<TagChip transparent={true} label={tagLabel} />, tag={child}
<Typography variant="h5">/</Typography>]} prependElems={[...props.prependElems,
dispatch={props.dispatch} <TagChip transparent={true} label={tagLabel} />,
mainDispatch={props.mainDispatch} <Typography variant="h5">/</Typography>]}
state={props.state} dispatch={props.dispatch}
changedTags={props.changedTags} state={props.state}
/>)} changedTags={props.changedTags}
/>)}
<ManageTagMenu <ManageTagMenu
position={menuPos} position={menuPos}
open={menuPos !== null} open={menuPos !== null}
onClose={onCloseMenu} onClose={onCloseMenu}
onOpenInTab={() => { onOpenTag={() => {
props.mainDispatch({ history.push('/tag/' + tag.tagId);
type: MainWindowStateActions.AddTab,
tabState: {
tabLabel: <><LocalOfferIcon />{tag.name}</>,
tagId: tag.tagId,
metadata: null,
songGetters: songGetters,
songsWithTag: null,
},
tabReducer: newWindowReducer[WindowType.Tag],
tabType: WindowType.Tag,
})
}} }}
onRename={(s: string) => { onRename={(s: string) => {
props.dispatch({ props.dispatch({
@ -349,10 +340,19 @@ function applyTagsChanges(tags: Record<string, any>, changes: TagChange[]) {
return retval; return retval;
} }
export default function ManageTagsWindow(props: { export default function ManageTagsWindow(props: {}) {
const [state, dispatch] = useReducer(ManageTagsWindowReducer, {
fetchedTags: null,
alert: null,
pendingChanges: [],
});
return <ManageTagsWindowControlled state={state} dispatch={dispatch} />
}
export function ManageTagsWindowControlled(props: {
state: ManageTagsWindowState, state: ManageTagsWindowState,
dispatch: (action: any) => void, dispatch: (action: any) => void,
mainDispatch: (action: any) => void,
}) { }) {
const [newTagMenuPos, setNewTagMenuPos] = React.useState<null | number[]>(null); const [newTagMenuPos, setNewTagMenuPos] = React.useState<null | number[]>(null);
@ -378,9 +378,9 @@ export default function ManageTagsWindow(props: {
})(); })();
}, [props.state.fetchedTags]); }, [props.state.fetchedTags]);
const tagsWithChanges = annotateTagsWithChanges(props.state.fetchedTags || {}, props.state.pendingChanges) const tagsWithChanges = annotateTagsWithChanges(props.state.fetchedTags || {}, props.state.pendingChanges || [])
const changedTags = organiseTags( const changedTags = organiseTags(
applyTagsChanges(props.state.fetchedTags || {}, props.state.pendingChanges), applyTagsChanges(props.state.fetchedTags || {}, props.state.pendingChanges || []),
null); null);
const tags = organiseTags(tagsWithChanges, null); const tags = organiseTags(tagsWithChanges, null);
@ -442,16 +442,17 @@ export default function ManageTagsWindow(props: {
mt={4} mt={4}
width="80%" width="80%"
> >
{tags && tags.length && tags.map((tag: any) => { {tags && tags.length && tags
return <SingleTag .sort((a: any, b: any) => a.name.localeCompare(b.name))
tag={tag} .map((tag: any) => {
prependElems={[]} return <SingleTag
dispatch={props.dispatch} tag={tag}
mainDispatch={props.mainDispatch} prependElems={[]}
state={props.state} dispatch={props.dispatch}
changedTags={changedTags} state={props.state}
/>; changedTags={changedTags}
})} />;
})}
<Box mt={3}><CreateTagButton onClick={(e: any) => { onOpenNewTagMenu(e) }} /></Box> <Box mt={3}><CreateTagButton onClick={(e: any) => { onOpenNewTagMenu(e) }} /></Box>
</Box> </Box>
</Box> </Box>

@ -1,4 +1,4 @@
import React, { useEffect } from 'react'; import React, { useEffect, useReducer } from 'react';
import { createMuiTheme, Box, LinearProgress } from '@material-ui/core'; import { createMuiTheme, Box, LinearProgress } from '@material-ui/core';
import { QueryElem, toApiQuery, QueryLeafBy, QueryLeafOp } from '../../../lib/query/Query'; import { QueryElem, toApiQuery, QueryLeafBy, QueryLeafOp } from '../../../lib/query/Query';
import QueryBuilder from '../../querybuilder/QueryBuilder'; import QueryBuilder from '../../querybuilder/QueryBuilder';
@ -47,7 +47,7 @@ async function getArtistNames(filter: string) {
limit: -1, limit: -1,
}); });
return [...(new Set([...(artists.map((a:any) => a.name))]))]; return [...(new Set([...(artists.map((a: any) => a.name))]))];
} }
async function getAlbumNames(filter: string) { async function getAlbumNames(filter: string) {
@ -61,7 +61,7 @@ async function getAlbumNames(filter: string) {
limit: -1, limit: -1,
}); });
return [...(new Set([...(albums.map((a:any) => a.name))]))]; return [...(new Set([...(albums.map((a: any) => a.name))]))];
} }
async function getSongTitles(filter: string) { async function getSongTitles(filter: string) {
@ -75,7 +75,7 @@ async function getSongTitles(filter: string) {
limit: -1, limit: -1,
}); });
return [...(new Set([...(songs.map((s:any) => s.title))]))]; return [...(new Set([...(songs.map((s: any) => s.title))]))];
} }
async function getTagItems() { async function getTagItems() {
@ -98,14 +98,20 @@ export function QueryWindowReducer(state: QueryWindowState, action: any) {
throw new Error("Unimplemented QueryWindow state update.") throw new Error("Unimplemented QueryWindow state update.")
} }
} }
export default function QueryWindow(props: {}) {
const [state, dispatch] = useReducer(QueryWindowReducer, {
editingQuery: false,
query: null,
resultsForQuery: null,
});
export interface IProps { return <QueryWindowControlled state={state} dispatch={dispatch} />
state: QueryWindowState,
dispatch: (action: any) => void,
mainDispatch: (action: any) => void,
} }
export default function QueryWindow(props: IProps) { export function QueryWindowControlled(props: {
state: QueryWindowState,
dispatch: (action: any) => void,
}) {
let query = props.state.query; let query = props.state.query;
let editing = props.state.editingQuery; let editing = props.state.editingQuery;
let resultsFor = props.state.resultsForQuery; let resultsFor = props.state.resultsForQuery;
@ -170,7 +176,6 @@ export default function QueryWindow(props: IProps) {
<SongTable <SongTable
songs={showResults} songs={showResults}
songGetters={songGetters} songGetters={songGetters}
mainDispatch={props.mainDispatch}
/> />
{loading && <LinearProgress />} {loading && <LinearProgress />}
</Box> </Box>

@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState, useReducer } from 'react';
import { Box, Typography, IconButton, Button, CircularProgress } from '@material-ui/core'; import { Box, Typography, IconButton, Button, CircularProgress } from '@material-ui/core';
import AudiotrackIcon from '@material-ui/icons/Audiotrack'; import AudiotrackIcon from '@material-ui/icons/Audiotrack';
import PersonIcon from '@material-ui/icons/Person'; import PersonIcon from '@material-ui/icons/Person';
@ -13,12 +13,14 @@ import SubmitChangesButton from '../../common/SubmitChangesButton';
import { saveSongChanges } from '../../../lib/saveChanges'; import { saveSongChanges } from '../../../lib/saveChanges';
import { QueryLeafBy, QueryLeafOp } from '../../../lib/query/Query'; import { QueryLeafBy, QueryLeafOp } from '../../../lib/query/Query';
import { querySongs } from '../../../lib/backend/queries'; import { querySongs } from '../../../lib/backend/queries';
import { songGetters } from '../../../lib/songGetters';
import { useParams } from 'react-router';
export type SongMetadata = serverApi.SongDetails; export type SongMetadata = serverApi.SongDetails;
export type SongMetadataChanges = serverApi.ModifySongRequest; export type SongMetadataChanges = serverApi.ModifySongRequest;
export interface SongWindowState extends WindowState { export interface SongWindowState extends WindowState {
songId: number, id: number,
metadata: SongMetadata | null, metadata: SongMetadata | null,
pendingChanges: SongMetadataChanges | null, pendingChanges: SongMetadataChanges | null,
} }
@ -45,7 +47,6 @@ export function SongWindowReducer(state: SongWindowState, action: any) {
export interface IProps { export interface IProps {
state: SongWindowState, state: SongWindowState,
dispatch: (action: any) => void, dispatch: (action: any) => void,
mainDispatch: (action: any) => void,
} }
export async function getSongMetadata(id: number) { export async function getSongMetadata(id: number) {
@ -60,12 +61,26 @@ export async function getSongMetadata(id: number) {
}))[0]; }))[0];
} }
export default function SongWindow(props: IProps) { export default function SongWindow(props: {}) {
const { id } = useParams();
const [state, dispatch] = useReducer(SongWindowReducer, {
id: id,
metadata: null,
pendingChanges: null,
});
return <SongWindowControlled state={state} dispatch={dispatch} />
}
export function SongWindowControlled(props: {
state: SongWindowState,
dispatch: (action: any) => void,
}) {
let metadata = props.state.metadata; let metadata = props.state.metadata;
let pendingChanges = props.state.pendingChanges; let pendingChanges = props.state.pendingChanges;
useEffect(() => { useEffect(() => {
getSongMetadata(props.state.songId) getSongMetadata(props.state.id)
.then((m: SongMetadata) => { .then((m: SongMetadata) => {
props.dispatch({ props.dispatch({
type: SongWindowStateActions.SetMetadata, type: SongWindowStateActions.SetMetadata,
@ -122,7 +137,7 @@ export default function SongWindow(props: IProps) {
<Box> <Box>
<SubmitChangesButton onClick={() => { <SubmitChangesButton onClick={() => {
setApplying(true); setApplying(true);
saveSongChanges(props.state.songId, pendingChanges || {}) saveSongChanges(props.state.id, pendingChanges || {})
.then(() => { .then(() => {
setApplying(false); setApplying(false);
props.dispatch({ props.dispatch({

@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState, useReducer } from 'react';
import { Box, Typography, IconButton, CircularProgress } 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';
@ -10,6 +10,8 @@ import SongTable, { SongGetters } from '../../tables/ResultsTable';
import { saveTagChanges } from '../../../lib/saveChanges'; import { saveTagChanges } from '../../../lib/saveChanges';
import { queryTags, querySongs } from '../../../lib/backend/queries'; import { queryTags, querySongs } from '../../../lib/backend/queries';
import { QueryLeafBy, QueryLeafOp } from '../../../lib/query/Query'; import { QueryLeafBy, QueryLeafOp } from '../../../lib/query/Query';
import { songGetters } from '../../../lib/songGetters';
import { useParams } from 'react-router';
var _ = require('lodash'); var _ = require('lodash');
export interface FullTagMetadata extends serverApi.TagDetails { export interface FullTagMetadata extends serverApi.TagDetails {
@ -21,7 +23,7 @@ export type TagMetadata = FullTagMetadata;
export type TagMetadataChanges = serverApi.ModifyTagRequest; export type TagMetadataChanges = serverApi.ModifyTagRequest;
export interface TagWindowState extends WindowState { export interface TagWindowState extends WindowState {
tagId: number, id: number,
metadata: TagMetadata | null, metadata: TagMetadata | null,
pendingChanges: TagMetadataChanges | null, pendingChanges: TagMetadataChanges | null,
songsWithTag: any[] | null, songsWithTag: any[] | null,
@ -50,12 +52,6 @@ export function TagWindowReducer(state: TagWindowState, action: any) {
} }
} }
export interface IProps {
state: TagWindowState,
dispatch: (action: any) => void,
mainDispatch: (action: any) => void,
}
export async function getTagMetadata(id: number) { export async function getTagMetadata(id: number) {
var tag = (await queryTags({ var tag = (await queryTags({
query: { query: {
@ -80,13 +76,29 @@ export async function getTagMetadata(id: number) {
return tag; return tag;
} }
export default function TagWindow(props: IProps) { export default function TagWindow(props: {}) {
const { id } = useParams();
const [state, dispatch] = useReducer(TagWindowReducer,{
id: id,
metadata: null,
pendingChanges: null,
songGetters: songGetters,
songsWithTag: null,
});
return <TagWindowControlled state={state} dispatch={dispatch} />
}
export function TagWindowControlled(props: {
state: TagWindowState,
dispatch: (action: any) => void,
}) {
let metadata = props.state.metadata; let metadata = props.state.metadata;
let pendingChanges = props.state.pendingChanges; let pendingChanges = props.state.pendingChanges;
// Effect to get the tag's metadata. // Effect to get the tag's metadata.
useEffect(() => { useEffect(() => {
getTagMetadata(props.state.tagId) getTagMetadata(props.state.id)
.then((m: TagMetadata) => { .then((m: TagMetadata) => {
props.dispatch({ props.dispatch({
type: TagWindowStateActions.SetMetadata, type: TagWindowStateActions.SetMetadata,
@ -103,7 +115,7 @@ export default function TagWindow(props: IProps) {
const songs = await querySongs({ const songs = await querySongs({
query: { query: {
a: QueryLeafBy.TagId, a: QueryLeafBy.TagId,
b: props.state.tagId, b: props.state.id,
leafOp: QueryLeafOp.Equals, leafOp: QueryLeafOp.Equals,
}, },
offset: 0, offset: 0,
@ -163,7 +175,7 @@ export default function TagWindow(props: IProps) {
<Box> <Box>
<SubmitChangesButton onClick={() => { <SubmitChangesButton onClick={() => {
setApplying(true); setApplying(true);
saveTagChanges(props.state.tagId, pendingChanges || {}) saveTagChanges(props.state.id, pendingChanges || {})
.then(() => { .then(() => {
setApplying(false); setApplying(false);
props.dispatch({ props.dispatch({
@ -213,7 +225,6 @@ export default function TagWindow(props: IProps) {
{props.state.songsWithTag && <SongTable {props.state.songsWithTag && <SongTable
songs={props.state.songsWithTag} songs={props.state.songsWithTag}
songGetters={props.state.songGetters} songGetters={props.state.songGetters}
mainDispatch={props.mainDispatch}
/>} />}
{!props.state.songsWithTag && <CircularProgress />} {!props.state.songsWithTag && <CircularProgress />}
</Box> </Box>

Loading…
Cancel
Save