From 87af6e18a4b534df8a3dd2aef45f27117871be9e Mon Sep 17 00:00:00 2001 From: Sander Vocke Date: Tue, 10 Nov 2020 15:10:24 +0000 Subject: [PATCH] Back to simple declarative routing. (#28) Fix routing. Boy-scout: alphabetic ordering in the tags window Reviewed-on: https://gitea.octiron.soleus.nu/sander/MuDBase/pulls/28 --- client/src/App.tsx | 18 +- client/src/components/MainWindow.tsx | 181 ++++-------------- client/src/components/appbar/AddTabMenu.tsx | 40 ---- client/src/components/appbar/AppBar.tsx | 93 +++------ client/src/components/tables/ResultsTable.tsx | 65 +------ client/src/components/windows/Windows.tsx | 33 ++-- .../components/windows/album/AlbumWindow.tsx | 39 ++-- .../windows/artist/ArtistWindow.tsx | 34 +++- .../windows/manage_tags/ManageTagMenu.tsx | 4 +- .../windows/manage_tags/ManageTagsWindow.tsx | 83 ++++---- .../components/windows/query/QueryWindow.tsx | 25 ++- .../components/windows/song/SongWindow.tsx | 27 ++- .../src/components/windows/tag/TagWindow.tsx | 37 ++-- 13 files changed, 243 insertions(+), 436 deletions(-) delete mode 100644 client/src/components/appbar/AddTabMenu.tsx diff --git a/client/src/App.tsx b/client/src/App.tsx index 6f9ae76..075a107 100644 --- a/client/src/App.tsx +++ b/client/src/App.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 ( - - - - - - - - - - + + + ); } diff --git a/client/src/components/MainWindow.tsx b/client/src/components/MainWindow.tsx index d600867..2839106 100644 --- a/client/src/components/MainWindow.tsx +++ b/client/src/components/MainWindow.tsx @@ -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[], - 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 - case WindowType.Artist: - return - case WindowType.Album: - return - case WindowType.Tag: - return - case WindowType.Song: - return - case WindowType.ManageTags: - return - default: - throw new Error("Unimplemented window type"); - } - }); - return - 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]} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + } \ No newline at end of file diff --git a/client/src/components/appbar/AddTabMenu.tsx b/client/src/components/appbar/AddTabMenu.tsx deleted file mode 100644 index a9e0755..0000000 --- a/client/src/components/appbar/AddTabMenu.tsx +++ /dev/null @@ -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 - New Tab - { - props.onClose(); - props.onCreateTab({ - windowType: WindowType.Query, - }) - }} - >{WindowType.Query} - { - props.onClose(); - props.onCreateTab({ - windowType: WindowType.ManageTags, - }) - }} - >Manage Tags - -} \ No newline at end of file diff --git a/client/src/components/appbar/AppBar.tsx b/client/src/components/appbar/AppBar.tsx index 820b479..28809f2 100644 --- a/client/src/components/appbar/AppBar.tsx +++ b/client/src/components/appbar/AppBar.tsx @@ -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 = - {label} - - - - - - ; - - return +export const appBarTabProps: Record = { + [AppBarTab.Query]: { + label: Query, + path: "/query", + }, + [AppBarTab.Tags]: { + label: Tags, + path: "/tags", + }, } -export default function AppBar(props: IProps) { - const [addMenuAnchorEl, setAddMenuAnchorEl] = React.useState(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 <> - - error - + + + error + + props.setSelectedTab(v)} + onChange={(e: any, val: AppBarTab) => history.push(appBarTabProps[val].path)} variant="scrollable" scrollButtons="auto" > - {props.tabLabels.map((l: string, idx: number) => props.onCloseTab(idx)} />)} - - } \ No newline at end of file diff --git a/client/src/components/tables/ResultsTable.tsx b/client/src/components/tables/ResultsTable.tsx index cce223f..5fee3d1 100644 --- a/client/src/components/tables/ResultsTable.tsx +++ b/client/src/components/tables/ResultsTable.tsx @@ -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: <>{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: <>{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: <>{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: <>{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) => { diff --git a/client/src/components/windows/Windows.tsx b/client/src/components/windows/Windows.tsx index 1411e33..edd6768 100644 --- a/client/src/components/windows/Windows.tsx +++ b/client/src/components/windows/Windows.tsx @@ -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: <>Query, editingQuery: false, query: null, resultsForQuery: null, @@ -46,8 +43,7 @@ export const newWindowState = { }, [WindowType.Artist]: () => { return { - tabLabel: <>Artist 1, - artistId: 1, + id: 1, metadata: null, pendingChanges: null, songGetters: songGetters, @@ -56,8 +52,7 @@ export const newWindowState = { }, [WindowType.Album]: () => { return { - tabLabel: <>Album 1, - albumId: 1, + id: 1, metadata: null, pendingChanges: null, songGetters: songGetters, @@ -66,16 +61,14 @@ export const newWindowState = { }, [WindowType.Song]: () => { return { - tabLabel: <>Song 1, - songId: 1, + id: 1, metadata: null, pendingChanges: null, } }, [WindowType.Tag]: () => { return { - tabLabel: <>Tag 1, - tagId: 1, + id: 1, metadata: null, pendingChanges: null, songGetters: songGetters, @@ -84,8 +77,8 @@ export const newWindowState = { }, [WindowType.ManageTags]: () => { return { - tabLabel: <>Manage Tags, fetchedTags: null, + alert: null, pendingChanges: [], } } diff --git a/client/src/components/windows/album/AlbumWindow.tsx b/client/src/components/windows/album/AlbumWindow.tsx index d879bfc..07daa38 100644 --- a/client/src/components/windows/album/AlbumWindow.tsx +++ b/client/src/components/windows/album/AlbumWindow.tsx @@ -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 +} + +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) { { 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 && } {!props.state.songsOnAlbum && } diff --git a/client/src/components/windows/artist/ArtistWindow.tsx b/client/src/components/windows/artist/ArtistWindow.tsx index fd76abb..ce28da5 100644 --- a/client/src/components/windows/artist/ArtistWindow.tsx +++ b/client/src/components/windows/artist/ArtistWindow.tsx @@ -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 +} + +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) { { 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 && } {!props.state.songsByArtist && } diff --git a/client/src/components/windows/manage_tags/ManageTagMenu.tsx b/client/src/components/windows/manage_tags/ManageTagMenu.tsx index 5b5f9a1..376f340 100644 --- a/client/src/components/windows/manage_tags/ManageTagMenu.tsx +++ b/client/src/components/windows/manage_tags/ManageTagMenu.tsx @@ -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: { { props.onClose(); - props.onOpenInTab(); + props.onOpenTag(); }} >Browse 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); - const [expanded, setExpanded] = useState(false); + const [expanded, setExpanded] = useState(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} - {hasChildren && expanded && tag.children.map((child: any) => , - /]} - 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) => , + /]} + dispatch={props.dispatch} + state={props.state} + changedTags={props.changedTags} + />)} { - props.mainDispatch({ - type: MainWindowStateActions.AddTab, - tabState: { - tabLabel: <>{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, 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 +} + +export function ManageTagsWindowControlled(props: { state: ManageTagsWindowState, dispatch: (action: any) => void, - mainDispatch: (action: any) => void, }) { const [newTagMenuPos, setNewTagMenuPos] = React.useState(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 ; - })} + {tags && tags.length && tags + .sort((a: any, b: any) => a.name.localeCompare(b.name)) + .map((tag: any) => { + return ; + })} { onOpenNewTagMenu(e) }} /> diff --git a/client/src/components/windows/query/QueryWindow.tsx b/client/src/components/windows/query/QueryWindow.tsx index b410a9d..9561883 100644 --- a/client/src/components/windows/query/QueryWindow.tsx +++ b/client/src/components/windows/query/QueryWindow.tsx @@ -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 } -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) { {loading && } diff --git a/client/src/components/windows/song/SongWindow.tsx b/client/src/components/windows/song/SongWindow.tsx index e7048aa..6016366 100644 --- a/client/src/components/windows/song/SongWindow.tsx +++ b/client/src/components/windows/song/SongWindow.tsx @@ -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 +} + +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) { { setApplying(true); - saveSongChanges(props.state.songId, pendingChanges || {}) + saveSongChanges(props.state.id, pendingChanges || {}) .then(() => { setApplying(false); props.dispatch({ diff --git a/client/src/components/windows/tag/TagWindow.tsx b/client/src/components/windows/tag/TagWindow.tsx index ec33f91..fd092ee 100644 --- a/client/src/components/windows/tag/TagWindow.tsx +++ b/client/src/components/windows/tag/TagWindow.tsx @@ -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 +} + +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) { { 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 && } {!props.state.songsWithTag && }