diff --git a/client/src/App.tsx b/client/src/App.tsx
index bb39ca6..b8dde12 100644
--- a/client/src/App.tsx
+++ b/client/src/App.tsx
@@ -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, isSongQuery } from './types/Query';
+import { SongQuery, toApiQuery, isSongQuery, QueryKeys } from './types/Query';
import { SongDisplayItem, ArtistDisplayItem } from './types/DisplayItem';
import { ReactComponent as GooglePlayIcon } from './assets/googleplaymusic_icon.svg';
@@ -153,7 +153,7 @@ function AppBody() {
if (!isSongQuery(songQuery)) {
console.log("query");
queryParams.set('query', JSURL.stringify({
- 'titleLike': ''
+ [QueryKeys.TitleLike]: ''
}));
fixed = true;
}
@@ -183,7 +183,7 @@ function AppBody() {
return;
}
- const query: SongQuery = songQuery || { 'titleLike': '' };
+ const query: SongQuery = songQuery || { [QueryKeys.TitleLike]: '' };
setSongs([]);
const request: serverApi.QuerySongsRequest = {
query: toApiQuery(query),
@@ -204,12 +204,8 @@ function AppBody() {
const onAppBarTabChange = (value: AppBarActiveTab) => {
switch (value) {
- case AppBarActiveTab.Artists: {
- history.push('/artists');
- break;
- }
- case AppBarActiveTab.Songs: {
- history.push('/songs');
+ case AppBarActiveTab.Query: {
+ history.push('/query');
break;
}
}
@@ -218,9 +214,9 @@ function AppBody() {
return (
-
-
-
+
+
+
{
@@ -234,12 +230,6 @@ function AppBody() {
-
-
-
-
-
-
);
diff --git a/client/src/components/AppBar.tsx b/client/src/components/AppBar.tsx
index c173a64..a05de87 100644
--- a/client/src/components/AppBar.tsx
+++ b/client/src/components/AppBar.tsx
@@ -68,8 +68,7 @@ const useStyles = makeStyles((theme: Theme) =>
);
export enum ActiveTab {
- Songs = 0,
- Artists = 1,
+ Query = 0,
}
export interface IProps {
@@ -108,8 +107,7 @@ export default function AppBar(props: IProps) {
{ props.onActiveTabChange(idx); }}>
-
-
+
diff --git a/client/src/components/ArtistTable.tsx b/client/src/components/ArtistTable.tsx
deleted file mode 100644
index ceef43e..0000000
--- a/client/src/components/ArtistTable.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-import React from 'react';
-import MaterialTable from 'material-table';
-
-export interface Entry {
- name: String,
- id: Number,
-}
-
-export interface IProps {
- artists: Entry[]
-}
-
-export default function ArtistTable(props: IProps) {
- const tableTitle = "Artists";
- const tableColumns = [
- { title: "Name", field: 'name' },
- ];
- const tableData = props.artists;
-
- const options = {
- filtering: true,
- paging: true,
- pageSize: 100,
- pageSizeOptions: [ 5, 10, 20, 50, 100 ]
- };
-
- return (
-
- );
-}
\ No newline at end of file
diff --git a/client/src/components/FilterControl.tsx b/client/src/components/FilterControl.tsx
index 7265873..107c894 100644
--- a/client/src/components/FilterControl.tsx
+++ b/client/src/components/FilterControl.tsx
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { ReactElement } from 'react';
import {
TextField,
@@ -13,10 +13,12 @@ import {
ArtistQuery,
isTitleQuery,
isArtistQuery,
- SongQuery
+ SongQuery,
+ isAndQuery,
+ isOrQuery,
+ QueryKeys,
} from '../types/Query';
-
interface TitleFilterControlProps {
query: TitleQuery,
onChangeQuery: (q: SongQuery) => void,
@@ -24,9 +26,9 @@ interface TitleFilterControlProps {
function TitleFilterControl(props: TitleFilterControlProps) {
return props.onChangeQuery({
- titleLike: i.target.value
+ [QueryKeys.TitleLike]: i.target.value
})}
/>
}
@@ -38,35 +40,106 @@ interface ArtistFilterControlProps {
function ArtistFilterControl(props: ArtistFilterControlProps) {
return props.onChangeQuery({
- artistLike: i.target.value
+ [QueryKeys.ArtistLike]: i.target.value
})}
/>
}
+interface AndNodeControlProps {
+ query: any,
+ onChangeQuery: (q: SongQuery) => void,
+}
+function AndNodeControl(props: AndNodeControlProps) {
+ const onChangeSubQuery = (a: SongQuery, b: SongQuery) => {
+ props.onChangeQuery({
+ [QueryKeys.AndQuerySignature]: true,
+ [QueryKeys.OperandA]: a,
+ [QueryKeys.OperandB]: b
+ });
+ }
+
+ return
+ {props.query && isAndQuery(props.query) && <>
+ And
+ { onChangeSubQuery(q, props.query.b); }} />
+ { onChangeSubQuery(props.query.a, q); }} />
+ >}
+ ;
+}
+
+interface OrNodeControlProps {
+ query: any,
+ onChangeQuery: (q: SongQuery) => void,
+}
+function OrNodeControl(props: OrNodeControlProps) {
+ const onChangeSubQuery = (a: SongQuery, b: SongQuery) => {
+ props.onChangeQuery({
+ [QueryKeys.OrQuerySignature]: true,
+ [QueryKeys.OperandA]: a,
+ [QueryKeys.OperandB]: b
+ });
+ }
+
+ return
+ {props.query && isOrQuery(props.query) && <>
+ Or
+ { onChangeSubQuery(q, props.query.b); }} />
+ { onChangeSubQuery(props.query.a, q); }} />
+ >}
+ ;
+}
+
export interface IProps {
query: SongQuery | undefined,
onChangeQuery: (query: SongQuery) => void,
}
-export default function FilterControl(props: IProps) {
- const selectOptions: string[] = ['Title', 'Artist'];
- const selectOption: string = (props.query && isTitleQuery(props.query) && 'Title') ||
+export function FilterControlLeaf(props: IProps) {
+ const selectTypeOptions: string[] = ['Title', 'Artist'];
+ const selectTypeOption: string = (props.query && isTitleQuery(props.query) && 'Title') ||
(props.query && isArtistQuery(props.query) && 'Artist') ||
"Unknown";
+ const selectInsertOptions: string[] = ['And', 'Or'];
+
const handleQueryOnChange = (event: any) => {
switch (event.target.value) {
case 'Title': {
props.onChangeQuery({
- titleLike: ''
+ [QueryKeys.TitleLike]: ''
})
break;
}
case 'Artist': {
props.onChangeQuery({
- artistLike: ''
+ [QueryKeys.ArtistLike]: ''
+ })
+ break;
+ }
+ }
+ }
+
+ const handleInsertElem = (event: any) => {
+ switch (event.target.value) {
+ case 'And': {
+ props.onChangeQuery({
+ [QueryKeys.AndQuerySignature]: true,
+ [QueryKeys.OperandA]: props.query || { [QueryKeys.TitleLike]: '' },
+ [QueryKeys.OperandB]: {
+ [QueryKeys.TitleLike]: ''
+ }
+ })
+ break;
+ }
+ case 'Or': {
+ props.onChangeQuery({
+ [QueryKeys.OrQuerySignature]: true,
+ [QueryKeys.OperandA]: props.query || { [QueryKeys.TitleLike]: '' },
+ [QueryKeys.OperandB]: {
+ [QueryKeys.TitleLike]: ''
+ }
})
break;
}
@@ -74,15 +147,48 @@ export default function FilterControl(props: IProps) {
}
return
+ {/* The selector for inserting another element here. */}
+
+ {/* The selector for the type of filter element. */}
{props.query && isTitleQuery(props.query) && }
{props.query && isArtistQuery(props.query) && }
;
+}
+
+export function FilterControlNode(props: IProps) {
+ const selectTypeOptions: string[] = ['And', 'Or'];
+ const selectTypeOption: string = (props.query && isAndQuery(props.query) && 'And') ||
+ (props.query && isOrQuery(props.query) && 'Or') ||
+ "Unknown";
+
+ return <>
+ {props.query && isAndQuery(props.query) && }
+ {props.query && isOrQuery(props.query) && }
+ >;
+}
+
+export default function FilterControl(props: IProps) {
+ const isLeaf = (query: SongQuery | undefined) => {
+ return query && (isTitleQuery(query) || isArtistQuery(query));
+ }
+ const isNode = (query: SongQuery | undefined) => !isLeaf(query);
+
+ return <>
+ {isLeaf(props.query) && }
+ {isNode(props.query) && }
+ >
}
\ No newline at end of file
diff --git a/client/src/components/SongTable.tsx b/client/src/components/SongTable.tsx
deleted file mode 100644
index d22d19a..0000000
--- a/client/src/components/SongTable.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import React from 'react';
-import MaterialTable from 'material-table';
-
-export interface Entry {
- title: String,
- artistName: String
-}
-
-export interface IProps {
- songs: Entry[]
-}
-
-export default function SongTable(props: IProps) {
- const tableTitle = "Songs";
- const tableColumns = [
- { title: "Title", field: 'title' },
- { title: "Artist", field: 'artistName' },
- ];
- const tableData = props.songs;
-
- const options = {
- filtering: true,
- paging: true,
- pageSize: 100,
- pageSizeOptions: [ 5, 10, 20, 50, 100 ]
- };
-
- return (
-
- );
-}
\ No newline at end of file
diff --git a/client/src/types/Query.tsx b/client/src/types/Query.tsx
index 9b2836c..7ddb1dd 100644
--- a/client/src/types/Query.tsx
+++ b/client/src/types/Query.tsx
@@ -1,40 +1,89 @@
-import { SongQueryElemProperty, SongQueryFilterOp } from '../api';
+import { SongQueryElemProperty, SongQueryFilterOp, SongQueryElemOp } from '../api';
+
+export enum QueryKeys {
+ TitleLike = 'tl',
+ ArtistLike = 'al',
+ AndQuerySignature = 'and',
+ OrQuerySignature = 'or',
+ OperandA = 'a',
+ OperandB = 'b',
+}
export interface TitleQuery {
- titleLike: String
+ [QueryKeys.TitleLike]: String
};
export function isTitleQuery(q: SongQuery): q is TitleQuery {
- return "titleLike" in q;
+ return QueryKeys.TitleLike in q;
}
export function TitleToApiQuery(q: TitleQuery) {
return {
'prop': SongQueryElemProperty.title,
- 'propOperand': '%' + q.titleLike + '%',
+ 'propOperand': '%' + q[QueryKeys.TitleLike] + '%',
'propOperator': SongQueryFilterOp.Like,
}
}
export interface ArtistQuery {
- artistLike: String
+ [QueryKeys.ArtistLike]: String
};
export function isArtistQuery(q: SongQuery): q is ArtistQuery {
- return "artistLike" in q;
+ return QueryKeys.ArtistLike in q;
}
export function ArtistToApiQuery(q: ArtistQuery) {
return {
'prop': SongQueryElemProperty.artistNames,
- 'propOperand': '%' + q.artistLike + '%',
+ 'propOperand': '%' + q[QueryKeys.ArtistLike] + '%',
'propOperator': SongQueryFilterOp.Like,
}
}
-export type SongQuery = TitleQuery | ArtistQuery;
+export interface AndQuery {
+ [QueryKeys.AndQuerySignature]: any,
+ [QueryKeys.OperandA]: T,
+ [QueryKeys.OperandB]: T,
+}
+export function isAndQuery(q: SongQuery): q is AndQuery {
+ return QueryKeys.AndQuerySignature in q;
+}
+export function AndToApiQuery(q: AndQuery) {
+ return {
+ 'childrenOperator': SongQueryElemOp.And,
+ 'children': [
+ toApiQuery(q.a),
+ toApiQuery(q.b),
+ ]
+ }
+}
+
+export interface OrQuery {
+ [QueryKeys.OrQuerySignature]: any,
+ [QueryKeys.OperandA]: T,
+ [QueryKeys.OperandB]: T,
+}
+export function isOrQuery(q: SongQuery): q is OrQuery {
+ return QueryKeys.OrQuerySignature in q;
+}
+export function OrToApiQuery(q: OrQuery) {
+ return {
+ 'childrenOperator': SongQueryElemOp.Or,
+ 'children': [
+ toApiQuery(q.a),
+ toApiQuery(q.b),
+ ]
+ }
+}
+
+export type SongQuery = TitleQuery | ArtistQuery | AndQuery | OrQuery;
+
export function isSongQuery(q: any): q is SongQuery {
return q != null &&
- (isTitleQuery(q) || isArtistQuery(q));
+ (isTitleQuery(q) || isArtistQuery(q) || isAndQuery(q) || isOrQuery(q));
}
-export function toApiQuery(q: SongQuery) {
+export function toApiQuery(q: SongQuery): any {
return (isTitleQuery(q) && TitleToApiQuery(q)) ||
- (isArtistQuery(q) && ArtistToApiQuery(q)) || {};
+ (isArtistQuery(q) && ArtistToApiQuery(q)) ||
+ (isAndQuery(q) && AndToApiQuery(q)) ||
+ (isOrQuery(q) && OrToApiQuery(q)) ||
+ {};
}
\ No newline at end of file