Add ordering, output types

pull/7/head
Sander Vocke 5 years ago
parent 2bf21aa28f
commit fa64c9f8b4
  1. 64
      client/src/App.tsx
  2. 11
      client/src/api.ts
  3. 143
      client/src/components/QueryBrowseWindow.tsx
  4. 45
      client/src/types/Query.tsx
  5. 10
      server/endpoints/QueryEndpointHandler.ts

@ -1,11 +1,13 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import AppBar, { ActiveTab as AppBarActiveTab } from './components/AppBar'; import AppBar, { ActiveTab as AppBarActiveTab } from './components/AppBar';
import { Query, isQuery, QueryKeys } from './types/Query'; import { Query, isQuery, QueryKeys, QueryOrdering, OrderKey, TypesIncluded, isTypesIncluded, isQueryOrdering } from './types/Query';
import QueryBrowseWindow, { TypesIncluded } from './components/QueryBrowseWindow'; import QueryBrowseWindow from './components/QueryBrowseWindow';
import { DndProvider } from 'react-dnd'; import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend'; import { HTML5Backend } from 'react-dnd-html5-backend';
import * as serverApi from './api';
import { import {
BrowserRouter as Router, BrowserRouter as Router,
Switch, Switch,
@ -27,22 +29,50 @@ function fixQuery(q: any): Query {
return q; 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() { function AppBody() {
const history = useHistory(); const history = useHistory();
const location = useLocation(); const location = useLocation();
const queryParams = new URLSearchParams(location.search); const queryParams = new URLSearchParams(location.search);
const [types, setTypes] = useState<TypesIncluded>({ 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 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 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 limit: number | undefined = queryParams.get('limit') ? parseInt(queryParams.get('limit') || '0') : undefined;
const pushQuery = (q: Query) => { const pushQuery = (
const newParams = new URLSearchParams(); //TODO this throws away all other stuff q: Query,
o: QueryOrdering,
t: TypesIncluded
) => {
const newParams = new URLSearchParams(location.search);
newParams.set('query', JSURL.stringify(q)); newParams.set('query', JSURL.stringify(q));
newParams.set('order', JSURL.stringify(o));
newParams.set('types', JSURL.stringify(t));
history.push({ history.push({
search: "?" + newParams.toString() search: "?" + newParams.toString()
}) })
@ -50,8 +80,10 @@ function AppBody() {
useEffect(() => { useEffect(() => {
const fq = fixQuery(itemQuery); const fq = fixQuery(itemQuery);
if (fq != itemQuery) { const fo = fixOrder(itemOrder);
pushQuery(fq); const ft = fixTypes(itemTypes);
if (fq != itemQuery || fo != itemOrder || ft != itemTypes) {
pushQuery(fq, fo, ft);
return; return;
} }
}, [location]); }, [location]);
@ -66,7 +98,13 @@ function AppBody() {
} }
const onQueryChange = (q: Query) => { 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 ( return (
@ -77,9 +115,11 @@ function AppBody() {
<AppBar activeTab={AppBarActiveTab.Query} onActiveTabChange={onAppBarTabChange} /> <AppBar activeTab={AppBarActiveTab.Query} onActiveTabChange={onAppBarTabChange} />
<QueryBrowseWindow <QueryBrowseWindow
query={itemQuery} query={itemQuery}
typesIncluded={types} typesIncluded={itemTypes}
resultOrder={itemOrder}
onQueryChange={onQueryChange} onQueryChange={onQueryChange}
onTypesChange={setTypes} onTypesChange={onTypesChange}
onOrderChange={onOrderChange}
/> />
</Route> </Route>
</Switch> </Switch>

@ -72,8 +72,10 @@ export enum QueryElemProperty {
artistName = "artistName", artistName = "artistName",
albumName = "albumName", albumName = "albumName",
} }
export enum OrderBy { export enum OrderByType {
Name = 0 Name = 0,
ArtistRanking,
TagRanking
} }
export interface QueryElem { export interface QueryElem {
prop?: QueryElemProperty, prop?: QueryElemProperty,
@ -83,7 +85,10 @@ export interface QueryElem {
childrenOperator?: QueryElemOp, childrenOperator?: QueryElemOp,
} }
export interface Ordering { export interface Ordering {
orderBy: OrderBy, orderBy: {
type: OrderByType,
itemId?: number,
}
ascending: boolean, ascending: boolean,
} }
export interface Query extends QueryElem { } export interface Query extends QueryElem { }

@ -1,19 +1,13 @@
import React, { useState, useEffect } from 'react'; 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 FilterControl from './FilterControl';
import * as serverApi from '../api'; import * as serverApi from '../api';
import BrowseWindow, { Item } from './BrowseWindow'; 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'); const _ = require('lodash');
export interface TypesIncluded {
songs: boolean,
artists: boolean,
tags: boolean,
}
interface ItemTypeCheckboxesProps { interface ItemTypeCheckboxesProps {
types: TypesIncluded, types: TypesIncluded,
onChange: (types: TypesIncluded) => void; onChange: (types: TypesIncluded) => void;
@ -22,23 +16,23 @@ interface ItemTypeCheckboxesProps {
function ItemTypeCheckboxes(props: ItemTypeCheckboxesProps) { function ItemTypeCheckboxes(props: ItemTypeCheckboxesProps) {
const songChange = (v: any) => { const songChange = (v: any) => {
props.onChange({ props.onChange({
songs: v.target.checked, [QueryKeys.Songs]: v.target.checked,
artists: props.types.artists, [QueryKeys.Artists]: props.types[QueryKeys.Artists],
tags: props.types.tags [QueryKeys.Tags]: props.types[QueryKeys.Tags]
}); });
} }
const artistChange = (v: any) => { const artistChange = (v: any) => {
props.onChange({ props.onChange({
songs: props.types.songs, [QueryKeys.Songs]: props.types[QueryKeys.Songs],
artists: v.target.checked, [QueryKeys.Artists]: v.target.checked,
tags: props.types.tags [QueryKeys.Tags]: props.types[QueryKeys.Tags]
}); });
} }
const tagChange = (v: any) => { const tagChange = (v: any) => {
props.onChange({ props.onChange({
songs: props.types.songs, [QueryKeys.Songs]: props.types[QueryKeys.Songs],
artists: props.types.artists, [QueryKeys.Artists]: props.types[QueryKeys.Artists],
tags: v.target.checked [QueryKeys.Tags]: v.target.checked
}); });
} }
@ -46,26 +40,91 @@ function ItemTypeCheckboxes(props: ItemTypeCheckboxesProps) {
<FormLabel component='legend'>Result types</FormLabel> <FormLabel component='legend'>Result types</FormLabel>
<FormGroup> <FormGroup>
<FormControlLabel <FormControlLabel
control={<Checkbox checked={props.types.songs} onChange={songChange} name='Songs' />} control={<Checkbox checked={props.types[QueryKeys.Songs]} onChange={songChange} name='Songs' />}
label="Songs" label="Songs"
/> />
<FormControlLabel <FormControlLabel
control={<Checkbox checked={props.types.artists} onChange={artistChange} name='Artists' />} control={<Checkbox checked={props.types[QueryKeys.Artists]} onChange={artistChange} name='Artists' />}
label="Artists" label="Artists"
/> />
<FormControlLabel <FormControlLabel
control={<Checkbox checked={props.types.tags} onChange={tagChange} name='Tags' />} control={<Checkbox checked={props.types[QueryKeys.Tags]} onChange={tagChange} name='Tags' />}
label="Tags" label="Tags"
/> />
</FormGroup> </FormGroup>
</FormControl>; </FormControl>;
} }
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 <FormControl component='fieldset'>
<FormLabel component='legend'>Ordering</FormLabel>
<FormGroup>
<Select
onChange={onTypeChange}
value={props.ordering[QueryKeys.OrderBy][QueryKeys.OrderKey]}
>
<MenuItem value={OrderKey.Name}>Name</MenuItem>
</Select>
<Select
onChange={onAscendingChange}
value={props.ordering[QueryKeys.Ascending] ? 'asc' : 'desc'}
>
<MenuItem value={'asc'}>Ascending</MenuItem>
<MenuItem value={'desc'}>Descending</MenuItem>
</Select>
</FormGroup>
</FormControl>;
}
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 { export interface IProps {
query: Query | undefined, query: Query | undefined,
typesIncluded: TypesIncluded, typesIncluded: TypesIncluded | undefined,
resultOrder: QueryOrdering | undefined,
onQueryChange: (q: Query) => void, onQueryChange: (q: Query) => void,
onTypesChange: (t: TypesIncluded) => void, onTypesChange: (t: TypesIncluded) => void,
onOrderChange: (o: QueryOrdering) => void,
} }
export default function QueryBrowseWindow(props: IProps) { export default function QueryBrowseWindow(props: IProps) {
@ -74,12 +133,14 @@ export default function QueryBrowseWindow(props: IProps) {
//const [tags, setTags] = useState<serverApi.TagDetails[]>([]); //const [tags, setTags] = useState<serverApi.TagDetails[]>([]);
var items: Item[] = []; var items: Item[] = [];
props.typesIncluded.songs && items.push(...songs); props.typesIncluded && props.typesIncluded[QueryKeys.Songs] && items.push(...songs);
props.typesIncluded.artists && items.push(...artists); props.typesIncluded && props.typesIncluded[QueryKeys.Artists] && items.push(...artists);
useEffect(() => { useEffect(() => {
if (!props.query) { return; } if (!props.query) { return; }
const q = _.cloneDeep(props.query); const q = _.cloneDeep(props.query);
const r = _.cloneDeep(props.resultOrder);
const t = _.cloneDeep(props.typesIncluded);
const request: serverApi.QueryRequest = { const request: serverApi.QueryRequest = {
query: toApiQuery(props.query), query: toApiQuery(props.query),
@ -89,10 +150,7 @@ export default function QueryBrowseWindow(props: IProps) {
artistLimit: 5, artistLimit: 5,
tagOffset: 0, tagOffset: 0,
tagLimit: 5, tagLimit: 5,
ordering: { ordering: toServerOrdering(props.resultOrder),
orderBy: serverApi.OrderBy.Name,
ascending: true,
}
} }
const requestOpts = { const requestOpts = {
method: 'POST', method: 'POST',
@ -102,20 +160,37 @@ export default function QueryBrowseWindow(props: IProps) {
fetch(serverApi.QueryEndpoint, requestOpts) fetch(serverApi.QueryEndpoint, requestOpts)
.then((response: any) => response.json()) .then((response: any) => response.json())
.then((json: any) => { .then((json: any) => {
'songs' in json && _.isEqual(q, props.query) && setSongs(json.songs); const match = _.isEqual(q, props.query) && _.isEqual(r, props.resultOrder) && _.isEqual(t, props.typesIncluded);
'artists' in json && _.isEqual(q, props.query) && setArtists(json.artists); 'songs' in json && match && setSongs(json.songs);
'artists' in json && match && setArtists(json.artists);
}); });
}, [props.query]); }, [props.query]);
return <> return <>
<FilterControl <FormControl component='fieldset'>
query={props.query} <FormLabel component='legend'>Query</FormLabel>
onChangeQuery={props.onQueryChange} <FilterControl
/> query={props.query}
onChangeQuery={props.onQueryChange}
/>
</FormControl>
<ItemTypeCheckboxes <ItemTypeCheckboxes
types={props.typesIncluded} types={props.typesIncluded || {
[QueryKeys.Songs]: true,
[QueryKeys.Artists]: true,
[QueryKeys.Tags]: true,
}}
onChange={props.onTypesChange} onChange={props.onTypesChange}
/> />
<OrderingWidget
ordering={props.resultOrder || {
[QueryKeys.OrderBy]: {
[QueryKeys.OrderKey]: OrderKey.Name
},
[QueryKeys.Ascending]: true
}}
onChange={props.onOrderChange}
/>
<BrowseWindow items={items} /> <BrowseWindow items={items} />
</> </>
} }

@ -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 { export enum QueryKeys {
TitleLike = 'tl', TitleLike = 'tl',
@ -7,6 +8,15 @@ export enum QueryKeys {
OrQuerySignature = 'or', OrQuerySignature = 'or',
OperandA = 'a', OperandA = 'a',
OperandB = 'b', OperandB = 'b',
Name = 'n',
ArtistRanking = 'an',
TagRanking = 'tn',
Songs = 's',
Artists = 'at',
Tags = 't',
OrderBy = 'ob',
OrderKey = 'ok',
Ascending = 'asc'
} }
export interface TitleQuery { export interface TitleQuery {
@ -75,9 +85,40 @@ export function OrToApiQuery(q: OrQuery<Query>) {
export type Query = TitleQuery | ArtistQuery | AndQuery<Query> | OrQuery<Query>; export type Query = TitleQuery | ArtistQuery | AndQuery<Query> | OrQuery<Query>;
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 { export function isQuery(q: any): q is Query {
return q != null && 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 { export function toApiQuery(q: Query): any {

@ -42,13 +42,15 @@ const sequelizeProps: any = {
const sequelizeOrderColumns: any = { const sequelizeOrderColumns: any = {
[QueryType.Song]: { [QueryType.Song]: {
[api.OrderBy.Name]: 'title' [api.OrderByType.Name]: 'title',
[api.OrderByType.ArtistRanking]: '$Rankings.rank$',
[api.OrderByType.TagRanking]: '$Rankings.rank$',
}, },
[QueryType.Artist]: { [QueryType.Artist]: {
[api.OrderBy.Name]: 'name' [api.OrderByType.Name]: 'name'
}, },
[QueryType.Tag]: { [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'; const ascstring = order.ascending ? 'ASC' : 'DESC';
return [ return [
[ sequelizeOrderColumns[type][order.orderBy], ascstring ] [ sequelizeOrderColumns[type][order.orderBy.type], ascstring ]
]; ];
} }

Loading…
Cancel
Save