Add ordering.

pull/7/head
Sander Vocke 5 years ago
parent 98a01bb3ca
commit 2bf21aa28f
  1. 8
      client/src/App.tsx
  2. 20
      client/src/api.ts
  3. 35
      client/src/components/BrowseWindow.tsx
  4. 19
      client/src/components/DraggableItemListItem.tsx
  5. 4
      client/src/components/ItemList.tsx
  6. 13
      client/src/components/QueryBrowseWindow.tsx
  7. 3
      client/src/types/DragTypes.tsx
  8. 4
      package.json
  9. 23
      server/endpoints/QueryEndpointHandler.ts
  10. 116
      yarn.lock

@ -3,6 +3,8 @@ 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 { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import {
BrowserRouter as Router,
@ -30,7 +32,7 @@ function AppBody() {
const location = useLocation();
const queryParams = new URLSearchParams(location.search);
const [ types, setTypes ] = useState<TypesIncluded>({songs: true, artists: true, tags: true});
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);
@ -88,7 +90,9 @@ function AppBody() {
function App() {
return (
<Router>
<AppBody />
<DndProvider backend={HTML5Backend}>
<AppBody />
</DndProvider>
</Router>
);
}

@ -19,12 +19,18 @@ export interface ArtistDetails {
name: String,
storeLinks?: String[],
}
export function isArtistDetails(q: any): q is ArtistDetails {
return 'artistId' in q;
}
export interface TagDetails {
tagId: Number,
name: String,
parent?: TagDetails,
storeLinks?: String[],
}
export function isTagDetails(q: any): q is TagDetails {
return 'tagId' in q;
}
export interface RankingDetails {
rankingId: Number,
type: ItemType, // The item type being ranked
@ -32,6 +38,9 @@ export interface RankingDetails {
context: ArtistDetails | TagDetails,
value: Number, // The ranking (higher = better)
}
export function isRankingDetails(q: any): q is RankingDetails {
return 'rankingId' in q;
}
export interface SongDetails {
songId: Number,
title: String,
@ -40,6 +49,9 @@ export interface SongDetails {
storeLinks?: String[],
rankings?: RankingDetails[],
}
export function isSongDetails(q: any): q is SongDetails {
return 'songId' in q;
}
// Query for items (POST).
export const QueryEndpoint = '/query';
@ -60,6 +72,9 @@ export enum QueryElemProperty {
artistName = "artistName",
albumName = "albumName",
}
export enum OrderBy {
Name = 0
}
export interface QueryElem {
prop?: QueryElemProperty,
propOperand?: any,
@ -67,6 +82,10 @@ export interface QueryElem {
children?: QueryElem[]
childrenOperator?: QueryElemOp,
}
export interface Ordering {
orderBy: OrderBy,
ascending: boolean,
}
export interface Query extends QueryElem { }
export interface QueryRequest {
query: Query,
@ -76,6 +95,7 @@ export interface QueryRequest {
artistLimit: number,
tagOffset: number,
tagLimit: number,
ordering: Ordering,
}
export interface QueryResponse {
songs: SongDetails[],

@ -2,37 +2,14 @@ import React from 'react';
import { Paper } from '@material-ui/core';
import { DisplayItem } from '../types/DisplayItem';
import ItemListItem from './ItemListItem';
import DraggableItemListItem from './DraggableItemListItem';
import ItemList from './ItemList';
import * as serverApi from '../api';
import StoreIcon from '@material-ui/icons/Store';
import { ReactComponent as GooglePlayIcon } from '../assets/googleplaymusic_icon.svg';
export interface SongItem extends serverApi.SongDetails {
songSignature: any
}
export function isSongItem(q: any): q is SongItem {
return 'songSignature' in q;
}
export function toSongItem(i: serverApi.SongDetails) {
const r: any = i;
r['songSignature'] = true;
return r;
}
export interface ArtistItem extends serverApi.ArtistDetails {
artistSignature: any
}
export function isArtistItem(q: any): q is ArtistItem {
return 'artistSignature' in q;
}
export function toArtistItem(i: serverApi.ArtistDetails) {
const r: any = i;
r['artistSignature'] = true;
return r;
}
type SongItem = serverApi.SongDetails;
type ArtistItem = serverApi.ArtistDetails;
export type Item = SongItem | ArtistItem;
const getStoreIcon = (url: String) => {
@ -43,7 +20,7 @@ const getStoreIcon = (url: String) => {
}
function toDisplayItem(item: Item): DisplayItem | undefined {
if (isSongItem(item)) {
if (serverApi.isSongDetails(item)) {
return {
title: item.title,
artistNames: item.artists && item.artists.map((artist: serverApi.ArtistDetails) => {
@ -59,7 +36,7 @@ function toDisplayItem(item: Item): DisplayItem | undefined {
}
}) || [],
}
} else if (isArtistItem(item)) {
} else if (serverApi.isArtistDetails(item)) {
return {
name: item.name ? item.name : "Unknown",
tagNames: [], // TODO
@ -84,7 +61,7 @@ export default function BrowseWindow(props: IProps) {
<ItemList>
{props.items.map((item: Item) => {
const di = toDisplayItem(item);
return di && <ItemListItem item={di} />;
return di && <DraggableItemListItem item={di} />;
})}
</ItemList>
</Paper>;

@ -0,0 +1,19 @@
import React from 'react';
import ItemListItem from './ItemListItem';
import { useDrag } from 'react-dnd';
import { dragTypes } from '../types/DragTypes';
export default function DraggableItemListItem(props: any) {
const [{ isDragging: boolean }, drag] = useDrag({
item: { type: dragTypes.ListItem },
collect: (monitor: any) => ({
isDragging: !!monitor.isDragging(),
}),
});
return <div
ref={drag}
>
<ItemListItem {...props} />
</div>;
}

@ -17,9 +17,7 @@ export default function ItemList(props:any) {
return (
<div className={classes.root}>
<List dense={true}>
{props.children.map((child: any) => {
return child;
})}
{props.children}
</List>
</div>
);

@ -3,7 +3,7 @@ import React, { useState, useEffect } from 'react';
import { Query, toApiQuery } from '../types/Query';
import FilterControl from './FilterControl';
import * as serverApi from '../api';
import BrowseWindow, { toSongItem, toArtistItem, Item } from './BrowseWindow';
import BrowseWindow, { Item } from './BrowseWindow';
import { FormControl, FormLabel, FormGroup, FormControlLabel, Checkbox } from '@material-ui/core';
const _ = require('lodash');
@ -73,12 +73,9 @@ export default function QueryBrowseWindow(props: IProps) {
const [artists, setArtists] = useState<serverApi.ArtistDetails[]>([]);
//const [tags, setTags] = useState<serverApi.TagDetails[]>([]);
const songItems: Item[] = songs.map(toSongItem);
const artistItems: Item[] = artists.map(toArtistItem);
var items: Item[] = [];
props.typesIncluded.songs && items.push(...songItems);
props.typesIncluded.artists && items.push(...artistItems);
props.typesIncluded.songs && items.push(...songs);
props.typesIncluded.artists && items.push(...artists);
useEffect(() => {
if (!props.query) { return; }
@ -92,6 +89,10 @@ export default function QueryBrowseWindow(props: IProps) {
artistLimit: 5,
tagOffset: 0,
tagLimit: 5,
ordering: {
orderBy: serverApi.OrderBy.Name,
ascending: true,
}
}
const requestOpts = {
method: 'POST',

@ -0,0 +1,3 @@
export const dragTypes = {
ListItem: 'list item'
}

@ -8,5 +8,9 @@
},
"devDependencies": {
"concurrently": "^4.0.1"
},
"dependencies": {
"react-dnd": "^11.1.3",
"react-dnd-html5-backend": "^11.1.3"
}
}

@ -40,6 +40,18 @@ const sequelizeProps: any = {
}
};
const sequelizeOrderColumns: any = {
[QueryType.Song]: {
[api.OrderBy.Name]: 'title'
},
[QueryType.Artist]: {
[api.OrderBy.Name]: 'name'
},
[QueryType.Tag]: {
[api.OrderBy.Name]: 'name'
},
}
// Returns the "where" clauses for Sequelize, per object type.
const getSequelizeWhere = (queryElem: api.QueryElem, type: QueryType) => {
var where: any = {
@ -66,6 +78,14 @@ const getSequelizeWhere = (queryElem: api.QueryElem, type: QueryType) => {
return where;
}
function getSequelizeOrder(order: api.Ordering, type: QueryType) {
const ascstring = order.ascending ? 'ASC' : 'DESC';
return [
[ sequelizeOrderColumns[type][order.orderBy], ascstring ]
];
}
export const QueryEndpointHandler: EndpointHandler = async (req: any, res: any) => {
if (!api.checkQueryRequest(req.body)) {
const e: EndpointError = {
@ -81,6 +101,7 @@ export const QueryEndpointHandler: EndpointHandler = async (req: any, res: any)
// NOTE: have to disable limit and offset because of bug: https://github.com/sequelize/sequelize/issues/11938.
// Custom pagination is implemented before responding.
where: getSequelizeWhere(reqObject.query, QueryType.Song),
order: getSequelizeOrder(reqObject.ordering, QueryType.Song),
include: [models.Artist, models.Album, models.Tag, models.Ranking],
//limit: reqObject.limit,
//offset: reqObject.offset,
@ -89,6 +110,7 @@ export const QueryEndpointHandler: EndpointHandler = async (req: any, res: any)
// NOTE: have to disable limit and offset because of bug: https://github.com/sequelize/sequelize/issues/11938.
// Custom pagination is implemented before responding.
where: getSequelizeWhere(reqObject.query, QueryType.Artist),
order: getSequelizeOrder(reqObject.ordering, QueryType.Artist),
include: [models.Song, models.Album, models.Tag],
//limit: reqObject.limit,
//offset: reqObject.offset,
@ -97,6 +119,7 @@ export const QueryEndpointHandler: EndpointHandler = async (req: any, res: any)
// NOTE: have to disable limit and offset because of bug: https://github.com/sequelize/sequelize/issues/11938.
// Custom pagination is implemented before responding.
where: getSequelizeWhere(reqObject.query, QueryType.Tag),
order: getSequelizeOrder(reqObject.ordering, QueryType.Tag),
include: [models.Song, models.Album, models.Artist],
//limit: reqObject.limit,
//offset: reqObject.offset,

@ -2,6 +2,42 @@
# yarn lockfile v1
"@react-dnd/asap@^4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/@react-dnd/asap/-/asap-4.0.0.tgz#b300eeed83e9801f51bd66b0337c9a6f04548651"
integrity sha512-0XhqJSc6pPoNnf8DhdsPHtUhRzZALVzYMTzRwV4VI6DJNJ/5xxfL9OQUwb8IH5/2x7lSf7nAZrnzUD+16VyOVQ==
"@react-dnd/invariant@^2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@react-dnd/invariant/-/invariant-2.0.0.tgz#09d2e81cd39e0e767d7da62df9325860f24e517e"
integrity sha512-xL4RCQBCBDJ+GRwKTFhGUW8GXa4yoDfJrPbLblc3U09ciS+9ZJXJ3Qrcs/x2IODOdIE5kQxvMmE2UKyqUictUw==
"@react-dnd/shallowequal@^2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@react-dnd/shallowequal/-/shallowequal-2.0.0.tgz#a3031eb54129f2c66b2753f8404266ec7bf67f0a"
integrity sha512-Pc/AFTdwZwEKJxFJvlxrSmGe/di+aAOBn60sremrpLo6VI/6cmiUYNNwlI5KNYttg7uypzA3ILPMPgxB2GYZEg==
"@types/hoist-non-react-statics@^3.3.1":
version "3.3.1"
resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f"
integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==
dependencies:
"@types/react" "*"
hoist-non-react-statics "^3.3.0"
"@types/prop-types@*":
version "15.7.3"
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7"
integrity sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==
"@types/react@*":
version "16.9.44"
resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.44.tgz#da84b179c031aef67dc92c33bd3401f1da2fa3bc"
integrity sha512-BtLoJrXdW8DVZauKP+bY4Kmiq7ubcJq+H/aCpRfvPF7RAT3RwR73Sg8szdc2YasbAlWBDrQ6Q+AFM0KwtQY+WQ==
dependencies:
"@types/prop-types" "*"
csstype "^3.0.2"
ansi-regex@^2.0.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
@ -85,6 +121,11 @@ cross-spawn@^6.0.0:
shebang-command "^1.2.0"
which "^1.2.9"
csstype@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.2.tgz#ee5ff8f208c8cd613b389f7b222c9801ca62b3f7"
integrity sha512-ofovWglpqoqbfLNOTBNZLSbMuGrblAf1efvvArGKOZMBrIoJeu5UsAipQolkijtyQx5MtAzT/J9IHj/CEY1mJw==
date-fns@^1.30.1:
version "1.30.1"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.30.1.tgz#2e71bf0b119153dbb4cc4e88d9ea5acfb50dc05c"
@ -95,6 +136,15 @@ decamelize@^1.2.0:
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=
dnd-core@^11.1.3:
version "11.1.3"
resolved "https://registry.yarnpkg.com/dnd-core/-/dnd-core-11.1.3.tgz#f92099ba7245e49729d2433157031a6267afcc98"
integrity sha512-QugF55dNW+h+vzxVJ/LSJeTeUw9MCJ2cllhmVThVPEtF16ooBkxj0WBE5RB+AceFxMFo1rO6bJKXtqKl+JNnyA==
dependencies:
"@react-dnd/asap" "^4.0.0"
"@react-dnd/invariant" "^2.0.0"
redux "^4.0.4"
end-of-stream@^1.1.0:
version "1.4.4"
resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
@ -156,6 +206,13 @@ has-flag@^3.0.0:
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0=
hoist-non-react-statics@^3.3.0:
version "3.3.2"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
dependencies:
react-is "^16.7.0"
hosted-git-info@^2.1.4:
version "2.8.8"
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488"
@ -193,6 +250,11 @@ isexe@^2.0.0:
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=
"js-tokens@^3.0.0 || ^4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
json-parse-better-errors@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"
@ -218,6 +280,13 @@ lodash@^4.17.15:
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
loose-envify@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
dependencies:
js-tokens "^3.0.0 || ^4.0.0"
map-age-cleaner@^0.1.1:
version "0.1.3"
resolved "https://registry.yarnpkg.com/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz#7d583a7306434c055fe474b0f45078e6e1b4b92a"
@ -352,6 +421,28 @@ pump@^3.0.0:
end-of-stream "^1.1.0"
once "^1.3.1"
react-dnd-html5-backend@^11.1.3:
version "11.1.3"
resolved "https://registry.yarnpkg.com/react-dnd-html5-backend/-/react-dnd-html5-backend-11.1.3.tgz#2749f04f416ec230ea193f5c1fbea2de7dffb8f7"
integrity sha512-/1FjNlJbW/ivkUxlxQd7o3trA5DE33QiRZgxent3zKme8DwF4Nbw3OFVhTRFGaYhHFNL1rZt6Rdj1D78BjnNLw==
dependencies:
dnd-core "^11.1.3"
react-dnd@^11.1.3:
version "11.1.3"
resolved "https://registry.yarnpkg.com/react-dnd/-/react-dnd-11.1.3.tgz#f9844f5699ccc55dfc81462c2c19f726e670c1af"
integrity sha512-8rtzzT8iwHgdSC89VktwhqdKKtfXaAyC4wiqp0SywpHG12TTLvfOoL6xNEIUWXwIEWu+CFfDn4GZJyynCEuHIQ==
dependencies:
"@react-dnd/shallowequal" "^2.0.0"
"@types/hoist-non-react-statics" "^3.3.1"
dnd-core "^11.1.3"
hoist-non-react-statics "^3.3.0"
react-is@^16.7.0:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
read-pkg@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-4.0.1.tgz#963625378f3e1c4d48c85872b5a6ec7d5d093237"
@ -361,6 +452,14 @@ read-pkg@^4.0.1:
parse-json "^4.0.0"
pify "^3.0.0"
redux@^4.0.4:
version "4.0.5"
resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.5.tgz#4db5de5816e17891de8a80c424232d06f051d93f"
integrity sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w==
dependencies:
loose-envify "^1.4.0"
symbol-observable "^1.2.0"
require-directory@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
@ -493,6 +592,11 @@ supports-color@^5.3.0:
dependencies:
has-flag "^3.0.0"
symbol-observable@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==
tree-kill@^1.2.1:
version "1.2.2"
resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc"
@ -503,18 +607,6 @@ tslib@^1.9.0:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043"
integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==
typescript-require@^0.2.10:
version "0.2.10"
resolved "https://registry.yarnpkg.com/typescript-require/-/typescript-require-0.2.10.tgz#8c8ee2aa75f3530b560b849c2927cd3697eba68e"
integrity sha1-jI7iqnXzUwtWC4ScKSfNNpfrpo4=
dependencies:
typescript "^1.5.3"
typescript@^1.5.3:
version "1.8.10"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-1.8.10.tgz#b475d6e0dff0bf50f296e5ca6ef9fbb5c7320f1e"
integrity sha1-tHXW4N/wv1DyluXKbvn7tccyDx4=
validate-npm-package-license@^3.0.1:
version "3.0.4"
resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a"

Loading…
Cancel
Save