Fix pagination and song / artist search via URL query.

pull/7/head
Sander Vocke 5 years ago
parent 7a137d7012
commit 678228d223
  1. 1
      client/package.json
  2. 83
      client/src/App.tsx
  3. 11
      client/src/api.ts
  4. 10
      client/src/components/FilterControl.tsx
  5. 8
      client/src/types/Query.tsx
  6. 5
      client/yarn.lock
  7. 18
      server/endpoints/QuerySongsEndpointHandler.ts

@ -15,6 +15,7 @@
"@types/react-dom": "^16.9.0",
"@types/react-router": "^5.1.8",
"@types/react-router-dom": "^5.1.5",
"jsurl": "^0.1.5",
"lodash": "^4.17.19",
"material-table": "^1.64.0",
"react": "^16.13.1",

@ -8,7 +8,7 @@ import AppBar, { ActiveTab as AppBarActiveTab } from './components/AppBar';
import ItemList from './components/ItemList';
import ItemListItem from './components/ItemListItem';
import FilterControl from './components/FilterControl';
import { SongQuery, toApiQuery } from './types/Query';
import { SongQuery, toApiQuery, isSongQuery } from './types/Query';
import { SongDisplayItem, ArtistDisplayItem } from './types/DisplayItem';
import { ReactComponent as GooglePlayIcon } from './assets/googleplaymusic_icon.svg';
@ -17,9 +17,11 @@ import {
Switch,
Route,
useHistory,
useLocation,
Redirect
} from "react-router-dom";
import { timeLog } from 'console';
const JSURL = require('jsurl');
interface SongItemProps {
song: serverApi.SongDetails,
@ -46,13 +48,12 @@ function SongItem(props: SongItemProps) {
tagNames: props.song.tags && props.song.tags.map((tag: serverApi.TagDetails) => {
return tag.name;
}) || [],
storeLinks: []
// json.storeLinks.map((url: String) => {
// return {
// icon: getStoreIcon(url),
// url: url
// }
// })
storeLinks: props.song.storeLinks && props.song.storeLinks.map((url: String) => {
return {
icon: getStoreIcon(url),
url: url
}
}) || [],
}
return <ItemListItem item={displayItem} />;
@ -137,18 +138,57 @@ function ArtistList() {
function AppBody() {
const history = useHistory();
const [songQuery, setSongQuery] = useState<SongQuery>({
'titleLike': ''
});
const location = useLocation();
const queryParams = new URLSearchParams(location.search);
// If we have an invalid query, change to the default one.
const songQuery: SongQuery | undefined = JSURL.tryParse(queryParams.get('query'), undefined);
const [songs, setSongs] = useState<serverApi.SongDetails[]>([]);
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;
React.useEffect(() => {
const query = songQuery;
const fixQueryParams = () => {
var fixed = false;
if (!isSongQuery(songQuery)) {
console.log("query");
queryParams.set('query', JSURL.stringify({
'titleLike': ''
}));
fixed = true;
}
if (offset == undefined) {
console.log("offset", offset);
queryParams.set('offset', '0');
fixed = true;
}
if (limit == undefined) {
console.log("limit");
queryParams.set('limit', '20');
fixed = true;
}
console.log("fixed", fixed);
return fixed;
}
const pushQueryParams = () => {
history.push({
search: "?" + queryParams.toString()
})
}
useEffect(() => {
if (fixQueryParams()) {
pushQueryParams();
return;
}
const query: SongQuery = songQuery || { 'titleLike': '' };
setSongs([]);
const request: serverApi.QuerySongsRequest = {
query: toApiQuery(query),
offset: 0,
limit: 20,
offset: offset || 0,
limit: limit || 0,
}
const requestOpts = {
method: 'POST',
@ -160,7 +200,7 @@ function AppBody() {
.then((json: any) => {
'songs' in json && query === songQuery && setSongs(json.songs);
});
}, [songQuery]);
}, [location]);
const onAppBarTabChange = (value: AppBarActiveTab) => {
switch (value) {
@ -178,12 +218,17 @@ function AppBody() {
return (
<div style={{ maxWidth: '100%' }}>
<Switch>
<Redirect exact from='/' to='/songs' />
<Redirect exact from='/' to="/songs" />
<Route path='/songs'>
<AppBar activeTab={AppBarActiveTab.Songs} onActiveTabChange={onAppBarTabChange} />
<FilterControl
query={songQuery}
onChangeQuery={(query: SongQuery) => { setSongQuery(query); }}
onChangeQuery={(squery: SongQuery) => {
if (squery != songQuery) {
queryParams.set('query', JSURL.stringify(squery));
pushQueryParams();
}
}}
/>
<Paper>
<SongList songs={songs} />

@ -10,17 +10,20 @@
export interface ArtistDetails {
id: Number,
name: String,
storeLinks?: String[],
}
export interface TagDetails {
id: Number,
name: String,
parent?: TagDetails,
storeLinks?: String[],
}
export interface SongDetails {
id: Number,
title: String,
artists?: ArtistDetails[],
tags?: TagDetails[],
storeLinks?: String[],
}
// Query for songs (POST).
@ -39,8 +42,8 @@ export enum SongQueryFilterOp {
export enum SongQueryElemProperty {
title = "title",
id = "id",
artistIds = "artistIds",
albumIds = "albumIds",
artistNames = "artistNames",
albumNames = "albumNames",
}
export interface SongQueryElem {
prop?: SongQueryElemProperty,
@ -52,8 +55,8 @@ export interface SongQueryElem {
export interface SongQuery extends SongQueryElem { }
export interface QuerySongsRequest {
query: SongQuery,
offset: Number,
limit: Number,
offset: number,
limit: number,
}
export interface QuerySongsResponse {
songs: SongDetails[]

@ -46,14 +46,14 @@ function ArtistFilterControl(props: ArtistFilterControlProps) {
}
export interface IProps {
query: SongQuery,
query: SongQuery | undefined,
onChangeQuery: (query: SongQuery) => void,
}
export default function FilterControl(props: IProps) {
const selectOptions: string[] = ['Title', 'Artist'];
const selectOption: string = (isTitleQuery(props.query) && 'Title') ||
(isArtistQuery(props.query) && 'Artist') ||
const selectOption: string = (props.query && isTitleQuery(props.query) && 'Title') ||
(props.query && isArtistQuery(props.query) && 'Artist') ||
"Unknown";
const handleQueryOnChange = (event: any) => {
@ -82,7 +82,7 @@ export default function FilterControl(props: IProps) {
return <MenuItem value={option}>{option}</MenuItem>
})}
</Select>
{isTitleQuery(props.query) && <TitleFilterControl query={props.query} onChangeQuery={props.onChangeQuery} />}
{isArtistQuery(props.query) && <ArtistFilterControl query={props.query} onChangeQuery={props.onChangeQuery} />}
{props.query && isTitleQuery(props.query) && <TitleFilterControl query={props.query} onChangeQuery={props.onChangeQuery} />}
{props.query && isArtistQuery(props.query) && <ArtistFilterControl query={props.query} onChangeQuery={props.onChangeQuery} />}
</Paper>;
}

@ -22,10 +22,18 @@ export function isArtistQuery(q: SongQuery): q is ArtistQuery {
}
export function ArtistToApiQuery(q: ArtistQuery) {
return {
'prop': SongQueryElemProperty.artistNames,
'propOperand': '%' + q.artistLike + '%',
'propOperator': SongQueryFilterOp.Like,
}
}
export type SongQuery = TitleQuery | ArtistQuery;
export function isSongQuery(q: any): q is SongQuery {
return q != null &&
(isTitleQuery(q) || isArtistQuery(q));
}
export function toApiQuery(q: SongQuery) {
return (isTitleQuery(q) && TitleToApiQuery(q)) ||
(isArtistQuery(q) && ArtistToApiQuery(q)) || {};

@ -6736,6 +6736,11 @@ jss@^10.0.3, jss@^10.3.0:
is-in-browser "^1.1.3"
tiny-warning "^1.0.2"
jsurl@^0.1.5:
version "0.1.5"
resolved "https://registry.yarnpkg.com/jsurl/-/jsurl-0.1.5.tgz#2a5c8741de39cacafc12f448908bf34e960dcee8"
integrity sha1-KlyHQd45ysr8EvRIkIvzTpYNzug=
jsx-ast-utils@^2.2.1, jsx-ast-utils@^2.2.3:
version "2.2.3"
resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-2.2.3.tgz#8a9364e402448a3ce7f14d357738310d9248054f"

@ -16,8 +16,8 @@ const sequelizeOps: any = {
const sequelizeProps: any = {
[api.SongQueryElemProperty.title]: "title",
[api.SongQueryElemProperty.id]: "id",
[api.SongQueryElemProperty.artistIds]: "$Artists.id$",
[api.SongQueryElemProperty.albumIds]: "$Albums.id$",
[api.SongQueryElemProperty.artistNames]: "$Artists.name$",
[api.SongQueryElemProperty.albumNames]: "$Albums.name$",
};
// Returns the "where" clauses for Sequelize, per object type.
@ -57,21 +57,24 @@ export const QuerySongsEndpointHandler: EndpointHandler = async (req: any, res:
const reqObject: api.QuerySongsRequest = req.body;
try {
console.log('Song query:', reqObject.query, "where: ", getSequelizeWhere(reqObject.query))
const songs = await models.Song.findAll({
// 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),
include: [models.Artist, models.Album, models.Tag],
limit: reqObject.limit,
offset: reqObject.offset,
//limit: reqObject.limit,
//offset: reqObject.offset,
})
const response: api.QuerySongsResponse = {
songs: await Promise.all(songs.map(async (song: any) => {
console.log("Song:", song, "artists:", song.getArtists());
const artists = await song.getArtists();
const tags = await song.getTags();
return <api.SongDetails>{
id: song.id,
title: song.title,
storeLinks: song.storeLinks,
artists: artists.map((artist: any) => {
return <api.ArtistDetails>{
id: artist.id,
@ -83,9 +86,10 @@ export const QuerySongsEndpointHandler: EndpointHandler = async (req: any, res:
id: tag.id,
name: tag.name,
}
})
}),
};
}))
}).slice(reqObject.offset, reqObject.offset + reqObject.limit))
// TODO: custom pagination due to bug mentioned above
};
res.send(response);
} catch (e) {

Loading…
Cancel
Save