From 4f3cd30e774341c6b19ee3c536a80143e0829857 Mon Sep 17 00:00:00 2001 From: Sander Vocke Date: Wed, 9 Sep 2020 13:35:54 +0200 Subject: [PATCH] Removed old UI. Started with a mock-up and simple query rendering. --- client/src/App.tsx | 132 ++---------- client/src/components/AppBar.tsx | 115 ---------- client/src/components/BrowseWindow.tsx | 68 ------ .../src/components/DraggableItemListItem.tsx | 19 -- client/src/components/EditArtistDialog.tsx | 46 ---- client/src/components/EditSongDialog.tsx | 85 -------- client/src/components/FilterControl.tsx | 189 ----------------- client/src/components/ItemList.tsx | 24 --- client/src/components/ItemListArtistItem.tsx | 18 -- client/src/components/ItemListItem.tsx | 19 -- .../components/ItemListLoadedArtistItem.tsx | 35 ---- .../src/components/ItemListLoadedSongItem.tsx | 41 ---- .../components/ItemListLoadingArtistItem.tsx | 22 -- .../components/ItemListLoadingSongItem.tsx | 22 -- client/src/components/ItemListSongItem.tsx | 18 -- client/src/components/QueryBrowseWindow.tsx | 198 ------------------ client/src/components/Window.tsx | 37 ++++ .../components/querybuilder/QBAndBlock.tsx | 16 ++ .../src/components/querybuilder/QBOrBlock.tsx | 26 +++ .../components/querybuilder/QBQueryElem.tsx | 25 +++ .../querybuilder/QBQueryLeafElem.tsx | 29 +++ .../querybuilder/QBQueryNodeElem.tsx | 35 ++++ client/src/lib/Query.tsx | 63 ++++++ client/src/types/DisplayItem.tsx | 44 ---- client/src/types/DragTypes.tsx | 3 - client/src/types/Query.tsx | 129 ------------ 26 files changed, 243 insertions(+), 1215 deletions(-) delete mode 100644 client/src/components/AppBar.tsx delete mode 100644 client/src/components/BrowseWindow.tsx delete mode 100644 client/src/components/DraggableItemListItem.tsx delete mode 100644 client/src/components/EditArtistDialog.tsx delete mode 100644 client/src/components/EditSongDialog.tsx delete mode 100644 client/src/components/FilterControl.tsx delete mode 100644 client/src/components/ItemList.tsx delete mode 100644 client/src/components/ItemListArtistItem.tsx delete mode 100644 client/src/components/ItemListItem.tsx delete mode 100644 client/src/components/ItemListLoadedArtistItem.tsx delete mode 100644 client/src/components/ItemListLoadedSongItem.tsx delete mode 100644 client/src/components/ItemListLoadingArtistItem.tsx delete mode 100644 client/src/components/ItemListLoadingSongItem.tsx delete mode 100644 client/src/components/ItemListSongItem.tsx delete mode 100644 client/src/components/QueryBrowseWindow.tsx create mode 100644 client/src/components/Window.tsx create mode 100644 client/src/components/querybuilder/QBAndBlock.tsx create mode 100644 client/src/components/querybuilder/QBOrBlock.tsx create mode 100644 client/src/components/querybuilder/QBQueryElem.tsx create mode 100644 client/src/components/querybuilder/QBQueryLeafElem.tsx create mode 100644 client/src/components/querybuilder/QBQueryNodeElem.tsx create mode 100644 client/src/lib/Query.tsx delete mode 100644 client/src/types/DisplayItem.tsx delete mode 100644 client/src/types/DragTypes.tsx delete mode 100644 client/src/types/Query.tsx diff --git a/client/src/App.tsx b/client/src/App.tsx index 4adecdb..bbe4c9c 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,137 +1,29 @@ -import React, { useEffect } from 'react'; +import React from 'react'; -import AppBar, { ActiveTab as AppBarActiveTab } from './components/AppBar'; -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 { HashRouter as Router, Switch, - Route, - useHistory, - useLocation, - Redirect + Route } from "react-router-dom"; -import { Typography } from '@material-ui/core'; import Mockup from './components/Mockup'; - -const JSURL = require('jsurl'); - -function fixQuery(q: any): Query { - if (!isQuery(q)) { - return { - [QueryKeys.TitleLike]: '' - }; - } - 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 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 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() - }) - } - - useEffect(() => { - const fq = fixQuery(itemQuery); - const fo = fixOrder(itemOrder); - const ft = fixTypes(itemTypes); - if (fq !== itemQuery || fo !== itemOrder || ft !== itemTypes) { - pushQuery(fq, fo, ft); - return; - } - }, [ itemOrder, itemQuery, itemTypes ]); - - const onAppBarTabChange = (value: AppBarActiveTab) => { - switch (value) { - case AppBarActiveTab.Query: { - history.push('/query'); - break; - } - } - } - - const onQueryChange = (q: Query) => { - 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 ( -
- - - - - - - -
- ); -} - -function AppMockup() { - return -} +import Window from './components/Window'; function App() { return ( - + + + + + + + + + ); diff --git a/client/src/components/AppBar.tsx b/client/src/components/AppBar.tsx deleted file mode 100644 index a05de87..0000000 --- a/client/src/components/AppBar.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import React from 'react'; -import MuiAppBar from '@material-ui/core/AppBar'; -import Toolbar from '@material-ui/core/Toolbar'; -import IconButton from '@material-ui/core/IconButton'; -import Typography from '@material-ui/core/Typography'; -import InputBase from '@material-ui/core/InputBase'; -import { createStyles, fade, Theme, makeStyles } from '@material-ui/core/styles'; -import MenuIcon from '@material-ui/icons/Menu'; -import SearchIcon from '@material-ui/icons/Search'; -import Tabs from '@material-ui/core/Tabs'; -import Tab from '@material-ui/core/Tab'; - -const useStyles = makeStyles((theme: Theme) => - createStyles({ - root: { - flexGrow: 1, - }, - menuButton: { - marginRight: theme.spacing(2), - }, - title: { - flexGrow: 1, - display: 'none', - [theme.breakpoints.up('sm')]: { - display: 'block', - }, - }, - search: { - position: 'relative', - borderRadius: theme.shape.borderRadius, - backgroundColor: fade(theme.palette.common.white, 0.15), - '&:hover': { - backgroundColor: fade(theme.palette.common.white, 0.25), - }, - marginLeft: 0, - width: '100%', - [theme.breakpoints.up('sm')]: { - marginLeft: theme.spacing(1), - width: 'auto', - }, - }, - searchIcon: { - padding: theme.spacing(0, 2), - height: '100%', - position: 'absolute', - pointerEvents: 'none', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - }, - inputRoot: { - color: 'inherit', - }, - inputInput: { - padding: theme.spacing(1, 1, 1, 0), - // vertical padding + font size from searchIcon - paddingLeft: `calc(1em + ${theme.spacing(4)}px)`, - transition: theme.transitions.create('width'), - width: '100%', - [theme.breakpoints.up('sm')]: { - width: '12ch', - '&:focus': { - width: '20ch', - }, - }, - }, - }), -); - -export enum ActiveTab { - Query = 0, -} - -export interface IProps { - activeTab: ActiveTab, - onActiveTabChange: (tab:ActiveTab) => void -} - -export default function AppBar(props: IProps) { - const classes = useStyles(); - - return ( -
- - - - - - MuDBase -
-
- -
- -
-
- { props.onActiveTabChange(idx); }}> - - -
-
- ); -} diff --git a/client/src/components/BrowseWindow.tsx b/client/src/components/BrowseWindow.tsx deleted file mode 100644 index a117657..0000000 --- a/client/src/components/BrowseWindow.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import React from 'react'; - -import { Paper } from '@material-ui/core'; -import { DisplayItem } from '../types/DisplayItem'; -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'; - -type SongItem = serverApi.SongDetails; -type ArtistItem = serverApi.ArtistDetails; -export type Item = SongItem | ArtistItem; - -const getStoreIcon = (url: String) => { - if (url.includes('play.google.com')) { - return ; - } - return ; -} - -function toDisplayItem(item: Item): DisplayItem | undefined { - if (serverApi.isSongDetails(item)) { - return { - title: item.title, - artistNames: (item.artists && item.artists.map((artist: serverApi.ArtistDetails) => { - return artist.name; - })) || ['Unknown'], - tagNames: (item.tags && item.tags.map((tag: serverApi.TagDetails) => { - return tag.name; - })) || [], - storeLinks: (item.storeLinks && item.storeLinks.map((url: String) => { - return { - icon: getStoreIcon(url), - url: url - } - })) || [], - } - } else if (serverApi.isArtistDetails(item)) { - return { - name: item.name ? item.name : "Unknown", - tagNames: [], // TODO - storeLinks: (item.storeLinks && item.storeLinks.map((url: String) => { - return { - icon: getStoreIcon(url), - url: url - } - })) || [], - }; - - } - return undefined; -} - -interface IProps { - items: Item[] -} - -export default function BrowseWindow(props: IProps) { - return - - {props.items.map((item: Item) => { - const di = toDisplayItem(item); - return di && ; - })} - - ; -} \ No newline at end of file diff --git a/client/src/components/DraggableItemListItem.tsx b/client/src/components/DraggableItemListItem.tsx deleted file mode 100644 index 318b0be..0000000 --- a/client/src/components/DraggableItemListItem.tsx +++ /dev/null @@ -1,19 +0,0 @@ -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
- -
; -} \ No newline at end of file diff --git a/client/src/components/EditArtistDialog.tsx b/client/src/components/EditArtistDialog.tsx deleted file mode 100644 index 760f5bc..0000000 --- a/client/src/components/EditArtistDialog.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import React from 'react'; -import { Dialog, Grid, Typography, TextField, Button } from '@material-ui/core'; - -var cloneDeep = require('lodash/cloneDeep'); - -export interface ArtistProperties { - name: String, -} - -export interface IProps { - dialogOpen: boolean, - onClose?: () => void, - onChangeArtistProperties?: (props: ArtistProperties) => void, - artistProperties: ArtistProperties, - onSubmit?: () => void, -} - -export default function EditArtistDialog(props: IProps) { - const onNameChange = (name: String) => { - if (props.onChangeArtistProperties) { - const p = cloneDeep(props.artistProperties); - p.name = name; - props.onChangeArtistProperties(p); - } - }; - - return - - Artist Details - - - - onNameChange(i.target.value)} - fullWidth - /> - - - - -} \ No newline at end of file diff --git a/client/src/components/EditSongDialog.tsx b/client/src/components/EditSongDialog.tsx deleted file mode 100644 index 70a60aa..0000000 --- a/client/src/components/EditSongDialog.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { Dialog, Grid, Typography, TextField, Button } from '@material-ui/core'; -import { Autocomplete } from '@material-ui/lab'; - -var cloneDeep = require('lodash/cloneDeep'); - -export interface SongProperties { - title: String, - artistId: Number | undefined, -} - -export interface ArtistProperties { - name: String, - id: Number, -} - -export interface IProps { - dialogOpen: boolean, - onClose?: () => void, - onChangeSongProperties?: (props: SongProperties) => void, - songProperties: SongProperties, - onSubmit?: () => void, - artists: ArtistProperties[], -} - -export default function EditSongDialog(props: IProps) { - const onTitleChange = (title: String) => { - if (props.onChangeSongProperties) { - const p = cloneDeep(props.songProperties); - p.title = title; - props.onChangeSongProperties(p); - } - }; - const onArtistChange = (artist: Number | undefined) => { - if (props.onChangeSongProperties) { - const p = cloneDeep(props.songProperties); - p.artistId = artist; - props.onChangeSongProperties(p); - } - }; - - return - - Song Details - - - - onTitleChange(i.target.value)} - fullWidth - /> - - - { // TODO: this autocomplete is not controlled but does send updates to the parent - // right away. In other words, there is no way to affect its value from outside - // the dialog. - } - option.name as string} - onChange={(event, newValue) => { - if(newValue) { - onArtistChange(newValue.id); - } else { - onArtistChange(undefined); - } - }} - renderInput={ - (params) => - - } - /> - - - - -} \ No newline at end of file diff --git a/client/src/components/FilterControl.tsx b/client/src/components/FilterControl.tsx deleted file mode 100644 index 510ac67..0000000 --- a/client/src/components/FilterControl.tsx +++ /dev/null @@ -1,189 +0,0 @@ -import React from 'react'; - -import { - TextField, - Paper, - Select, - MenuItem, - Typography -} from '@material-ui/core'; - -import { - TitleQuery, - ArtistQuery, - isTitleQuery, - isArtistQuery, - Query, - isAndQuery, - isOrQuery, - QueryKeys, -} from '../types/Query'; - -interface TitleFilterControlProps { - query: TitleQuery, - onChangeQuery: (q: Query) => void, -} -function TitleFilterControl(props: TitleFilterControlProps) { - return props.onChangeQuery({ - [QueryKeys.TitleLike]: i.target.value - })} - /> -} - -interface ArtistFilterControlProps { - query: ArtistQuery, - onChangeQuery: (q: Query) => void, -} -function ArtistFilterControl(props: ArtistFilterControlProps) { - return props.onChangeQuery({ - [QueryKeys.ArtistLike]: i.target.value - })} - /> -} - -interface AndNodeControlProps { - query: any, - onChangeQuery: (q: Query) => void, -} -function AndNodeControl(props: AndNodeControlProps) { - const onChangeSubQuery = (a: Query, b: Query) => { - 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: Query) => void, -} -function OrNodeControl(props: OrNodeControlProps) { - const onChangeSubQuery = (a: Query, b: Query) => { - 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: Query | undefined, - onChangeQuery: (query: Query) => void, -} - -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({ - [QueryKeys.TitleLike]: '' - }) - break; - } - case 'Artist': { - props.onChangeQuery({ - [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; - } - } - } - - 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) { - return <> - {props.query && isAndQuery(props.query) && } - {props.query && isOrQuery(props.query) && } - ; -} - -export default function FilterControl(props: IProps) { - const isLeaf = (query: Query | undefined) => { - return query && (isTitleQuery(query) || isArtistQuery(query)); - } - const isNode = (query: Query | undefined) => !isLeaf(query); - - return <> - {isLeaf(props.query) && } - {isNode(props.query) && } - -} \ No newline at end of file diff --git a/client/src/components/ItemList.tsx b/client/src/components/ItemList.tsx deleted file mode 100644 index 422224d..0000000 --- a/client/src/components/ItemList.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import React from 'react'; -import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'; -import List from '@material-ui/core/List'; - -const useStyles = makeStyles((theme: Theme) => - createStyles({ - root: { - flexGrow: 1, - maxWidth: 752, - }, - }), -); - -export default function ItemList(props:any) { - const classes = useStyles(); - - return ( -
- - {props.children} - -
- ); -} diff --git a/client/src/components/ItemListArtistItem.tsx b/client/src/components/ItemListArtistItem.tsx deleted file mode 100644 index b4458bb..0000000 --- a/client/src/components/ItemListArtistItem.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import React, { useEffect } from 'react'; -import ItemListItem from './ItemListItem'; -import { ArtistDisplayItem, LoadingArtistDisplayItem } from '../types/DisplayItem'; - -export interface IProps { - getDetails: () => Promise -} - -export default function ItemListArtistItem(props: IProps) { - const [ artist, setArtist ] = React.useState({ loadingArtist: true }); - - useEffect(() => { - props.getDetails() - .then((details:ArtistDisplayItem) => { setArtist(details); }); - }); - - return -} diff --git a/client/src/components/ItemListItem.tsx b/client/src/components/ItemListItem.tsx deleted file mode 100644 index e4d1430..0000000 --- a/client/src/components/ItemListItem.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from 'react'; -import { DisplayItem, isSong, isLoadingSong, isArtist, isLoadingArtist } from '../types/DisplayItem'; -import ItemListLoadedSongItem from './ItemListLoadedSongItem'; -import ItemListLoadingSongItem from './ItemListLoadingSongItem'; -import ItemListLoadedArtistItem from './ItemListLoadedArtistItem'; -import ItemListLoadingArtistItem from './ItemListLoadingArtistItem'; - -export interface IProps { - item: DisplayItem -} - -export default function ItemListItem(props: IProps) { - return <> - {isSong(props.item) && } - {isLoadingSong(props.item) && } - {isArtist(props.item) && } - {isLoadingArtist(props.item) && } - -} diff --git a/client/src/components/ItemListLoadedArtistItem.tsx b/client/src/components/ItemListLoadedArtistItem.tsx deleted file mode 100644 index 2a8937b..0000000 --- a/client/src/components/ItemListLoadedArtistItem.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import React from 'react'; -import ListItem from '@material-ui/core/ListItem'; -import ListItemIcon from '@material-ui/core/ListItemIcon'; -import ListItemText from '@material-ui/core/ListItemText'; -import GroupIcon from '@material-ui/icons/Group'; -import Chip from '@material-ui/core/Chip'; - -import { ArtistDisplayItem } from '../types/DisplayItem'; - -export interface IProps { - item: ArtistDisplayItem -} - -export default function ItemListLoadedArtistItem(props: IProps) { - return ( - - - - - - {props.item.tagNames.map((tag: any) => { - return - })} - {props.item.storeLinks.map((link: any) => { - return - - {link.icon} - - ; - })} - - ); -} diff --git a/client/src/components/ItemListLoadedSongItem.tsx b/client/src/components/ItemListLoadedSongItem.tsx deleted file mode 100644 index d3968a8..0000000 --- a/client/src/components/ItemListLoadedSongItem.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import React from 'react'; -import ListItem from '@material-ui/core/ListItem'; -import ListItemIcon from '@material-ui/core/ListItemIcon'; -import ListItemText from '@material-ui/core/ListItemText'; -import MusicNoteIcon from '@material-ui/icons/MusicNote'; -import Chip from '@material-ui/core/Chip'; - -import { SongDisplayItem } from '../types/DisplayItem'; - -export interface IProps { - item: SongDisplayItem -} - -export default function ItemListLoadedSongItem(props: IProps) { - var artists = props.item.artistNames.length ? props.item.artistNames[0] : "Unknown"; - for (var i: number = 1; i < props.item.artistNames.length; i++) { - artists = artists.concat(", " + props.item.artistNames[i]); - } - - return ( - - - - - - {props.item.tagNames.map((tag: any) => { - return - })} - {props.item.storeLinks.map((link: any) => { - return - - {link.icon} - - ; - })} - - ); -} diff --git a/client/src/components/ItemListLoadingArtistItem.tsx b/client/src/components/ItemListLoadingArtistItem.tsx deleted file mode 100644 index 614030d..0000000 --- a/client/src/components/ItemListLoadingArtistItem.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import React from 'react'; -import ListItem from '@material-ui/core/ListItem'; -import ListItemIcon from '@material-ui/core/ListItemIcon'; -import GroupIcon from '@material-ui/icons/Group'; -import CircularProgress from '@material-ui/core/CircularProgress'; - -import { LoadingArtistDisplayItem } from '../types/DisplayItem'; - -export interface IProps { - item: LoadingArtistDisplayItem -} - -export default function ItemListLoadingArtistItem(props: IProps) { - return ( - - - - - - - ); -} diff --git a/client/src/components/ItemListLoadingSongItem.tsx b/client/src/components/ItemListLoadingSongItem.tsx deleted file mode 100644 index 361ded6..0000000 --- a/client/src/components/ItemListLoadingSongItem.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import React from 'react'; -import ListItem from '@material-ui/core/ListItem'; -import ListItemIcon from '@material-ui/core/ListItemIcon'; -import MusicNoteIcon from '@material-ui/icons/MusicNote'; -import CircularProgress from '@material-ui/core/CircularProgress'; - -import { LoadingSongDisplayItem } from '../types/DisplayItem'; - -export interface IProps { - item: LoadingSongDisplayItem -} - -export default function ItemListLoadingSongItem(props: IProps) { - return ( - - - - - - - ); -} diff --git a/client/src/components/ItemListSongItem.tsx b/client/src/components/ItemListSongItem.tsx deleted file mode 100644 index f3586b5..0000000 --- a/client/src/components/ItemListSongItem.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import React, { useEffect } from 'react'; -import ItemListItem from './ItemListItem'; -import { SongDisplayItem, LoadingSongDisplayItem } from '../types/DisplayItem'; - -export interface IProps { - getDetails: () => Promise -} - -export default function ItemListSongItem(props: IProps) { - const [ song, setSong ] = React.useState({ loadingSong: true }); - - useEffect(() => { - props.getDetails() - .then((details:SongDisplayItem) => { setSong(details); }); - }); - - return -} diff --git a/client/src/components/QueryBrowseWindow.tsx b/client/src/components/QueryBrowseWindow.tsx deleted file mode 100644 index 16dac6c..0000000 --- a/client/src/components/QueryBrowseWindow.tsx +++ /dev/null @@ -1,198 +0,0 @@ -import React, { useState, useEffect } from 'react'; - -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, Select, MenuItem } from '@material-ui/core'; - -const _ = require('lodash'); - -interface ItemTypeCheckboxesProps { - types: TypesIncluded, - onChange: (types: TypesIncluded) => void; -} - -function ItemTypeCheckboxes(props: ItemTypeCheckboxesProps) { - const songChange = (v: any) => { - props.onChange({ - [QueryKeys.Songs]: v.target.checked, - [QueryKeys.Artists]: props.types[QueryKeys.Artists], - [QueryKeys.Tags]: props.types[QueryKeys.Tags] - }); - } - const artistChange = (v: any) => { - props.onChange({ - [QueryKeys.Songs]: props.types[QueryKeys.Songs], - [QueryKeys.Artists]: v.target.checked, - [QueryKeys.Tags]: props.types[QueryKeys.Tags] - }); - } - const tagChange = (v: any) => { - props.onChange({ - [QueryKeys.Songs]: props.types[QueryKeys.Songs], - [QueryKeys.Artists]: props.types[QueryKeys.Artists], - [QueryKeys.Tags]: v.target.checked - }); - } - - return - Result types - - } - label="Songs" - /> - } - label="Artists" - /> - } - label="Tags" - /> - - ; -} - -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 - Ordering - - - - - ; -} - -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 | undefined, - resultOrder: QueryOrdering | undefined, - onQueryChange: (q: Query) => void, - onTypesChange: (t: TypesIncluded) => void, - onOrderChange: (o: QueryOrdering) => void, -} - -export default function QueryBrowseWindow(props: IProps) { - const [songs, setSongs] = useState([]); - const [artists, setArtists] = useState([]); - //const [tags, setTags] = useState([]); - - var items: Item[] = []; - 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), - offsetsLimits: { - songOffset: 0, - songLimit: 5, // TODO - artistOffset: 0, - artistLimit: 5, - tagOffset: 0, - tagLimit: 5, - }, - ordering: toServerOrdering(props.resultOrder), - } - const requestOpts = { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(request) - }; - fetch((process.env.REACT_APP_BACKEND || "") + serverApi.QueryEndpoint, requestOpts) - .then((response: any) => response.json()) - .then((json: any) => { - 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, props.resultOrder, props.typesIncluded ]); - - return <> - - Query - - - - - - -} diff --git a/client/src/components/Window.tsx b/client/src/components/Window.tsx new file mode 100644 index 0000000..f241ca6 --- /dev/null +++ b/client/src/components/Window.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import { ThemeProvider, CssBaseline, createMuiTheme } from '@material-ui/core'; +import { QBQueryElem } from './querybuilder/QBQueryElem'; +import { QueryLeafElem, QueryLeafBy, QueryLeafOp, QueryElem, QueryNodeElem, queryOr, queryAnd } from '../lib/Query'; + +const darkTheme = createMuiTheme({ + palette: { + type: 'dark' + }, +}); + +export default function Window(props: any) { + let queens = new QueryLeafElem( + QueryLeafBy.ArtistName, + QueryLeafOp.Equals, + "Queens of the Stone Age" + ); + let muse = new QueryLeafElem( + QueryLeafBy.ArtistName, + QueryLeafOp.Equals, + "Muse" + ); + let dawnbros = new QueryLeafElem( + QueryLeafBy.ArtistName, + QueryLeafOp.Equals, + "Dawn Brothers" + ); + let query = queryOr( + queryAnd(queens, muse), + dawnbros + ); + + return + + + +} \ No newline at end of file diff --git a/client/src/components/querybuilder/QBAndBlock.tsx b/client/src/components/querybuilder/QBAndBlock.tsx new file mode 100644 index 0000000..402a326 --- /dev/null +++ b/client/src/components/querybuilder/QBAndBlock.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import { Box, Paper } from '@material-ui/core'; + +export default function QBAndBlock(props: any) { + return + + + {props.children.map((child: any) => { + return + {child} + + })} + + + +} \ No newline at end of file diff --git a/client/src/components/querybuilder/QBOrBlock.tsx b/client/src/components/querybuilder/QBOrBlock.tsx new file mode 100644 index 0000000..deb09a9 --- /dev/null +++ b/client/src/components/querybuilder/QBOrBlock.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { Box, Typography } from '@material-ui/core'; + +export default function QBOrBlock(props: any) { + const firstChild = Array.isArray(props.children) && props.children.length >= 1 ? + props.children[0] : undefined; + + const otherChildren = Array.isArray(props.children) && props.children.length > 1 ? + props.children.slice(1) : []; + + return + + {firstChild} + + {otherChildren.map((child: any) => { + return <> + + Or + + + {child} + + ; + })} + +} \ No newline at end of file diff --git a/client/src/components/querybuilder/QBQueryElem.tsx b/client/src/components/querybuilder/QBQueryElem.tsx new file mode 100644 index 0000000..1e71706 --- /dev/null +++ b/client/src/components/querybuilder/QBQueryElem.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { QueryLeafElem, QueryNodeElem } from '../../lib/Query'; +import { QBQueryLeafElem } from './QBQueryLeafElem'; +import { QBQueryNodeElem } from './QBQueryNodeElem'; + +export interface IProps { + elem: QueryLeafElem | QueryNodeElem, +} + +export function QBQueryElem(props: IProps) { + let e = props.elem; + + let renderLeaf = (l: any) => { + return + } + + if (e instanceof QueryLeafElem) { + return renderLeaf(e); + } else if (e instanceof QueryNodeElem) { + return + } + + throw "Unsupported query element"; +} \ No newline at end of file diff --git a/client/src/components/querybuilder/QBQueryLeafElem.tsx b/client/src/components/querybuilder/QBQueryLeafElem.tsx new file mode 100644 index 0000000..24dac07 --- /dev/null +++ b/client/src/components/querybuilder/QBQueryLeafElem.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { QueryLeafElem, QueryLeafBy, QueryLeafOp, QueryNodeElem, QueryNodeOp } from '../../lib/Query'; +import { Chip, Typography } from '@material-ui/core'; + +export interface LeafProps { + elem: QueryLeafElem +} + +export function QBQueryElemArtistEquals(props: LeafProps) { + let e = props.elem; + + const label = + By {e.b} + ; + + return +} + +export function QBQueryLeafElem(props: LeafProps) { + let e = props.elem; + + if (e.a == QueryLeafBy.ArtistName && + e.op == QueryLeafOp.Equals && + typeof e.b == "string") { + return + } + + throw "Unsupported leaf element"; +} \ No newline at end of file diff --git a/client/src/components/querybuilder/QBQueryNodeElem.tsx b/client/src/components/querybuilder/QBQueryNodeElem.tsx new file mode 100644 index 0000000..a5a5614 --- /dev/null +++ b/client/src/components/querybuilder/QBQueryNodeElem.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import QBOrBlock from './QBOrBlock'; +import QBAndBlock from './QBAndBlock'; +import { QueryNodeElem, QueryNodeOp } from '../../lib/Query'; + +export interface NodeProps { + elem: QueryNodeElem, + renderLeaf: (leaf: any) => any, +} + +export function QBQueryNodeElem(props: NodeProps) { + let e = props.elem; + + if (e.op == QueryNodeOp.And) { + return + {e.operands.map((o: any) => { + if(o instanceof QueryNodeElem) { + return + } + return props.renderLeaf(o); + })} + + } else if (e.op == QueryNodeOp.Or) { + return + {e.operands.map((o: any) => { + if(o instanceof QueryNodeElem) { + return + } + return props.renderLeaf(o); + })} + + } + + throw "Unsupported node element"; +} \ No newline at end of file diff --git a/client/src/lib/Query.tsx b/client/src/lib/Query.tsx new file mode 100644 index 0000000..f12578f --- /dev/null +++ b/client/src/lib/Query.tsx @@ -0,0 +1,63 @@ +export enum QueryLeafBy { + ArtistName = 0, + AlbumName, + TagName, + SongTitle +} + +export enum QueryLeafOp { + Equals = 0, + Like, +} + +export type QueryLeafOperand = string | number; + +export class QueryLeafElem { + a: QueryLeafBy; + op: QueryLeafOp; + b: QueryLeafOperand; + + constructor( + a: QueryLeafBy, + op: QueryLeafOp, + b: QueryLeafOperand + ) { + this.a = a; + this.op = op; + this.b = b; + } +}; + +export enum QueryNodeOp { + And = 0, + Or, +} + +export class QueryNodeElem { + operands: QueryElem[]; + op: QueryNodeOp; + + constructor( + operands: QueryElem[], + op: QueryNodeOp + ) { + this.operands = operands; + this.op = op; + } +} + +export function queryOr(...args: QueryElem[]) { + return new QueryNodeElem( + args, + QueryNodeOp.Or + ); +} + +export function queryAnd(...args: QueryElem[]) { + return new QueryNodeElem( + args, + QueryNodeOp.And + ); +} + +export type QueryElem = QueryLeafElem | QueryNodeElem; \ No newline at end of file diff --git a/client/src/types/DisplayItem.tsx b/client/src/types/DisplayItem.tsx deleted file mode 100644 index bfa76d0..0000000 --- a/client/src/types/DisplayItem.tsx +++ /dev/null @@ -1,44 +0,0 @@ -export interface SongDisplayItem { - title:String, - artistNames:String[], - tagNames:String[], - storeLinks: { - icon: JSX.Element, - url: String, - }[] -} - -export interface LoadingSongDisplayItem { - loadingSong: boolean, -} - -export interface ArtistDisplayItem { - name:String, - tagNames:String[], - storeLinks: { - icon: JSX.Element, - url: String, - }[] -} - -export interface LoadingArtistDisplayItem { - loadingArtist: boolean, -} - -export type DisplayItem = SongDisplayItem | LoadingSongDisplayItem | ArtistDisplayItem | LoadingArtistDisplayItem; - -export function isSong(item: DisplayItem): item is SongDisplayItem { - return "title" in item; -} - -export function isLoadingSong(item: DisplayItem): item is LoadingSongDisplayItem { - return "loadingSong" in item; -} - -export function isArtist(item: DisplayItem): item is ArtistDisplayItem { - return "name" in item; -} - -export function isLoadingArtist(item: DisplayItem): item is LoadingArtistDisplayItem { - return "loadingArtist" in item; -} \ No newline at end of file diff --git a/client/src/types/DragTypes.tsx b/client/src/types/DragTypes.tsx deleted file mode 100644 index e30b478..0000000 --- a/client/src/types/DragTypes.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export const dragTypes = { - ListItem: 'list item' -} \ No newline at end of file diff --git a/client/src/types/Query.tsx b/client/src/types/Query.tsx deleted file mode 100644 index b112b19..0000000 --- a/client/src/types/Query.tsx +++ /dev/null @@ -1,129 +0,0 @@ -import { QueryElemProperty, QueryFilterOp, QueryElemOp } from '../api'; - -export enum QueryKeys { - TitleLike = 'tl', - ArtistLike = 'al', - AndQuerySignature = 'and', - 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 { - [QueryKeys.TitleLike]: String -}; -export function isTitleQuery(q: Query): q is TitleQuery { - return QueryKeys.TitleLike in q; -} -export function TitleToApiQuery(q: TitleQuery) { - return { - 'prop': QueryElemProperty.songTitle, - 'propOperand': '%' + q[QueryKeys.TitleLike] + '%', - 'propOperator': QueryFilterOp.Like, - } -} - -export interface ArtistQuery { - [QueryKeys.ArtistLike]: String -}; -export function isArtistQuery(q: Query): q is ArtistQuery { - return QueryKeys.ArtistLike in q; -} -export function ArtistToApiQuery(q: ArtistQuery) { - return { - 'prop': QueryElemProperty.artistName, - 'propOperand': '%' + q[QueryKeys.ArtistLike] + '%', - 'propOperator': QueryFilterOp.Like, - } -} - -export interface AndQuery { - [QueryKeys.AndQuerySignature]: any, - [QueryKeys.OperandA]: T, - [QueryKeys.OperandB]: T, -} -export function isAndQuery(q: Query): q is AndQuery { - return QueryKeys.AndQuerySignature in q; -} -export function AndToApiQuery(q: AndQuery) { - return { - 'childrenOperator': QueryElemOp.And, - 'children': [ - toApiQuery(q.a), - toApiQuery(q.b), - ] - } -} - -export interface OrQuery { - [QueryKeys.OrQuerySignature]: any, - [QueryKeys.OperandA]: T, - [QueryKeys.OperandB]: T, -} -export function isOrQuery(q: Query): q is OrQuery { - return QueryKeys.OrQuerySignature in q; -} -export function OrToApiQuery(q: OrQuery) { - return { - 'childrenOperator': QueryElemOp.Or, - 'children': [ - toApiQuery(q.a), - toApiQuery(q.b), - ] - } -} - -export type Query = TitleQuery | ArtistQuery | AndQuery | OrQuery; - -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)); -} - -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 { - return (isTitleQuery(q) && TitleToApiQuery(q)) || - (isArtistQuery(q) && ArtistToApiQuery(q)) || - (isAndQuery(q) && AndToApiQuery(q)) || - (isOrQuery(q) && OrToApiQuery(q)) || - {}; -} \ No newline at end of file