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 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<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 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() {
<AppBar activeTab={AppBarActiveTab.Query} onActiveTabChange={onAppBarTabChange} />
<QueryBrowseWindow
query={itemQuery}
typesIncluded={types}
typesIncluded={itemTypes}
resultOrder={itemOrder}
onQueryChange={onQueryChange}
onTypesChange={setTypes}
onTypesChange={onTypesChange}
onOrderChange={onOrderChange}
/>
</Route>
</Switch>

@ -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 { }

@ -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) {
<FormLabel component='legend'>Result types</FormLabel>
<FormGroup>
<FormControlLabel
control={<Checkbox checked={props.types.songs} onChange={songChange} name='Songs' />}
control={<Checkbox checked={props.types[QueryKeys.Songs]} onChange={songChange} name='Songs' />}
label="Songs"
/>
<FormControlLabel
control={<Checkbox checked={props.types.artists} onChange={artistChange} name='Artists' />}
control={<Checkbox checked={props.types[QueryKeys.Artists]} onChange={artistChange} name='Artists' />}
label="Artists"
/>
<FormControlLabel
control={<Checkbox checked={props.types.tags} onChange={tagChange} name='Tags' />}
control={<Checkbox checked={props.types[QueryKeys.Tags]} onChange={tagChange} name='Tags' />}
label="Tags"
/>
</FormGroup>
</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 {
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<serverApi.TagDetails[]>([]);
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 <>
<FilterControl
query={props.query}
onChangeQuery={props.onQueryChange}
/>
<FormControl component='fieldset'>
<FormLabel component='legend'>Query</FormLabel>
<FilterControl
query={props.query}
onChangeQuery={props.onQueryChange}
/>
</FormControl>
<ItemTypeCheckboxes
types={props.typesIncluded}
types={props.typesIncluded || {
[QueryKeys.Songs]: true,
[QueryKeys.Artists]: true,
[QueryKeys.Tags]: true,
}}
onChange={props.onTypesChange}
/>
<OrderingWidget
ordering={props.resultOrder || {
[QueryKeys.OrderBy]: {
[QueryKeys.OrderKey]: OrderKey.Name
},
[QueryKeys.Ascending]: true
}}
onChange={props.onOrderChange}
/>
<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 {
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<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 {
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 {

@ -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 ]
];
}

Loading…
Cancel
Save