diff --git a/client/src/App.tsx b/client/src/App.tsx index eb77a66..a263862 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,11 +1,13 @@ import React, { useState, useEffect } from 'react'; import AppBar, { ActiveTab as AppBarActiveTab } from './components/AppBar'; -import { Query, isQuery, QueryKeys } from './types/Query'; -import QueryBrowseWindow, { TypesIncluded } from './components/QueryBrowseWindow'; +import { Query, isQuery, QueryKeys, QueryOrdering, OrderKey, TypesIncluded, isTypesIncluded, isQueryOrdering } from './types/Query'; +import QueryBrowseWindow from './components/QueryBrowseWindow'; import { DndProvider } from 'react-dnd'; import { HTML5Backend } from 'react-dnd-html5-backend'; +import * as serverApi from './api'; + import { BrowserRouter as Router, Switch, @@ -27,22 +29,50 @@ function fixQuery(q: any): Query { return q; } +function fixOrder(q: any): QueryOrdering { + if (!isQueryOrdering(q)) { + return { + [QueryKeys.OrderBy]: { + [QueryKeys.OrderKey]: OrderKey.Name, + }, + [QueryKeys.Ascending]: true, + }; + } + return q; +} + +function fixTypes(q: any): TypesIncluded { + if (!isTypesIncluded(q)) { + return { + [QueryKeys.Songs]: true, + [QueryKeys.Artists]: false, + [QueryKeys.Tags]: false, + }; + } + return q; +} + function AppBody() { const history = useHistory(); const location = useLocation(); const queryParams = new URLSearchParams(location.search); - const [types, setTypes] = useState({ songs: true, artists: true, tags: true }); - - // If we have an invalid query, change to the default one. const itemQuery: Query | undefined = JSURL.tryParse(queryParams.get('query'), undefined); + const itemOrder: QueryOrdering | undefined = JSURL.tryParse(queryParams.get('order'), undefined); + const itemTypes: TypesIncluded | undefined = JSURL.tryParse(queryParams.get('types'), undefined); const offset: number | undefined = queryParams.get('offset') ? parseInt(queryParams.get('offset') || '0') : undefined; const limit: number | undefined = queryParams.get('limit') ? parseInt(queryParams.get('limit') || '0') : undefined; - const pushQuery = (q: Query) => { - const newParams = new URLSearchParams(); //TODO this throws away all other stuff + const pushQuery = ( + q: Query, + o: QueryOrdering, + t: TypesIncluded + ) => { + const newParams = new URLSearchParams(location.search); newParams.set('query', JSURL.stringify(q)); + newParams.set('order', JSURL.stringify(o)); + newParams.set('types', JSURL.stringify(t)); history.push({ search: "?" + newParams.toString() }) @@ -50,8 +80,10 @@ function AppBody() { useEffect(() => { const fq = fixQuery(itemQuery); - if (fq != itemQuery) { - pushQuery(fq); + const fo = fixOrder(itemOrder); + const ft = fixTypes(itemTypes); + if (fq != itemQuery || fo != itemOrder || ft != itemTypes) { + pushQuery(fq, fo, ft); return; } }, [location]); @@ -66,7 +98,13 @@ function AppBody() { } const onQueryChange = (q: Query) => { - pushQuery(q); + pushQuery(q, fixOrder(itemOrder), fixTypes(itemTypes)); + } + const onOrderChange = (o: QueryOrdering) => { + pushQuery(fixQuery(itemQuery), o, fixTypes(itemTypes)); + } + const onTypesChange = (t: TypesIncluded) => { + pushQuery(fixQuery(itemQuery), fixOrder(itemOrder), t); } return ( @@ -77,9 +115,11 @@ function AppBody() { diff --git a/client/src/api.ts b/client/src/api.ts index d80b82b..915cd80 100644 --- a/client/src/api.ts +++ b/client/src/api.ts @@ -72,8 +72,10 @@ export enum QueryElemProperty { artistName = "artistName", albumName = "albumName", } -export enum OrderBy { - Name = 0 +export enum OrderByType { + Name = 0, + ArtistRanking, + TagRanking } export interface QueryElem { prop?: QueryElemProperty, @@ -83,7 +85,10 @@ export interface QueryElem { childrenOperator?: QueryElemOp, } export interface Ordering { - orderBy: OrderBy, + orderBy: { + type: OrderByType, + itemId?: number, + } ascending: boolean, } export interface Query extends QueryElem { } diff --git a/client/src/components/QueryBrowseWindow.tsx b/client/src/components/QueryBrowseWindow.tsx index 6ebdcc2..cb7d732 100644 --- a/client/src/components/QueryBrowseWindow.tsx +++ b/client/src/components/QueryBrowseWindow.tsx @@ -1,19 +1,13 @@ import React, { useState, useEffect } from 'react'; -import { Query, toApiQuery } from '../types/Query'; +import { Query, toApiQuery, QueryOrdering, TypesIncluded, QueryKeys, OrderKey } from '../types/Query'; import FilterControl from './FilterControl'; import * as serverApi from '../api'; import BrowseWindow, { Item } from './BrowseWindow'; -import { FormControl, FormLabel, FormGroup, FormControlLabel, Checkbox } from '@material-ui/core'; +import { FormControl, FormLabel, FormGroup, FormControlLabel, Checkbox, Select, MenuItem } from '@material-ui/core'; const _ = require('lodash'); -export interface TypesIncluded { - songs: boolean, - artists: boolean, - tags: boolean, -} - interface ItemTypeCheckboxesProps { types: TypesIncluded, onChange: (types: TypesIncluded) => void; @@ -22,23 +16,23 @@ interface ItemTypeCheckboxesProps { function ItemTypeCheckboxes(props: ItemTypeCheckboxesProps) { const songChange = (v: any) => { props.onChange({ - songs: v.target.checked, - artists: props.types.artists, - tags: props.types.tags + [QueryKeys.Songs]: v.target.checked, + [QueryKeys.Artists]: props.types[QueryKeys.Artists], + [QueryKeys.Tags]: props.types[QueryKeys.Tags] }); } const artistChange = (v: any) => { props.onChange({ - songs: props.types.songs, - artists: v.target.checked, - tags: props.types.tags + [QueryKeys.Songs]: props.types[QueryKeys.Songs], + [QueryKeys.Artists]: v.target.checked, + [QueryKeys.Tags]: props.types[QueryKeys.Tags] }); } const tagChange = (v: any) => { props.onChange({ - songs: props.types.songs, - artists: props.types.artists, - tags: v.target.checked + [QueryKeys.Songs]: props.types[QueryKeys.Songs], + [QueryKeys.Artists]: props.types[QueryKeys.Artists], + [QueryKeys.Tags]: v.target.checked }); } @@ -46,26 +40,91 @@ function ItemTypeCheckboxes(props: ItemTypeCheckboxesProps) { Result types } + control={} label="Songs" /> } + control={} label="Artists" /> } + control={} label="Tags" /> ; } +interface OrderingWidgetProps { + ordering: QueryOrdering, + onChange: (o: QueryOrdering) => void; +} + +function OrderingWidget(props: OrderingWidgetProps) { + const onTypeChange = (e: any) => { + props.onChange({ + [QueryKeys.OrderBy]: { + [QueryKeys.OrderKey]: e.target.value, + }, + [QueryKeys.Ascending]: props.ordering[QueryKeys.Ascending], + }); + } + const onAscendingChange = (e: any) => { + props.onChange({ + [QueryKeys.OrderBy]: props.ordering[QueryKeys.OrderBy], + [QueryKeys.Ascending]: (e.target.value == 'asc'), + }); + } + + return + Ordering + + + + + ; +} + +function toServerOrdering(o: QueryOrdering | undefined) : serverApi.Ordering { + if(!o) { + return { + orderBy: { + type: serverApi.OrderByType.Name + }, + ascending: true + }; + } + + const keys = { + [OrderKey.Name]: serverApi.OrderByType.Name, + }; + + return { + orderBy: { + type: keys[o[QueryKeys.OrderBy][QueryKeys.OrderKey]] + }, + ascending: o[QueryKeys.Ascending], + } +} + export interface IProps { query: Query | undefined, - typesIncluded: TypesIncluded, + typesIncluded: TypesIncluded | undefined, + resultOrder: QueryOrdering | undefined, onQueryChange: (q: Query) => void, onTypesChange: (t: TypesIncluded) => void, + onOrderChange: (o: QueryOrdering) => void, } export default function QueryBrowseWindow(props: IProps) { @@ -74,12 +133,14 @@ export default function QueryBrowseWindow(props: IProps) { //const [tags, setTags] = useState([]); var items: Item[] = []; - props.typesIncluded.songs && items.push(...songs); - props.typesIncluded.artists && items.push(...artists); + props.typesIncluded && props.typesIncluded[QueryKeys.Songs] && items.push(...songs); + props.typesIncluded && props.typesIncluded[QueryKeys.Artists] && items.push(...artists); useEffect(() => { if (!props.query) { return; } const q = _.cloneDeep(props.query); + const r = _.cloneDeep(props.resultOrder); + const t = _.cloneDeep(props.typesIncluded); const request: serverApi.QueryRequest = { query: toApiQuery(props.query), @@ -89,10 +150,7 @@ export default function QueryBrowseWindow(props: IProps) { artistLimit: 5, tagOffset: 0, tagLimit: 5, - ordering: { - orderBy: serverApi.OrderBy.Name, - ascending: true, - } + ordering: toServerOrdering(props.resultOrder), } const requestOpts = { method: 'POST', @@ -102,20 +160,37 @@ export default function QueryBrowseWindow(props: IProps) { fetch(serverApi.QueryEndpoint, requestOpts) .then((response: any) => response.json()) .then((json: any) => { - 'songs' in json && _.isEqual(q, props.query) && setSongs(json.songs); - 'artists' in json && _.isEqual(q, props.query) && setArtists(json.artists); + const match = _.isEqual(q, props.query) && _.isEqual(r, props.resultOrder) && _.isEqual(t, props.typesIncluded); + 'songs' in json && match && setSongs(json.songs); + 'artists' in json && match && setArtists(json.artists); }); }, [props.query]); return <> - + + Query + + + } diff --git a/client/src/types/Query.tsx b/client/src/types/Query.tsx index e58c9c7..f4439a9 100644 --- a/client/src/types/Query.tsx +++ b/client/src/types/Query.tsx @@ -1,4 +1,5 @@ -import { QueryElemProperty, QueryFilterOp, QueryElemOp } from '../api'; +import { QueryElemProperty, QueryFilterOp, QueryElemOp, Ordering, OrderByType } from '../api'; +import { ServerStreamResponseOptions } from 'http2'; export enum QueryKeys { TitleLike = 'tl', @@ -7,6 +8,15 @@ export enum QueryKeys { OrQuerySignature = 'or', OperandA = 'a', OperandB = 'b', + Name = 'n', + ArtistRanking = 'an', + TagRanking = 'tn', + Songs = 's', + Artists = 'at', + Tags = 't', + OrderBy = 'ob', + OrderKey = 'ok', + Ascending = 'asc' } export interface TitleQuery { @@ -75,9 +85,40 @@ export function OrToApiQuery(q: OrQuery) { export type Query = TitleQuery | ArtistQuery | AndQuery | OrQuery; +export enum OrderKey { + Name = 'n', +} + +export interface QueryOrdering { + [QueryKeys.OrderBy]: { + [QueryKeys.OrderKey]: OrderKey, + } + [QueryKeys.Ascending]: boolean, +} + +export interface TypesIncluded { + [QueryKeys.Songs]: boolean, + [QueryKeys.Artists]: boolean, + [QueryKeys.Tags]: boolean, +} + export function isQuery(q: any): q is Query { return q != null && - (isTitleQuery(q) || isArtistQuery(q) || isAndQuery(q) || isOrQuery(q)); + (isTitleQuery(q) || isArtistQuery(q) || isAndQuery(q) || isOrQuery(q)); +} + +export function isQueryOrdering(q: any): q is QueryOrdering { + return q != null && + QueryKeys.OrderBy in q && + QueryKeys.OrderKey in q[QueryKeys.OrderBy] && + QueryKeys.Ascending in q; +} + +export function isTypesIncluded(q: any): q is TypesIncluded { + return q != null && + QueryKeys.Songs in q && + QueryKeys.Artists in q && + QueryKeys.Tags in q; } export function toApiQuery(q: Query): any { diff --git a/server/endpoints/QueryEndpointHandler.ts b/server/endpoints/QueryEndpointHandler.ts index 7edb833..c5d406c 100644 --- a/server/endpoints/QueryEndpointHandler.ts +++ b/server/endpoints/QueryEndpointHandler.ts @@ -42,13 +42,15 @@ const sequelizeProps: any = { const sequelizeOrderColumns: any = { [QueryType.Song]: { - [api.OrderBy.Name]: 'title' + [api.OrderByType.Name]: 'title', + [api.OrderByType.ArtistRanking]: '$Rankings.rank$', + [api.OrderByType.TagRanking]: '$Rankings.rank$', }, [QueryType.Artist]: { - [api.OrderBy.Name]: 'name' + [api.OrderByType.Name]: 'name' }, [QueryType.Tag]: { - [api.OrderBy.Name]: 'name' + [api.OrderByType.Name]: 'name' }, } @@ -82,7 +84,7 @@ function getSequelizeOrder(order: api.Ordering, type: QueryType) { const ascstring = order.ascending ? 'ASC' : 'DESC'; return [ - [ sequelizeOrderColumns[type][order.orderBy], ascstring ] + [ sequelizeOrderColumns[type][order.orderBy.type], ascstring ] ]; }