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 { HTML5Backend } from 'react-dnd-html5-backend';
import {
HashRouter as Router,
Switch,
Route
} from "react-router-dom";
import MainWindow from './components/MainWindow';
function App() {
return (
<Router>
<DndProvider backend={HTML5Backend}>
<Switch>
<Route path="/">
<MainWindow/>
</Route>
</Switch>
</DndProvider>
</Router>
<DndProvider backend={HTML5Backend}>
<MainWindow />
</DndProvider>
);
}

@ -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 { grey } from '@material-ui/core/colors';
import AppBar from './appbar/AppBar';
import QueryWindow from './windows/query/QueryWindow';
import { NewTabProps } from './appbar/AddTabMenu';
import AppBar, { AppBarTab } from './appbar/AppBar';
import QueryWindow, { QueryWindowReducer } from './windows/query/QueryWindow';
import { newWindowState, newWindowReducer, WindowType } from './windows/Windows';
import ArtistWindow from './windows/artist/ArtistWindow';
import AlbumWindow from './windows/album/AlbumWindow';
import TagWindow from './windows/tag/TagWindow';
import SongWindow from './windows/song/SongWindow';
import ManageTagsWindow from './windows/manage_tags/ManageTagsWindow';
import { BrowserRouter, Switch, Route, useParams, Redirect } from 'react-router-dom';
var _ = require('lodash');
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) {
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}>
<CssBaseline />
<AppBar
tabLabels={state.tabStates.map((s: any) => s.tabLabel)}
selectedTab={state.activeTab}
setSelectedTab={(t: number) => dispatch({ type: MainWindowStateActions.SetActiveTab, value: t })}
onCloseTab={(t: number) => dispatch({ type: MainWindowStateActions.CloseTab, idx: t })}
onAddTab={(w: NewTabProps) => {
dispatch({
type: MainWindowStateActions.AddTab,
tabState: newWindowState[w.windowType](),
tabReducer: newWindowReducer[w.windowType],
tabType: w.windowType,
})
}}
/>
{windows[state.activeTab]}
<BrowserRouter>
<Switch>
<Route exact path="/">
<Redirect to={"/query"} />
</Route>
<Route path="/query">
<AppBar selectedTab={AppBarTab.Query} />
<QueryWindow/>
</Route>
<Route path="/artist/:id">
<AppBar selectedTab={null} />
<ArtistWindow/>
</Route>
<Route path="/tag/:id">
<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>
}

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

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

@ -1,17 +1,17 @@
import React from 'react';
import { QueryWindowReducer } from "./query/QueryWindow";
import { ArtistWindowReducer } from "./artist/ArtistWindow";
import React, { useReducer } from 'react';
import QueryWindow, { QueryWindowReducer } from "./query/QueryWindow";
import ArtistWindow, { ArtistWindowReducer } from "./artist/ArtistWindow";
import SearchIcon from '@material-ui/icons/Search';
import PersonIcon from '@material-ui/icons/Person';
import AlbumIcon from '@material-ui/icons/Album';
import LocalOfferIcon from '@material-ui/icons/LocalOffer';
import AudiotrackIcon from '@material-ui/icons/Audiotrack';
import LoyaltyIcon from '@material-ui/icons/Loyalty';
import { SongWindowReducer } from './song/SongWindow';
import { AlbumWindowReducer } from './album/AlbumWindow';
import { TagWindowReducer } from './tag/TagWindow';
import SongWindow, { SongWindowReducer } from './song/SongWindow';
import AlbumWindow, { AlbumWindowReducer } from './album/AlbumWindow';
import TagWindow, { TagWindowReducer } from './tag/TagWindow';
import { songGetters } from '../../lib/songGetters';
import { ManageTagsWindowReducer } from './manage_tags/ManageTagsWindow';
import ManageTagsWindow, { ManageTagsWindowReducer } from './manage_tags/ManageTagsWindow';
export enum WindowType {
Query = "Query",
@ -22,9 +22,7 @@ export enum WindowType {
ManageTags = "ManageTags",
}
export interface WindowState {
tabLabel: string,
}
export interface WindowState { }
export const newWindowReducer = {
[WindowType.Query]: QueryWindowReducer,
@ -38,7 +36,6 @@ export const newWindowReducer = {
export const newWindowState = {
[WindowType.Query]: () => {
return {
tabLabel: <><SearchIcon/>Query</>,
editingQuery: false,
query: null,
resultsForQuery: null,
@ -46,8 +43,7 @@ export const newWindowState = {
},
[WindowType.Artist]: () => {
return {
tabLabel: <><PersonIcon/>Artist 1</>,
artistId: 1,
id: 1,
metadata: null,
pendingChanges: null,
songGetters: songGetters,
@ -56,8 +52,7 @@ export const newWindowState = {
},
[WindowType.Album]: () => {
return {
tabLabel: <><AlbumIcon/>Album 1</>,
albumId: 1,
id: 1,
metadata: null,
pendingChanges: null,
songGetters: songGetters,
@ -66,16 +61,14 @@ export const newWindowState = {
},
[WindowType.Song]: () => {
return {
tabLabel: <><AudiotrackIcon/>Song 1</>,
songId: 1,
id: 1,
metadata: null,
pendingChanges: null,
}
},
[WindowType.Tag]: () => {
return {
tabLabel: <><LocalOfferIcon/>Tag 1</>,
tagId: 1,
id: 1,
metadata: null,
pendingChanges: null,
songGetters: songGetters,
@ -84,8 +77,8 @@ export const newWindowState = {
},
[WindowType.ManageTags]: () => {
return {
tabLabel: <><LoyaltyIcon/>Manage Tags</>,
fetchedTags: null,
alert: null,
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 AlbumIcon from '@material-ui/icons/Album';
import * as serverApi from '../../../api';
@ -10,13 +10,15 @@ import SongTable, { SongGetters } from '../../tables/ResultsTable';
import { saveAlbumChanges } from '../../../lib/saveChanges';
import { QueryLeafBy, QueryLeafOp } from '../../../lib/query/Query';
import { queryAlbums, querySongs } from '../../../lib/backend/queries';
import { songGetters } from '../../../lib/songGetters';
import { useParams } from 'react-router';
var _ = require('lodash');
export type AlbumMetadata = serverApi.AlbumDetails;
export type AlbumMetadataChanges = serverApi.ModifyAlbumRequest;
export interface AlbumWindowState extends WindowState {
albumId: number,
id: number,
metadata: AlbumMetadata | null,
pendingChanges: AlbumMetadataChanges | 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) {
return (await queryAlbums({
query: {
@ -63,13 +59,29 @@ export async function getAlbumMetadata(id: number) {
}))[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 pendingChanges = props.state.pendingChanges;
// Effect to get the album's metadata.
useEffect(() => {
getAlbumMetadata(props.state.albumId)
getAlbumMetadata(props.state.id)
.then((m: AlbumMetadata) => {
props.dispatch({
type: AlbumWindowStateActions.SetMetadata,
@ -86,7 +98,7 @@ export default function AlbumWindow(props: IProps) {
const songs = await querySongs({
query: {
a: QueryLeafBy.AlbumId,
b: props.state.albumId,
b: props.state.id,
leafOp: QueryLeafOp.Equals,
},
offset: 0,
@ -95,7 +107,7 @@ export default function AlbumWindow(props: IProps) {
props.dispatch({
type: AlbumWindowStateActions.SetSongs,
value: songs,
});
});
})();
}, [props.state.songsOnAlbum]);
@ -135,7 +147,7 @@ export default function AlbumWindow(props: IProps) {
<Box>
<SubmitChangesButton onClick={() => {
setApplying(true);
saveAlbumChanges(props.state.albumId, pendingChanges || {})
saveAlbumChanges(props.state.id, pendingChanges || {})
.then(() => {
setApplying(false);
props.dispatch({
@ -185,7 +197,6 @@ export default function AlbumWindow(props: IProps) {
{props.state.songsOnAlbum && <SongTable
songs={props.state.songsOnAlbum}
songGetters={props.state.songGetters}
mainDispatch={props.mainDispatch}
/>}
{!props.state.songsOnAlbum && <CircularProgress />}
</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 PersonIcon from '@material-ui/icons/Person';
import * as serverApi from '../../../api';
@ -10,13 +10,15 @@ import SongTable, { SongGetters } from '../../tables/ResultsTable';
import { saveArtistChanges } from '../../../lib/saveChanges';
import { QueryLeafBy, QueryLeafOp } from '../../../lib/query/Query';
import { queryArtists, querySongs } from '../../../lib/backend/queries';
import { songGetters } from '../../../lib/songGetters';
import { useParams } from 'react-router';
var _ = require('lodash');
export type ArtistMetadata = serverApi.ArtistDetails;
export type ArtistMetadataChanges = serverApi.ModifyArtistRequest;
export interface ArtistWindowState extends WindowState {
artistId: number,
id: number,
metadata: ArtistMetadata | null,
pendingChanges: ArtistMetadataChanges | null,
songsByArtist: any[] | null,
@ -48,7 +50,6 @@ export function ArtistWindowReducer(state: ArtistWindowState, action: any) {
export interface IProps {
state: ArtistWindowState,
dispatch: (action: any) => void,
mainDispatch: (action: any) => void,
}
export async function getArtistMetadata(id: number) {
@ -63,13 +64,29 @@ export async function getArtistMetadata(id: number) {
}))[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 pendingChanges = props.state.pendingChanges;
// Effect to get the artist's metadata.
useEffect(() => {
getArtistMetadata(props.state.artistId)
getArtistMetadata(props.state.id)
.then((m: ArtistMetadata) => {
props.dispatch({
type: ArtistWindowStateActions.SetMetadata,
@ -86,7 +103,7 @@ export default function ArtistWindow(props: IProps) {
const songs = await querySongs({
query: {
a: QueryLeafBy.ArtistId,
b: props.state.artistId,
b: props.state.id,
leafOp: QueryLeafOp.Equals,
},
offset: 0,
@ -95,7 +112,7 @@ export default function ArtistWindow(props: IProps) {
props.dispatch({
type: ArtistWindowStateActions.SetSongs,
value: songs,
});
});
})();
}, [props.state.songsByArtist]);
@ -135,7 +152,7 @@ export default function ArtistWindow(props: IProps) {
<Box>
<SubmitChangesButton onClick={() => {
setApplying(true);
saveArtistChanges(props.state.artistId, pendingChanges || {})
saveArtistChanges(props.state.id, pendingChanges || {})
.then(() => {
setApplying(false);
props.dispatch({
@ -185,7 +202,6 @@ export default function ArtistWindow(props: IProps) {
{props.state.songsByArtist && <SongTable
songs={props.state.songsByArtist}
songGetters={props.state.songGetters}
mainDispatch={props.mainDispatch}
/>}
{!props.state.songsByArtist && <CircularProgress />}
</Box>

@ -35,7 +35,7 @@ export default function ManageTagMenu(props: {
onDelete: () => void,
onMove: (to: string | null) => void,
onMergeInto: (to: string) => void,
onOpenInTab: () => void,
onOpenTag: () => void,
tag: any,
changedTags: any[], // Tags organized hierarchically with "children" fields
}) {
@ -53,7 +53,7 @@ export default function ManageTagMenu(props: {
<MenuItem
onClick={() => {
props.onClose();
props.onOpenInTab();
props.onOpenTag();
}}
>Browse</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 { Box, Typography, Chip, IconButton, useTheme, Button } from '@material-ui/core';
import LoyaltyIcon from '@material-ui/icons/Loyalty';
@ -9,10 +9,10 @@ import ControlTagChanges, { TagChange, TagChangeType, submitTagChanges } from '.
import { queryTags } from '../../../lib/backend/queries';
import NewTagMenu from './NewTagMenu';
import { v4 as genUuid } from 'uuid';
import { MainWindowStateActions } from '../../MainWindow';
import LocalOfferIcon from '@material-ui/icons/LocalOffer';
import { songGetters } from '../../../lib/songGetters';
import Alert from '@material-ui/lab/Alert';
import { useHistory } from 'react-router';
var _ = require('lodash');
export interface ManageTagsWindowState extends WindowState {
@ -120,7 +120,6 @@ export function SingleTag(props: {
tag: any,
prependElems: any[],
dispatch: (action: any) => void,
mainDispatch: (action: any) => void,
state: ManageTagsWindowState,
changedTags: any[],
}) {
@ -128,9 +127,11 @@ export function SingleTag(props: {
const hasChildren = 'children' in tag && tag.children.length > 0;
const [menuPos, setMenuPos] = React.useState<null | number[]>(null);
const [expanded, setExpanded] = useState<boolean>(false);
const [expanded, setExpanded] = useState<boolean>(true);
const theme = useTheme();
const history = useHistory();
const onOpenMenu = (e: any) => {
setMenuPos([e.clientX, e.clientY])
};
@ -163,33 +164,23 @@ export function SingleTag(props: {
{props.prependElems}
<TagChip transparent={tag.proposeDelete} label={tagLabel} />
</Box>
{hasChildren && expanded && tag.children.map((child: any) => <SingleTag
tag={child}
prependElems={[...props.prependElems,
<TagChip transparent={true} label={tagLabel} />,
<Typography variant="h5">/</Typography>]}
dispatch={props.dispatch}
mainDispatch={props.mainDispatch}
state={props.state}
changedTags={props.changedTags}
/>)}
{hasChildren && expanded && tag.children
.sort((a: any, b: any) => a.name.localeCompare(b.name))
.map((child: any) => <SingleTag
tag={child}
prependElems={[...props.prependElems,
<TagChip transparent={true} label={tagLabel} />,
<Typography variant="h5">/</Typography>]}
dispatch={props.dispatch}
state={props.state}
changedTags={props.changedTags}
/>)}
<ManageTagMenu
position={menuPos}
open={menuPos !== null}
onClose={onCloseMenu}
onOpenInTab={() => {
props.mainDispatch({
type: MainWindowStateActions.AddTab,
tabState: {
tabLabel: <><LocalOfferIcon />{tag.name}</>,
tagId: tag.tagId,
metadata: null,
songGetters: songGetters,
songsWithTag: null,
},
tabReducer: newWindowReducer[WindowType.Tag],
tabType: WindowType.Tag,
})
onOpenTag={() => {
history.push('/tag/' + tag.tagId);
}}
onRename={(s: string) => {
props.dispatch({
@ -349,10 +340,19 @@ function applyTagsChanges(tags: Record<string, any>, changes: TagChange[]) {
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,
dispatch: (action: any) => void,
mainDispatch: (action: any) => void,
}) {
const [newTagMenuPos, setNewTagMenuPos] = React.useState<null | number[]>(null);
@ -378,9 +378,9 @@ export default function ManageTagsWindow(props: {
})();
}, [props.state.fetchedTags]);
const tagsWithChanges = annotateTagsWithChanges(props.state.fetchedTags || {}, props.state.pendingChanges)
const tagsWithChanges = annotateTagsWithChanges(props.state.fetchedTags || {}, props.state.pendingChanges || [])
const changedTags = organiseTags(
applyTagsChanges(props.state.fetchedTags || {}, props.state.pendingChanges),
applyTagsChanges(props.state.fetchedTags || {}, props.state.pendingChanges || []),
null);
const tags = organiseTags(tagsWithChanges, null);
@ -442,16 +442,17 @@ export default function ManageTagsWindow(props: {
mt={4}
width="80%"
>
{tags && tags.length && tags.map((tag: any) => {
return <SingleTag
tag={tag}
prependElems={[]}
dispatch={props.dispatch}
mainDispatch={props.mainDispatch}
state={props.state}
changedTags={changedTags}
/>;
})}
{tags && tags.length && tags
.sort((a: any, b: any) => a.name.localeCompare(b.name))
.map((tag: any) => {
return <SingleTag
tag={tag}
prependElems={[]}
dispatch={props.dispatch}
state={props.state}
changedTags={changedTags}
/>;
})}
<Box mt={3}><CreateTagButton onClick={(e: any) => { onOpenNewTagMenu(e) }} /></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 { QueryElem, toApiQuery, QueryLeafBy, QueryLeafOp } from '../../../lib/query/Query';
import QueryBuilder from '../../querybuilder/QueryBuilder';
@ -47,7 +47,7 @@ async function getArtistNames(filter: string) {
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) {
@ -61,7 +61,7 @@ async function getAlbumNames(filter: string) {
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) {
@ -75,7 +75,7 @@ async function getSongTitles(filter: string) {
limit: -1,
});
return [...(new Set([...(songs.map((s:any) => s.title))]))];
return [...(new Set([...(songs.map((s: any) => s.title))]))];
}
async function getTagItems() {
@ -98,14 +98,20 @@ export function QueryWindowReducer(state: QueryWindowState, action: any) {
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 {
state: QueryWindowState,
dispatch: (action: any) => void,
mainDispatch: (action: any) => void,
return <QueryWindowControlled state={state} dispatch={dispatch} />
}
export default function QueryWindow(props: IProps) {
export function QueryWindowControlled(props: {
state: QueryWindowState,
dispatch: (action: any) => void,
}) {
let query = props.state.query;
let editing = props.state.editingQuery;
let resultsFor = props.state.resultsForQuery;
@ -170,7 +176,6 @@ export default function QueryWindow(props: IProps) {
<SongTable
songs={showResults}
songGetters={songGetters}
mainDispatch={props.mainDispatch}
/>
{loading && <LinearProgress />}
</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 AudiotrackIcon from '@material-ui/icons/Audiotrack';
import PersonIcon from '@material-ui/icons/Person';
@ -13,12 +13,14 @@ import SubmitChangesButton from '../../common/SubmitChangesButton';
import { saveSongChanges } from '../../../lib/saveChanges';
import { QueryLeafBy, QueryLeafOp } from '../../../lib/query/Query';
import { querySongs } from '../../../lib/backend/queries';
import { songGetters } from '../../../lib/songGetters';
import { useParams } from 'react-router';
export type SongMetadata = serverApi.SongDetails;
export type SongMetadataChanges = serverApi.ModifySongRequest;
export interface SongWindowState extends WindowState {
songId: number,
id: number,
metadata: SongMetadata | null,
pendingChanges: SongMetadataChanges | null,
}
@ -45,7 +47,6 @@ export function SongWindowReducer(state: SongWindowState, action: any) {
export interface IProps {
state: SongWindowState,
dispatch: (action: any) => void,
mainDispatch: (action: any) => void,
}
export async function getSongMetadata(id: number) {
@ -60,12 +61,26 @@ export async function getSongMetadata(id: number) {
}))[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 pendingChanges = props.state.pendingChanges;
useEffect(() => {
getSongMetadata(props.state.songId)
getSongMetadata(props.state.id)
.then((m: SongMetadata) => {
props.dispatch({
type: SongWindowStateActions.SetMetadata,
@ -122,7 +137,7 @@ export default function SongWindow(props: IProps) {
<Box>
<SubmitChangesButton onClick={() => {
setApplying(true);
saveSongChanges(props.state.songId, pendingChanges || {})
saveSongChanges(props.state.id, pendingChanges || {})
.then(() => {
setApplying(false);
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 LocalOfferIcon from '@material-ui/icons/LocalOffer';
import * as serverApi from '../../../api';
@ -10,6 +10,8 @@ import SongTable, { SongGetters } from '../../tables/ResultsTable';
import { saveTagChanges } from '../../../lib/saveChanges';
import { queryTags, querySongs } from '../../../lib/backend/queries';
import { QueryLeafBy, QueryLeafOp } from '../../../lib/query/Query';
import { songGetters } from '../../../lib/songGetters';
import { useParams } from 'react-router';
var _ = require('lodash');
export interface FullTagMetadata extends serverApi.TagDetails {
@ -21,7 +23,7 @@ export type TagMetadata = FullTagMetadata;
export type TagMetadataChanges = serverApi.ModifyTagRequest;
export interface TagWindowState extends WindowState {
tagId: number,
id: number,
metadata: TagMetadata | null,
pendingChanges: TagMetadataChanges | 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) {
var tag = (await queryTags({
query: {
@ -80,13 +76,29 @@ export async function getTagMetadata(id: number) {
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 pendingChanges = props.state.pendingChanges;
// Effect to get the tag's metadata.
useEffect(() => {
getTagMetadata(props.state.tagId)
getTagMetadata(props.state.id)
.then((m: TagMetadata) => {
props.dispatch({
type: TagWindowStateActions.SetMetadata,
@ -103,7 +115,7 @@ export default function TagWindow(props: IProps) {
const songs = await querySongs({
query: {
a: QueryLeafBy.TagId,
b: props.state.tagId,
b: props.state.id,
leafOp: QueryLeafOp.Equals,
},
offset: 0,
@ -163,7 +175,7 @@ export default function TagWindow(props: IProps) {
<Box>
<SubmitChangesButton onClick={() => {
setApplying(true);
saveTagChanges(props.state.tagId, pendingChanges || {})
saveTagChanges(props.state.id, pendingChanges || {})
.then(() => {
setApplying(false);
props.dispatch({
@ -213,7 +225,6 @@ export default function TagWindow(props: IProps) {
{props.state.songsWithTag && <SongTable
songs={props.state.songsWithTag}
songGetters={props.state.songGetters}
mainDispatch={props.mainDispatch}
/>}
{!props.state.songsWithTag && <CircularProgress />}
</Box>

Loading…
Cancel
Save