From 80f35e031b5f40005ea0119e4e73bb540633c5ad Mon Sep 17 00:00:00 2001 From: Sander Vocke Date: Sun, 20 Sep 2020 22:21:33 +0200 Subject: [PATCH 1/7] Laid groundwork for having different types of tabs and windows. --- client/src/components/MainWindow.tsx | 63 ++++++++++++------- client/src/components/appbar/AddTabMenu.tsx | 37 +++++++++++ client/src/components/appbar/AppBar.tsx | 50 ++++++++++----- .../components/querybuilder/QBAddElemMenu.tsx | 2 +- .../components/windows/EditArtistWindow.tsx | 29 +++++++++ client/src/components/windows/QueryWindow.tsx | 2 +- client/src/components/windows/Windows.tsx | 27 ++++++++ 7 files changed, 170 insertions(+), 40 deletions(-) create mode 100644 client/src/components/appbar/AddTabMenu.tsx create mode 100644 client/src/components/windows/EditArtistWindow.tsx create mode 100644 client/src/components/windows/Windows.tsx diff --git a/client/src/components/MainWindow.tsx b/client/src/components/MainWindow.tsx index 6cbdc67..d3c22cd 100644 --- a/client/src/components/MainWindow.tsx +++ b/client/src/components/MainWindow.tsx @@ -1,8 +1,11 @@ import React, { useReducer, useState, Reducer } from 'react'; -import { ThemeProvider, CssBaseline, createMuiTheme } from '@material-ui/core'; +import { ThemeProvider, CssBaseline, createMuiTheme, withWidth } from '@material-ui/core'; import { grey } from '@material-ui/core/colors'; import AppBar from './appbar/AppBar'; import QueryWindow, { QueryWindowReducer, QueryWindowState } from './windows/QueryWindow'; +import { NewTabProps } from './appbar/AddTabMenu'; +import { newWindowState, newWindowReducer, WindowState, WindowType } from './windows/Windows'; +import EditArtistWindow from './windows/EditArtistWindow'; var _ = require('lodash'); const darkTheme = createMuiTheme({ @@ -18,6 +21,7 @@ export interface MainWindowState { tabLabels: string[], tabStates: any[], tabReducers: Reducer[], + tabTypes: WindowType[], activeTab: number, } @@ -39,7 +43,8 @@ export function MainWindowReducer(state: MainWindowState, action: any) { tabStates: state.tabStates.filter((i: any, idx: number) => idx != action.idx), tabLabels: state.tabLabels.filter((i: any, idx: number) => idx != action.idx), tabReducers: state.tabReducers.filter((i: any, idx: number) => idx != action.idx), - activeTab: state.activeTab >= (newSize-1) ? (newSize-1) : state.activeTab, + tabTypes: state.tabTypes.filter((i: any, idx: number) => idx != action.idx), + activeTab: state.activeTab >= (newSize - 1) ? (newSize - 1) : state.activeTab, } case MainWindowStateActions.AddTab: return { @@ -47,6 +52,7 @@ export function MainWindowReducer(state: MainWindowState, action: any) { tabStates: [...state.tabStates, action.tabState], tabLabels: [...state.tabLabels, action.tabLabel], tabReducers: [...state.tabReducers, action.tabReducer], + tabTypes: [...state.tabTypes, action.tabType], } case MainWindowStateActions.DispatchToTab: return { @@ -72,21 +78,35 @@ export default function MainWindow(props: any) { resultsForQuery: null, }, ], - tabReducers: [QueryWindowReducer, QueryWindowReducer], + tabReducers: [QueryWindowReducer], + tabTypes: [WindowType.Query], activeTab: 0 }) - const queryWindows = state.tabStates.map((state: QueryWindowState, i: number) => { - return { - dispatch({ - type: MainWindowStateActions.DispatchToTab, - tabAction: action, - idx: i - }); - }} - /> + const windows = state.tabStates.map((tabState: any, i: number) => { + const tabDispatch = (action: any) => { + dispatch({ + type: MainWindowStateActions.DispatchToTab, + tabAction: action, + idx: i + }); + } + console.log("state", state); + + switch (state.tabTypes[i]) { + case WindowType.Query: + return + case WindowType.EditArtist: + return + default: + throw new Error("Unimplemented window type"); + } }); return @@ -96,19 +116,16 @@ export default function MainWindow(props: any) { selectedTab={state.activeTab} setSelectedTab={(t: number) => dispatch({ type: MainWindowStateActions.SetActiveTab, value: t })} onCloseTab={(t: number) => dispatch({ type: MainWindowStateActions.CloseTab, idx: t })} - onAddTab={() => { + onAddTab={(w: NewTabProps) => { dispatch({ type: MainWindowStateActions.AddTab, - tabState: { - editingQuery: false, - query: null, - resultsForQuery: null, - }, - tabLabel: "Query", - tabReducer: QueryWindowReducer, + tabState: newWindowState[w.windowType](), + tabLabel: w.title, + tabReducer: newWindowReducer[w.windowType], + tabType: w.windowType, }) }} /> - {queryWindows[state.activeTab]} + {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 new file mode 100644 index 0000000..fb79682 --- /dev/null +++ b/client/src/components/appbar/AddTabMenu.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import { WindowType } from '../windows/Windows'; +import { Menu, MenuItem } from '@material-ui/core'; + +export interface NewTabProps { + title: string, + windowType: WindowType, +} + +export interface IProps { + anchorEl: null | HTMLElement, + onClose: () => void, + onCreateTab: (q: NewTabProps) => void, +} + +export default function AddTabMenu(props: IProps) { + return + New Tab + {([ + WindowType.Query, + WindowType.EditArtist + ]).map((v: WindowType) => { + props.onClose(); + props.onCreateTab({ + title: v, + windowType: v, + }) + }} + >{v})} + +} \ No newline at end of file diff --git a/client/src/components/appbar/AppBar.tsx b/client/src/components/appbar/AppBar.tsx index 0679f92..35839b6 100644 --- a/client/src/components/appbar/AppBar.tsx +++ b/client/src/components/appbar/AppBar.tsx @@ -2,13 +2,14 @@ import React, { useState } from 'react'; import { AppBar as MuiAppBar, Box, Tab as MuiTab, Tabs, IconButton } 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: () => void, + onAddTab: (w: NewTabProps) => void, } export interface TabProps { @@ -45,19 +46,38 @@ export function Tab(props: any) { } export default function AppBar(props: IProps) { - return - - - error + const [addMenuAnchorEl, setAddMenuAnchorEl] = React.useState(null); + + const onOpenAddMenu = (event: any) => { + setAddMenuAnchorEl(event.currentTarget); + }; + const onCloseAddMenu = () => { + setAddMenuAnchorEl(null); + }; + const onAddTab = (w: NewTabProps) => { + props.onAddTab(w); + }; + + return <> + + + + error + + props.setSelectedTab(v)}> + {props.tabLabels.map((l: string, idx: number) => props.onCloseTab(idx)} + />)} + + - props.setSelectedTab(v)}> - {props.tabLabels.map((l: string, idx: number) => props.onCloseTab(idx)} - />)} - - - - + + + } \ No newline at end of file diff --git a/client/src/components/querybuilder/QBAddElemMenu.tsx b/client/src/components/querybuilder/QBAddElemMenu.tsx index 6433435..1aa2b1d 100644 --- a/client/src/components/querybuilder/QBAddElemMenu.tsx +++ b/client/src/components/querybuilder/QBAddElemMenu.tsx @@ -3,7 +3,7 @@ import { Menu, MenuItem } from '@material-ui/core'; import NestedMenuItem from "material-ui-nested-menu-item"; import { QueryElem, QueryLeafBy, QueryLeafOp, TagQueryInfo } from '../../lib/query/Query'; import QBSelectWithRequest from './QBSelectWithRequest'; -import { Requests, TagItem } from './QueryBuilder'; +import { Requests } from './QueryBuilder'; export interface MenuProps { anchorEl: null | HTMLElement, diff --git a/client/src/components/windows/EditArtistWindow.tsx b/client/src/components/windows/EditArtistWindow.tsx new file mode 100644 index 0000000..ba512a4 --- /dev/null +++ b/client/src/components/windows/EditArtistWindow.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { Box } from '@material-ui/core'; + +export interface EditArtistWindowState { + +} + +export enum EditArtistWindowStateActions { +} + +export function EditArtistWindowReducer(state: EditArtistWindowState, action: any) { + return state; +} + +export interface IProps { + state: EditArtistWindowState, + dispatch: (action: any) => void +} + +export default function EditArtistWindow(props: IProps) { + return + + Hello! + + +} \ No newline at end of file diff --git a/client/src/components/windows/QueryWindow.tsx b/client/src/components/windows/QueryWindow.tsx index ff15103..d2f8339 100644 --- a/client/src/components/windows/QueryWindow.tsx +++ b/client/src/components/windows/QueryWindow.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useEffect } from 'react'; import { createMuiTheme, Box, LinearProgress } from '@material-ui/core'; import { QueryElem, toApiQuery } from '../../lib/query/Query'; import QueryBuilder from '../querybuilder/QueryBuilder'; diff --git a/client/src/components/windows/Windows.tsx b/client/src/components/windows/Windows.tsx new file mode 100644 index 0000000..d052ee3 --- /dev/null +++ b/client/src/components/windows/Windows.tsx @@ -0,0 +1,27 @@ +import { QueryWindowReducer, QueryWindowState } from "./QueryWindow"; +import { EditArtistWindowReducer, EditArtistWindowState } from "./EditArtistWindow"; + +export enum WindowType { + Query = "Query", + EditArtist = "EditArtist", +} + +export type WindowState = QueryWindowState | EditArtistWindowState; + +export const newWindowReducer = { + [WindowType.Query]: QueryWindowReducer, + [WindowType.EditArtist]: EditArtistWindowReducer, +} + +export const newWindowState = { + [WindowType.Query]: () => { + return { + editingQuery: false, + query: null, + resultsForQuery: null, + }; + }, + [WindowType.EditArtist]: () => { + + } +} \ No newline at end of file -- 2.36.1 From d4d4c67672dce7e701612e98e7313f3a1af890a6 Mon Sep 17 00:00:00 2001 From: Sander Vocke Date: Sun, 20 Sep 2020 22:42:53 +0200 Subject: [PATCH 2/7] Fetches artist and shows name --- client/src/api.ts | 2 +- client/src/components/MainWindow.tsx | 6 +- client/src/components/appbar/AddTabMenu.tsx | 2 +- .../src/components/windows/ArtistWindow.tsx | 91 +++++++++++++++++++ .../components/windows/EditArtistWindow.tsx | 29 ------ client/src/components/windows/Windows.tsx | 15 +-- server/endpoints/QueryEndpointHandler.ts | 2 + 7 files changed, 107 insertions(+), 40 deletions(-) create mode 100644 client/src/components/windows/ArtistWindow.tsx delete mode 100644 client/src/components/windows/EditArtistWindow.tsx diff --git a/client/src/api.ts b/client/src/api.ts index b162fff..3ab437b 100644 --- a/client/src/api.ts +++ b/client/src/api.ts @@ -130,7 +130,7 @@ export function checkQueryElem(elem: any): boolean { }); } return (elem.childrenOperator && elem.children) || - (elem.prop && elem.propOperand && elem.propOperator) || + ("prop" in elem && "propOperand" in elem && "propOperator" in elem) || Object.keys(elem).length === 0; } export function checkQueryRequest(req: any): boolean { diff --git a/client/src/components/MainWindow.tsx b/client/src/components/MainWindow.tsx index d3c22cd..052a319 100644 --- a/client/src/components/MainWindow.tsx +++ b/client/src/components/MainWindow.tsx @@ -5,7 +5,7 @@ import AppBar from './appbar/AppBar'; import QueryWindow, { QueryWindowReducer, QueryWindowState } from './windows/QueryWindow'; import { NewTabProps } from './appbar/AddTabMenu'; import { newWindowState, newWindowReducer, WindowState, WindowType } from './windows/Windows'; -import EditArtistWindow from './windows/EditArtistWindow'; +import ArtistWindow from './windows/ArtistWindow'; var _ = require('lodash'); const darkTheme = createMuiTheme({ @@ -99,8 +99,8 @@ export default function MainWindow(props: any) { state={tabState} dispatch={tabDispatch} /> - case WindowType.EditArtist: - return diff --git a/client/src/components/appbar/AddTabMenu.tsx b/client/src/components/appbar/AddTabMenu.tsx index fb79682..f2ce996 100644 --- a/client/src/components/appbar/AddTabMenu.tsx +++ b/client/src/components/appbar/AddTabMenu.tsx @@ -23,7 +23,7 @@ export default function AddTabMenu(props: IProps) { New Tab {([ WindowType.Query, - WindowType.EditArtist + WindowType.Artist ]).map((v: WindowType) => { props.onClose(); diff --git a/client/src/components/windows/ArtistWindow.tsx b/client/src/components/windows/ArtistWindow.tsx new file mode 100644 index 0000000..5bb0825 --- /dev/null +++ b/client/src/components/windows/ArtistWindow.tsx @@ -0,0 +1,91 @@ +import React, { useEffect } from 'react'; +import { Box } from '@material-ui/core'; +import * as serverApi from '../../api'; + +export interface ArtistMetadata { + name: string, +} + +export interface ArtistWindowState { + artistId: number, + metadata: ArtistMetadata | null, +} + +export enum ArtistWindowStateActions { + SetMetadata = "SetMetadata", +} + +export function ArtistWindowReducer(state: ArtistWindowState, action: any) { + switch (action.type) { + case ArtistWindowStateActions.SetMetadata: + return { ...state, metadata: action.value } + default: + throw new Error("Unimplemented QueryWindow state update.") + } +} + +export interface IProps { + state: ArtistWindowState, + dispatch: (action: any) => void +} + +export async function getArtistMetadata(id: number) { + const query = { + prop: serverApi.QueryElemProperty.artistId, + propOperand: id, + propOperator: serverApi.QueryFilterOp.Eq, + }; + + var q: serverApi.QueryRequest = { + query: query, + offsetsLimits: { + artistOffset: 0, + artistLimit: 1, + }, + ordering: { + orderBy: { + type: serverApi.OrderByType.Name, + }, + ascending: true, + }, + }; + + const requestOpts = { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(q), + }; + + return (async () => { + const response = await fetch((process.env.REACT_APP_BACKEND || "") + serverApi.QueryEndpoint, requestOpts) + let json: any = await response.json(); + let artist = json.artists[0]; + return { + name: artist.name + } + })(); +} + +export default function ArtistWindow(props: IProps) { + let metadata = props.state.metadata; + + useEffect(() => { + getArtistMetadata(props.state.artistId) + .then((m: ArtistMetadata) => { + console.log("metadata", m); + props.dispatch({ + type: ArtistWindowStateActions.SetMetadata, + value: m + }); + }) + }, []); + + return + + {metadata && metadata.name} + + +} \ No newline at end of file diff --git a/client/src/components/windows/EditArtistWindow.tsx b/client/src/components/windows/EditArtistWindow.tsx deleted file mode 100644 index ba512a4..0000000 --- a/client/src/components/windows/EditArtistWindow.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import React from 'react'; -import { Box } from '@material-ui/core'; - -export interface EditArtistWindowState { - -} - -export enum EditArtistWindowStateActions { -} - -export function EditArtistWindowReducer(state: EditArtistWindowState, action: any) { - return state; -} - -export interface IProps { - state: EditArtistWindowState, - dispatch: (action: any) => void -} - -export default function EditArtistWindow(props: IProps) { - return - - Hello! - - -} \ No newline at end of file diff --git a/client/src/components/windows/Windows.tsx b/client/src/components/windows/Windows.tsx index d052ee3..a9a48c1 100644 --- a/client/src/components/windows/Windows.tsx +++ b/client/src/components/windows/Windows.tsx @@ -1,16 +1,16 @@ import { QueryWindowReducer, QueryWindowState } from "./QueryWindow"; -import { EditArtistWindowReducer, EditArtistWindowState } from "./EditArtistWindow"; +import { ArtistWindowReducer, ArtistWindowState } from "./ArtistWindow"; export enum WindowType { Query = "Query", - EditArtist = "EditArtist", + Artist = "Artist", } -export type WindowState = QueryWindowState | EditArtistWindowState; +export type WindowState = QueryWindowState | ArtistWindowState; export const newWindowReducer = { [WindowType.Query]: QueryWindowReducer, - [WindowType.EditArtist]: EditArtistWindowReducer, + [WindowType.Artist]: ArtistWindowReducer, } export const newWindowState = { @@ -21,7 +21,10 @@ export const newWindowState = { resultsForQuery: null, }; }, - [WindowType.EditArtist]: () => { - + [WindowType.Artist]: () => { + return { + artistId: 1, + metadata: null, + } } } \ No newline at end of file diff --git a/server/endpoints/QueryEndpointHandler.ts b/server/endpoints/QueryEndpointHandler.ts index 0aed145..e0ca7ec 100644 --- a/server/endpoints/QueryEndpointHandler.ts +++ b/server/endpoints/QueryEndpointHandler.ts @@ -377,6 +377,8 @@ export const QueryEndpointHandler: EndpointHandler = async (req: any, res: any, }), } + console.log("Query repsonse", response); + res.send(response); } catch (e) { catchUnhandledErrors(e); -- 2.36.1 From e02c99954a85038e364a96a15ff1bb9030548cff Mon Sep 17 00:00:00 2001 From: Sander Vocke Date: Sun, 20 Sep 2020 23:07:18 +0200 Subject: [PATCH 3/7] Clicking on artist in results now opens its tab. --- client/src/components/MainWindow.tsx | 3 ++ client/src/components/tables/ResultsTable.tsx | 36 ++++++++++++++----- .../src/components/windows/ArtistWindow.tsx | 15 ++++++-- client/src/components/windows/QueryWindow.tsx | 11 +++--- 4 files changed, 50 insertions(+), 15 deletions(-) diff --git a/client/src/components/MainWindow.tsx b/client/src/components/MainWindow.tsx index 052a319..8983e3f 100644 --- a/client/src/components/MainWindow.tsx +++ b/client/src/components/MainWindow.tsx @@ -47,6 +47,7 @@ export function MainWindowReducer(state: MainWindowState, action: any) { activeTab: state.activeTab >= (newSize - 1) ? (newSize - 1) : state.activeTab, } case MainWindowStateActions.AddTab: + console.log("Add tab: ", action) return { ...state, tabStates: [...state.tabStates, action.tabState], @@ -98,11 +99,13 @@ export default function MainWindow(props: any) { return case WindowType.Artist: return default: throw new Error("Unimplemented window type"); diff --git a/client/src/components/tables/ResultsTable.tsx b/client/src/components/tables/ResultsTable.tsx index 3c6fb5e..81d8ea6 100644 --- a/client/src/components/tables/ResultsTable.tsx +++ b/client/src/components/tables/ResultsTable.tsx @@ -1,17 +1,21 @@ 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'; export interface SongGetters { getTitle: (song: any) => string, - getArtist: (song: any) => string, - getAlbum: (song: any) => string, - getTags: (song: any) => string[][], // Each tag is represented as a series of strings. + getArtistNames: (song: any) => string[], + getArtistIds: (song: any) => number[], + getAlbumNames: (song: any) => string[], + getTagNames: (song: any) => string[][], // Each tag is represented as a series of strings. } export interface IProps { songs: any[], songGetters: SongGetters, + mainDispatch: (action: any) => void, } export function SongTable(props: IProps) { @@ -36,9 +40,11 @@ export function SongTable(props: IProps) { {props.songs.map((song: any) => { const title = props.songGetters.getTitle(song); - const artist = props.songGetters.getArtist(song); - const album = props.songGetters.getAlbum(song); - const tags = props.songGetters.getTags(song).map((tag: string[]) => { + // TODO / FIXME: display artists and albums separately! + const artist = stringifyList(props.songGetters.getArtistNames(song)); + const mainArtistId = props.songGetters.getArtistIds(song)[0]; + const album = stringifyList(props.songGetters.getAlbumNames(song)); + const tags = props.songGetters.getTagNames(song).map((tag: string[]) => { return { return (idx === 0) ? e : " / " + e; @@ -46,6 +52,20 @@ export function SongTable(props: IProps) { }); + const onClickArtist = () => { + console.log("onClick!") + props.mainDispatch({ + type: MainWindowStateActions.AddTab, + tabState: { + artistId: mainArtistId, + metadata: null, + }, + tabLabel: "Artist " + mainArtistId, + tabReducer: newWindowReducer[WindowType.Artist], + tabType: WindowType.Artist, + }) + } + const TextCell = (props: any) => { const classes = makeStyles({ button: { @@ -56,7 +76,7 @@ export function SongTable(props: IProps) { } })(); return -