From 9f16723798e70e9c8762723a6146760959138312 Mon Sep 17 00:00:00 2001 From: Sander Vocke Date: Fri, 4 Sep 2020 18:59:50 +0200 Subject: [PATCH 01/15] Initial mock. --- client/src/App.tsx | 8 +- client/src/components/Mockup.tsx | 147 +++++++++++++++++++++++++++++++ server/knexfile.ts | 2 +- 3 files changed, 155 insertions(+), 2 deletions(-) create mode 100644 client/src/components/Mockup.tsx diff --git a/client/src/App.tsx b/client/src/App.tsx index cfade88..4adecdb 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -14,6 +14,8 @@ import { useLocation, Redirect } from "react-router-dom"; +import { Typography } from '@material-ui/core'; +import Mockup from './components/Mockup'; const JSURL = require('jsurl'); @@ -121,11 +123,15 @@ function AppBody() { ); } +function AppMockup() { + return +} + function App() { return ( - + ); diff --git a/client/src/components/Mockup.tsx b/client/src/components/Mockup.tsx new file mode 100644 index 0000000..44e8bd6 --- /dev/null +++ b/client/src/components/Mockup.tsx @@ -0,0 +1,147 @@ +import React from 'react'; +import { makeStyles, createMuiTheme, ThemeProvider } from '@material-ui/core/styles'; +import Table from '@material-ui/core/Table'; +import TableBody from '@material-ui/core/TableBody'; +import TableCell from '@material-ui/core/TableCell'; +import TableContainer from '@material-ui/core/TableContainer'; +import TableHead from '@material-ui/core/TableHead'; +import TableRow from '@material-ui/core/TableRow'; +import Paper from '@material-ui/core/Paper'; +import Box from '@material-ui/core/Box'; +import SearchIcon from '@material-ui/icons/Search'; +import { IconButton, Button, Typography, Chip, Link, Card, CardContent, CssBaseline } from '@material-ui/core'; + +export function MockTable() { + + const rows = [ + ['No One Knows', "Queens of the Stone Age", "Blabla", "Tag"], + ['Ice cream sandwich', "My man", "What?", 37], + ['Eclair', "George Baker", "Bakeroo", 24], + ['Cupcake', "The Sweetie Pies", "Bla", "Bloo"], + ['Gingerbread', "Is ahead", "But you'll get", "Vury fet"], + ]; + + const useTableStyles = makeStyles({ + table: { + minWidth: 650, + }, + }); + const classes = useTableStyles(); + + return ( + + + + + Title + Artist + Album + Tags + + + + {rows.map((row) => ( + + {row[0]} + {row[1]} + {row[2]} + {row[3]} + + ))} + +
+
+ ); +} + +export function QueryContainer(props: any) { + return + {props.children} + +} + +export function QueryElem(props: any) { + return <> + + +} + +export function QueryBar() { + return + + + + + + + + + + + + By Queens of the Stone Age + + + + And + + + + Released before 2020 + + + + + + + + Or + + + + + Genre: "Awesome" + + + + +} + +function MockupBody() { + return <> + + + + + + + + + + + + + +} + + +const darkTheme = createMuiTheme({ + palette: { + type: 'dark' + }, +}); + +export default function Mockup() { + return <> + + + + + +} \ No newline at end of file diff --git a/server/knexfile.ts b/server/knexfile.ts index 18d26c5..1377c10 100644 --- a/server/knexfile.ts +++ b/server/knexfile.ts @@ -12,6 +12,6 @@ export default > { // In production, we base the config on an environment // variable setting. - production: JSON.parse(process.env.MUDBASE_DB_CONFIG || "") + production: JSON.parse(process.env.MUDBASE_DB_CONFIG || "{}") }; -- 2.36.1 From ca6239a4134e708bdfe3315a7bad6d92dadacb6e Mon Sep 17 00:00:00 2001 From: Sander Vocke Date: Sat, 5 Sep 2020 01:52:21 +0200 Subject: [PATCH 02/15] Add app bar with logo. --- client/public/logo.svg | 108 ++++++++++++++++++++++++++ client/src/components/Mockup.tsx | 18 +++-- resources/logo.svg | 108 ++++++++++++++++++++++++++ resources/logo_src.svg | 129 +++++++++++++++++++++++++++++++ 4 files changed, 355 insertions(+), 8 deletions(-) create mode 100644 client/public/logo.svg create mode 100644 resources/logo.svg create mode 100644 resources/logo_src.svg diff --git a/client/public/logo.svg b/client/public/logo.svg new file mode 100644 index 0000000..ddec38e --- /dev/null +++ b/client/public/logo.svg @@ -0,0 +1,108 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + diff --git a/client/src/components/Mockup.tsx b/client/src/components/Mockup.tsx index 44e8bd6..3afde9d 100644 --- a/client/src/components/Mockup.tsx +++ b/client/src/components/Mockup.tsx @@ -9,7 +9,7 @@ import TableRow from '@material-ui/core/TableRow'; import Paper from '@material-ui/core/Paper'; import Box from '@material-ui/core/Box'; import SearchIcon from '@material-ui/icons/Search'; -import { IconButton, Button, Typography, Chip, Link, Card, CardContent, CssBaseline } from '@material-ui/core'; +import { IconButton, Icon, Typography, Chip, Link, AppBar, CssBaseline } from '@material-ui/core'; export function MockTable() { @@ -82,9 +82,6 @@ export function QueryBar() { By Queens of the Stone Age - - And - Released before 2020 @@ -109,14 +106,19 @@ export function QueryBar() { function MockupBody() { return <> + + + + + - - - + {/* */} + + {/* */} - + diff --git a/resources/logo.svg b/resources/logo.svg new file mode 100644 index 0000000..ddec38e --- /dev/null +++ b/resources/logo.svg @@ -0,0 +1,108 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/logo_src.svg b/resources/logo_src.svg new file mode 100644 index 0000000..bce6312 --- /dev/null +++ b/resources/logo_src.svg @@ -0,0 +1,129 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + muDBase + + + + + + + + + + + + + + + -- 2.36.1 From fc25e1ba8dca303f8525fabd73d7901f0ed549e3 Mon Sep 17 00:00:00 2001 From: Sander Vocke Date: Tue, 8 Sep 2020 17:05:52 +0200 Subject: [PATCH 03/15] First mock-up. --- client/src/components/Mockup.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/components/Mockup.tsx b/client/src/components/Mockup.tsx index 3afde9d..d27f363 100644 --- a/client/src/components/Mockup.tsx +++ b/client/src/components/Mockup.tsx @@ -106,7 +106,7 @@ export function QueryBar() { function MockupBody() { return <> - + -- 2.36.1 From 4f3cd30e774341c6b19ee3c536a80143e0829857 Mon Sep 17 00:00:00 2001 From: Sander Vocke Date: Wed, 9 Sep 2020 13:35:54 +0200 Subject: [PATCH 04/15] 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 -- 2.36.1 From c197e23c04b0b13739f707467faf47cd2c13127e Mon Sep 17 00:00:00 2001 From: Sander Vocke Date: Thu, 10 Sep 2020 18:11:12 +0200 Subject: [PATCH 05/15] Add query button with basic menu. --- client/package-lock.json | 5 + client/package.json | 1 + client/src/components/Window.tsx | 4 +- .../components/querybuilder/QBQueryButton.tsx | 122 ++++++++++++++++++ .../components/querybuilder/QueryBuilder.tsx | 20 +++ 5 files changed, 150 insertions(+), 2 deletions(-) create mode 100644 client/src/components/querybuilder/QBQueryButton.tsx create mode 100644 client/src/components/querybuilder/QueryBuilder.tsx diff --git a/client/package-lock.json b/client/package-lock.json index bbec58c..5fbb9c8 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -8698,6 +8698,11 @@ "react-double-scrollbar": "0.0.15" } }, + "material-ui-nested-menu-item": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/material-ui-nested-menu-item/-/material-ui-nested-menu-item-1.0.2.tgz", + "integrity": "sha512-LZb8xI0FrAI/A3P2vT3CB9bmSoOFWOK0dikTc1t9VvEpp1a8hZkbVUz7VhETnoLUYu3NXCkgulmXcl3zitqI9A==" + }, "md5.js": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", diff --git a/client/package.json b/client/package.json index 3fe72d3..fec4ea1 100644 --- a/client/package.json +++ b/client/package.json @@ -18,6 +18,7 @@ "jsurl": "^0.1.5", "lodash": "^4.17.20", "material-table": "^1.69.0", + "material-ui-nested-menu-item": "^1.0.2", "react": "^16.13.1", "react-dnd": "^11.1.3", "react-dnd-html5-backend": "^11.1.3", diff --git a/client/src/components/Window.tsx b/client/src/components/Window.tsx index f241ca6..31842be 100644 --- a/client/src/components/Window.tsx +++ b/client/src/components/Window.tsx @@ -1,7 +1,7 @@ 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'; +import QueryBuilder from './querybuilder/QueryBuilder'; const darkTheme = createMuiTheme({ palette: { @@ -32,6 +32,6 @@ export default function Window(props: any) { return - + } \ No newline at end of file diff --git a/client/src/components/querybuilder/QBQueryButton.tsx b/client/src/components/querybuilder/QBQueryButton.tsx new file mode 100644 index 0000000..e1a1246 --- /dev/null +++ b/client/src/components/querybuilder/QBQueryButton.tsx @@ -0,0 +1,122 @@ +import React, { useState } from 'react'; +import { IconButton, Menu, MenuItem, TextField, Box } from '@material-ui/core'; +import SearchIcon from '@material-ui/icons/Search'; +import NestedMenuItem from "material-ui-nested-menu-item"; +import { QueryElem, QueryLeafBy, QueryLeafOp } from '../../lib/Query'; +import Autocomplete from '@material-ui/lab/Autocomplete' + +export function QBQueryButtonBase(props: any) { + return + + +} + +export interface FreeSoloInputProps { + label: string, + options: string[], + onSubmit: (s: string, exactMatch: boolean) => void, +} + +export function QBMenuFreeSoloInput(props: FreeSoloInputProps) { + const [value, setValue] = useState(''); + + const onInputChange = (event: any, _val: any, reason: any) => { + if (reason === 'reset') { + // User selected a preset option. + props.onSubmit(_val, true); + } else { + setValue(_val); + } + } + + return { + if (e.key === 'Enter') { + // User submitted free-form value. + props.onSubmit(value, props.options.includes(value)); + } + }} + renderInput={(params: any) => } + /> +} + + +export interface MenuProps { + anchorEl: null | HTMLElement, + onClose: () => void, + onCreateQuery: (q: QueryElem) => void, +} + +export function QBQueryButtonMenu(props: MenuProps) { + let anchorEl = props.anchorEl; + let onClose = props.onClose; + + const artists = [ + 'Queens', + 'Muse', + 'Triggerfinger' + ]; + + return + + { + onClose(); + props.onCreateQuery({ + a: QueryLeafBy.ArtistName, + op: exact ? QueryLeafOp.Equals : QueryLeafOp.Like, + b: s + }); + }} + /> + + +} + +export interface IProps { + +} + +export default function QBQueryButton(props: IProps) { + const [anchorEl, setAnchorEl] = React.useState(null); + const onOpen = (e: React.MouseEvent) => { + setAnchorEl(e.currentTarget); + } + const onClose = () => { + setAnchorEl(null); + } + const onCreateQuery = (q: QueryElem) => { + console.log("created query:", q); + } + + return <> + + + ; +} \ No newline at end of file diff --git a/client/src/components/querybuilder/QueryBuilder.tsx b/client/src/components/querybuilder/QueryBuilder.tsx new file mode 100644 index 0000000..2b43124 --- /dev/null +++ b/client/src/components/querybuilder/QueryBuilder.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { Box } from '@material-ui/core'; +import QBQueryButton from './QBQueryButton'; +import { QBQueryElem } from './QBQueryElem'; +import { QueryElem } from '../../lib/Query'; + +export interface IProps { + query: QueryElem +} + +export default function QueryBuilder(props: IProps) { + return + + + + + + + +} \ No newline at end of file -- 2.36.1 From e3d741d46638a9c21baa0ca5fd1251352febf046 Mon Sep 17 00:00:00 2001 From: Sander Vocke Date: Fri, 11 Sep 2020 17:37:46 +0200 Subject: [PATCH 06/15] Almost done with basic query manipulation. --- client/src/components/Window.tsx | 27 +--- .../components/querybuilder/QBAndBlock.tsx | 4 +- .../src/components/querybuilder/QBOrBlock.tsx | 15 +- .../components/querybuilder/QBQueryButton.tsx | 123 +-------------- .../components/querybuilder/QBQueryElem.tsx | 19 ++- .../querybuilder/QBQueryElemMenu.tsx | 108 +++++++++++++ .../querybuilder/QBQueryLeafElem.tsx | 10 +- .../querybuilder/QBQueryNodeElem.tsx | 47 +++--- .../querybuilder/QBQueryPlaceholder.tsx | 38 +++++ .../components/querybuilder/QueryBuilder.tsx | 40 +++-- client/src/lib/Query.tsx | 144 ++++++++++++++---- 11 files changed, 360 insertions(+), 215 deletions(-) create mode 100644 client/src/components/querybuilder/QBQueryElemMenu.tsx create mode 100644 client/src/components/querybuilder/QBQueryPlaceholder.tsx diff --git a/client/src/components/Window.tsx b/client/src/components/Window.tsx index 31842be..49b0bc7 100644 --- a/client/src/components/Window.tsx +++ b/client/src/components/Window.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState } from 'react'; import { ThemeProvider, CssBaseline, createMuiTheme } from '@material-ui/core'; import { QueryLeafElem, QueryLeafBy, QueryLeafOp, QueryElem, QueryNodeElem, queryOr, queryAnd } from '../lib/Query'; import QueryBuilder from './querybuilder/QueryBuilder'; @@ -10,28 +10,13 @@ const darkTheme = createMuiTheme({ }); 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 - ); + const [query, setQuery] = useState(null); return - + } \ No newline at end of file diff --git a/client/src/components/querybuilder/QBAndBlock.tsx b/client/src/components/querybuilder/QBAndBlock.tsx index 402a326..398517d 100644 --- a/client/src/components/querybuilder/QBAndBlock.tsx +++ b/client/src/components/querybuilder/QBAndBlock.tsx @@ -5,8 +5,8 @@ export default function QBAndBlock(props: any) { return - {props.children.map((child: any) => { - return + {props.children.map((child: any, idx: number) => { + return {child} })} diff --git a/client/src/components/querybuilder/QBOrBlock.tsx b/client/src/components/querybuilder/QBOrBlock.tsx index deb09a9..e25b575 100644 --- a/client/src/components/querybuilder/QBOrBlock.tsx +++ b/client/src/components/querybuilder/QBOrBlock.tsx @@ -1,6 +1,10 @@ import React from 'react'; import { Box, Typography } from '@material-ui/core'; +export interface IProps { + children: any, +} + export default function QBOrBlock(props: any) { const firstChild = Array.isArray(props.children) && props.children.length >= 1 ? props.children[0] : undefined; @@ -8,19 +12,22 @@ export default function QBOrBlock(props: any) { const otherChildren = Array.isArray(props.children) && props.children.length > 1 ? props.children.slice(1) : []; - return + return {firstChild} - {otherChildren.map((child: any) => { - return <> + {otherChildren.map((child: any, idx: number) => { + return Or {child} - ; + ; })} } \ No newline at end of file diff --git a/client/src/components/querybuilder/QBQueryButton.tsx b/client/src/components/querybuilder/QBQueryButton.tsx index e1a1246..91430eb 100644 --- a/client/src/components/querybuilder/QBQueryButton.tsx +++ b/client/src/components/querybuilder/QBQueryButton.tsx @@ -1,122 +1,15 @@ import React, { useState } from 'react'; -import { IconButton, Menu, MenuItem, TextField, Box } from '@material-ui/core'; +import { IconButton } from '@material-ui/core'; import SearchIcon from '@material-ui/icons/Search'; -import NestedMenuItem from "material-ui-nested-menu-item"; -import { QueryElem, QueryLeafBy, QueryLeafOp } from '../../lib/Query'; -import Autocomplete from '@material-ui/lab/Autocomplete' - -export function QBQueryButtonBase(props: any) { - return - - -} - -export interface FreeSoloInputProps { - label: string, - options: string[], - onSubmit: (s: string, exactMatch: boolean) => void, -} - -export function QBMenuFreeSoloInput(props: FreeSoloInputProps) { - const [value, setValue] = useState(''); - - const onInputChange = (event: any, _val: any, reason: any) => { - if (reason === 'reset') { - // User selected a preset option. - props.onSubmit(_val, true); - } else { - setValue(_val); - } - } - - return { - if (e.key === 'Enter') { - // User submitted free-form value. - props.onSubmit(value, props.options.includes(value)); - } - }} - renderInput={(params: any) => } - /> -} - - -export interface MenuProps { - anchorEl: null | HTMLElement, - onClose: () => void, - onCreateQuery: (q: QueryElem) => void, -} - -export function QBQueryButtonMenu(props: MenuProps) { - let anchorEl = props.anchorEl; - let onClose = props.onClose; - - const artists = [ - 'Queens', - 'Muse', - 'Triggerfinger' - ]; - - return - - { - onClose(); - props.onCreateQuery({ - a: QueryLeafBy.ArtistName, - op: exact ? QueryLeafOp.Equals : QueryLeafOp.Like, - b: s - }); - }} - /> - - -} +import CheckIcon from '@material-ui/icons/Check'; export interface IProps { - + editing: boolean } -export default function QBQueryButton(props: IProps) { - const [anchorEl, setAnchorEl] = React.useState(null); - const onOpen = (e: React.MouseEvent) => { - setAnchorEl(e.currentTarget); - } - const onClose = () => { - setAnchorEl(null); - } - const onCreateQuery = (q: QueryElem) => { - console.log("created query:", q); - } - - return <> - - - ; +export default function QBQueryButton(props: any) { + return + {(!props.editing) && } + {(props.editing) && } + } \ No newline at end of file diff --git a/client/src/components/querybuilder/QBQueryElem.tsx b/client/src/components/querybuilder/QBQueryElem.tsx index 1e71706..2478610 100644 --- a/client/src/components/querybuilder/QBQueryElem.tsx +++ b/client/src/components/querybuilder/QBQueryElem.tsx @@ -1,24 +1,23 @@ import React from 'react'; -import { QueryLeafElem, QueryNodeElem } from '../../lib/Query'; +import { QueryLeafElem, QueryNodeElem, QueryElem, isLeafElem, isNodeElem } from '../../lib/Query'; import { QBQueryLeafElem } from './QBQueryLeafElem'; import { QBQueryNodeElem } from './QBQueryNodeElem'; export interface IProps { elem: QueryLeafElem | QueryNodeElem, + onReplace: (q: QueryElem) => void, } 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 + if (isLeafElem(e)) { + return + } else if (isNodeElem(e)) { + return } throw "Unsupported query element"; diff --git a/client/src/components/querybuilder/QBQueryElemMenu.tsx b/client/src/components/querybuilder/QBQueryElemMenu.tsx new file mode 100644 index 0000000..9d8ed8c --- /dev/null +++ b/client/src/components/querybuilder/QBQueryElemMenu.tsx @@ -0,0 +1,108 @@ +import React, { useState } from 'react'; +import { Menu, TextField } from '@material-ui/core'; +import NestedMenuItem from "material-ui-nested-menu-item"; +import { QueryElem, QueryLeafBy, QueryLeafOp } from '../../lib/Query'; +import Autocomplete from '@material-ui/lab/Autocomplete' + +export interface FreeSoloInputProps { + label: string, + options: string[], + onSubmit: (s: string, exactMatch: boolean) => void, +} + +export function QBMenuFreeSoloInput(props: FreeSoloInputProps) { + const [value, setValue] = useState(''); + + const onInputChange = (event: any, _val: any, reason: any) => { + if (reason === 'reset') { + // User selected a preset option. + props.onSubmit(_val, true); + } else { + setValue(_val); + } + } + + return { + if (e.key === 'Enter') { + // User submitted free-form value. + props.onSubmit(value, props.options.includes(value)); + } + }} + renderInput={(params: any) => } + /> +} + + +export interface MenuProps { + anchorEl: null | HTMLElement, + onClose: () => void, + onCreateQuery: (q: QueryElem) => void, +} + +export function QBQueryElemMenu(props: MenuProps) { + let anchorEl = props.anchorEl; + let onClose = props.onClose; + + const artists = [ + 'Queens', + 'Muse', + 'Triggerfinger' + ]; + + const songs = [ + 'Drop It Like Its Hot', + 'Bla', + 'Stuff' + ]; + + return + + { + onClose(); + props.onCreateQuery({ + a: QueryLeafBy.SongTitle, + leafOp: exact ? QueryLeafOp.Equals : QueryLeafOp.Like, + b: s + }); + }} + /> + + + { + onClose(); + props.onCreateQuery({ + a: QueryLeafBy.ArtistName, + leafOp: exact ? QueryLeafOp.Equals : QueryLeafOp.Like, + b: s + }); + }} + /> + + +} diff --git a/client/src/components/querybuilder/QBQueryLeafElem.tsx b/client/src/components/querybuilder/QBQueryLeafElem.tsx index 24dac07..d1625ae 100644 --- a/client/src/components/querybuilder/QBQueryLeafElem.tsx +++ b/client/src/components/querybuilder/QBQueryLeafElem.tsx @@ -1,9 +1,11 @@ import React from 'react'; -import { QueryLeafElem, QueryLeafBy, QueryLeafOp, QueryNodeElem, QueryNodeOp } from '../../lib/Query'; +import { QueryLeafElem, QueryLeafBy, QueryLeafOp, QueryElem } from '../../lib/Query'; import { Chip, Typography } from '@material-ui/core'; +import { QBQueryPlaceholder } from './QBQueryPlaceholder'; export interface LeafProps { elem: QueryLeafElem + onReplace: (q: QueryElem) => void } export function QBQueryElemArtistEquals(props: LeafProps) { @@ -20,9 +22,13 @@ export function QBQueryLeafElem(props: LeafProps) { let e = props.elem; if (e.a == QueryLeafBy.ArtistName && - e.op == QueryLeafOp.Equals && + e.leafOp == QueryLeafOp.Equals && typeof e.b == "string") { return + } else if (e.leafOp == QueryLeafOp.Placeholder) { + return } throw "Unsupported leaf element"; diff --git a/client/src/components/querybuilder/QBQueryNodeElem.tsx b/client/src/components/querybuilder/QBQueryNodeElem.tsx index a5a5614..6df3205 100644 --- a/client/src/components/querybuilder/QBQueryNodeElem.tsx +++ b/client/src/components/querybuilder/QBQueryNodeElem.tsx @@ -1,34 +1,41 @@ import React from 'react'; import QBOrBlock from './QBOrBlock'; import QBAndBlock from './QBAndBlock'; -import { QueryNodeElem, QueryNodeOp } from '../../lib/Query'; +import { QueryNodeElem, QueryNodeOp, QueryElem, isNodeElem } from '../../lib/Query'; +import { QBQueryLeafElem } from './QBQueryLeafElem'; export interface NodeProps { elem: QueryNodeElem, - renderLeaf: (leaf: any) => any, + onReplace: (q: QueryElem) => void, } 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); - })} - + const onReplace = (idx: number, q: QueryElem) => { + var ops = e.operands; + ops[idx] = q; + let newNode = { operands: ops, nodeOp: e.nodeOp }; + props.onReplace(newNode); + } + + const children = e.operands.map((o: any, idx: number) => { + if (isNodeElem(o)) { + return onReplace(idx, q)} + /> + } + return onReplace(idx, q)} + /> + }); + + if (e.nodeOp == QueryNodeOp.And) { + return {children} + } else if (e.nodeOp == QueryNodeOp.Or) { + return {children} } throw "Unsupported node element"; diff --git a/client/src/components/querybuilder/QBQueryPlaceholder.tsx b/client/src/components/querybuilder/QBQueryPlaceholder.tsx new file mode 100644 index 0000000..23ea5f7 --- /dev/null +++ b/client/src/components/querybuilder/QBQueryPlaceholder.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { Chip } from '@material-ui/core'; +import { QBQueryElemMenu } from './QBQueryElemMenu'; +import { QueryElem } from '../../lib/Query'; + +export interface IProps { + onReplace: (q: QueryElem) => void, +} + +export function QBQueryPlaceholder(props: IProps) { + const [anchorEl, setAnchorEl] = React.useState(null); + + const onOpen = (event: any) => { + setAnchorEl(event.currentTarget); + }; + const onClose = () => { + setAnchorEl(null); + }; + const onCreate = (q: QueryElem) => { + props.onReplace(q); + }; + + return <> + + + +} \ No newline at end of file diff --git a/client/src/components/querybuilder/QueryBuilder.tsx b/client/src/components/querybuilder/QueryBuilder.tsx index 2b43124..f8574e6 100644 --- a/client/src/components/querybuilder/QueryBuilder.tsx +++ b/client/src/components/querybuilder/QueryBuilder.tsx @@ -1,20 +1,40 @@ -import React from 'react'; +import React, { useState } from 'react'; import { Box } from '@material-ui/core'; import QBQueryButton from './QBQueryButton'; import { QBQueryElem } from './QBQueryElem'; -import { QueryElem } from '../../lib/Query'; +import { QueryElem, addPlaceholders, removePlaceholders, simplify } from '../../lib/Query'; export interface IProps { - query: QueryElem + query: QueryElem | null, + onChangeQuery: (q: QueryElem | null) => void, } export default function QueryBuilder(props: IProps) { - return - - - - - + const [editing, setEditing] = useState(false); + + const simpleQuery = simplify(props.query); + const showQuery = editing ? + addPlaceholders(simpleQuery, null) : simpleQuery; + + const onReplace = (q: any) => { + const newQ = removePlaceholders(q); + props.onChangeQuery(newQ); + } + + return <> + + + setEditing(!editing)} + editing={editing} + /> + + + {showQuery && } + - + } \ No newline at end of file diff --git a/client/src/lib/Query.tsx b/client/src/lib/Query.tsx index f12578f..117c6da 100644 --- a/client/src/lib/Query.tsx +++ b/client/src/lib/Query.tsx @@ -1,3 +1,5 @@ +import { inflateRawSync } from "zlib"; + export enum QueryLeafBy { ArtistName = 0, AlbumName, @@ -8,56 +10,136 @@ export enum QueryLeafBy { export enum QueryLeafOp { Equals = 0, Like, + Placeholder, // Special op which indicates that this leaf is not filled in yet. } export type QueryLeafOperand = string | number; -export class QueryLeafElem { +export interface QueryLeafElem { a: QueryLeafBy; - op: QueryLeafOp; + leafOp: QueryLeafOp; b: QueryLeafOperand; - - constructor( - a: QueryLeafBy, - op: QueryLeafOp, - b: QueryLeafOperand - ) { - this.a = a; - this.op = op; - this.b = b; - } }; +export function isLeafElem(q: QueryElem): q is QueryLeafElem { + return 'leafOp' in q; +} export enum QueryNodeOp { And = 0, Or, } -export class QueryNodeElem { +export interface QueryNodeElem { operands: QueryElem[]; - op: QueryNodeOp; - - constructor( - operands: QueryElem[], - op: QueryNodeOp - ) { - this.operands = operands; - this.op = op; - } + nodeOp: QueryNodeOp; +} +export function isNodeElem(q: QueryElem): q is QueryNodeElem { + return 'nodeOp' in q; } + export function queryOr(...args: QueryElem[]) { - return new QueryNodeElem( - args, - QueryNodeOp.Or - ); + return { + operands: args, + nodeOp: QueryNodeOp.Or + } } export function queryAnd(...args: QueryElem[]) { - return new QueryNodeElem( - args, - QueryNodeOp.And - ); + return { + operands: args, + nodeOp: QueryNodeOp.And + }; +} + +export type QueryElem = QueryLeafElem | QueryNodeElem; + +// Take a query and add placeholders. The placeholders are empty +// leaves. They should be placed so that all possible node combinations +// from the existing nodes could have an added combinational leaf. +// In other words: for AND/OR, this should result in a query that has +// placeholders for all AND/OR combinations with existing nodes. +export function addPlaceholders( + q: QueryElem | null, + inNode: null | QueryNodeOp.And | QueryNodeOp.Or, +): QueryElem { + + const makePlaceholder = () => { + return { + a: 0, + leafOp: QueryLeafOp.Placeholder, + b: "" + } + }; + + const otherOp: Record = { + [QueryNodeOp.And]: QueryNodeOp.Or, + [QueryNodeOp.Or]: QueryNodeOp.And, + } + + if (q == null) { + return makePlaceholder(); + } else if (isNodeElem(q)) { + var operands = q.operands.map((op: any, idx: number) => { + return addPlaceholders(op, q.nodeOp); + }); + operands.push(makePlaceholder()); + const newBlock = { operands: operands, nodeOp: q.nodeOp }; + + if (inNode == null) { + return { operands: [newBlock, makePlaceholder()], nodeOp: otherOp[q.nodeOp] }; + } else { + return newBlock; + } + } else if (isLeafElem(q) && + q.leafOp != QueryLeafOp.Placeholder && + inNode !== null) { + return { operands: [q, makePlaceholder()], nodeOp: otherOp[inNode] }; + } + + return q; } -export type QueryElem = QueryLeafElem | QueryNodeElem; \ No newline at end of file +// See addPlaceholders. +export function removePlaceholders(q: QueryElem | null): QueryElem | null { + if (q && isNodeElem(q)) { + var newOperands: QueryElem[] = []; + + q.operands.forEach((op: any) => { + if (isLeafElem(op) && op.leafOp == QueryLeafOp.Placeholder) { + return; + } + const newOp = removePlaceholders(op); + if (newOp) { + newOperands.push(newOp); + } + }) + + if (newOperands.length == 0) { + return null; + } + if (newOperands.length == 1) { + return newOperands[0]; + } + return { operands: newOperands, nodeOp: q.nodeOp }; + } else if (q && isLeafElem(q) && q.leafOp == QueryLeafOp.Placeholder) { + return null; + } + + return q; +} + +export function simplify(q: QueryElem | null): QueryElem | null { + if (q && isNodeElem(q)) { + var newOperands: QueryElem[] = []; + q.operands.forEach((o: QueryElem) => { + const s = simplify(o); + if (s !== null) { newOperands.push(s); } + }) + if (newOperands.length === 0) { return null; } + if (newOperands.length === 1) { return newOperands[0]; } + return { operands: newOperands, nodeOp: q.nodeOp }; + } + + return q; +} \ No newline at end of file -- 2.36.1 From 475cd2cf2f33d246be2bdbb8d1ac712780c6e85f Mon Sep 17 00:00:00 2001 From: Sander Vocke Date: Sat, 12 Sep 2020 16:46:34 +0200 Subject: [PATCH 07/15] Songs now also supported, fixed bug --- client/src/components/Window.tsx | 4 +- .../querybuilder/QBQueryLeafElem.tsx | 37 ++++++++++++++++--- client/src/lib/Query.tsx | 9 +++++ 3 files changed, 44 insertions(+), 6 deletions(-) diff --git a/client/src/components/Window.tsx b/client/src/components/Window.tsx index 49b0bc7..e022e92 100644 --- a/client/src/components/Window.tsx +++ b/client/src/components/Window.tsx @@ -1,6 +1,6 @@ import React, { useState } from 'react'; import { ThemeProvider, CssBaseline, createMuiTheme } from '@material-ui/core'; -import { QueryLeafElem, QueryLeafBy, QueryLeafOp, QueryElem, QueryNodeElem, queryOr, queryAnd } from '../lib/Query'; +import { QueryElem } from '../lib/Query'; import QueryBuilder from './querybuilder/QueryBuilder'; const darkTheme = createMuiTheme({ @@ -12,6 +12,8 @@ const darkTheme = createMuiTheme({ export default function Window(props: any) { const [query, setQuery] = useState(null); + console.log("Query:", query); + return {props.label} + return +} + export interface LeafProps { elem: QueryLeafElem onReplace: (q: QueryElem) => void } export function QBQueryElemArtistEquals(props: LeafProps) { - let e = props.elem; + return +} - const label = - By {e.b} - ; +export function QBQueryElemArtistLike(props: LeafProps) { + return +} - return +export function QBQueryElemTitleEquals(props: LeafProps) { + return +} + +export function QBQueryElemTitleLike(props: LeafProps) { + return } export function QBQueryLeafElem(props: LeafProps) { @@ -25,6 +40,18 @@ export function QBQueryLeafElem(props: LeafProps) { e.leafOp == QueryLeafOp.Equals && typeof e.b == "string") { return + } else if (e.a == QueryLeafBy.ArtistName && + e.leafOp == QueryLeafOp.Like && + typeof e.b == "string") { + return + } if (e.a == QueryLeafBy.SongTitle && + e.leafOp == QueryLeafOp.Equals && + typeof e.b == "string") { + return + } else if (e.a == QueryLeafBy.SongTitle && + e.leafOp == QueryLeafOp.Like && + typeof e.b == "string") { + return } else if (e.leafOp == QueryLeafOp.Placeholder) { return Date: Mon, 14 Sep 2020 09:31:49 +0200 Subject: [PATCH 08/15] Deleting query elements works. --- client/src/components/Window.tsx | 2 +- .../components/querybuilder/QBQueryElem.tsx | 12 ++- .../querybuilder/QBQueryElemMenu.tsx | 2 +- .../querybuilder/QBQueryLeafElem.tsx | 81 +++++++++++++++---- .../querybuilder/QBQueryNodeElem.tsx | 28 ++++--- .../querybuilder/QBQueryPlaceholder.tsx | 2 +- .../components/querybuilder/QueryBuilder.tsx | 3 +- client/src/lib/{ => query}/Query.tsx | 0 8 files changed, 95 insertions(+), 35 deletions(-) rename client/src/lib/{ => query}/Query.tsx (100%) diff --git a/client/src/components/Window.tsx b/client/src/components/Window.tsx index e022e92..61c0678 100644 --- a/client/src/components/Window.tsx +++ b/client/src/components/Window.tsx @@ -1,6 +1,6 @@ import React, { useState } from 'react'; import { ThemeProvider, CssBaseline, createMuiTheme } from '@material-ui/core'; -import { QueryElem } from '../lib/Query'; +import { QueryElem } from '../lib/query/Query'; import QueryBuilder from './querybuilder/QueryBuilder'; const darkTheme = createMuiTheme({ diff --git a/client/src/components/querybuilder/QBQueryElem.tsx b/client/src/components/querybuilder/QBQueryElem.tsx index 2478610..1655ec8 100644 --- a/client/src/components/querybuilder/QBQueryElem.tsx +++ b/client/src/components/querybuilder/QBQueryElem.tsx @@ -1,22 +1,28 @@ import React from 'react'; -import { QueryLeafElem, QueryNodeElem, QueryElem, isLeafElem, isNodeElem } from '../../lib/Query'; +import { QueryLeafElem, QueryNodeElem, QueryElem, isLeafElem, isNodeElem } from '../../lib/query/Query'; import { QBQueryLeafElem } from './QBQueryLeafElem'; import { QBQueryNodeElem } from './QBQueryNodeElem'; export interface IProps { elem: QueryLeafElem | QueryNodeElem, - onReplace: (q: QueryElem) => void, + onReplace: (q: QueryElem | null) => void, + editingQuery: boolean, } export function QBQueryElem(props: IProps) { let e = props.elem; if (isLeafElem(e)) { - return + return } else if (isNodeElem(e)) { return } diff --git a/client/src/components/querybuilder/QBQueryElemMenu.tsx b/client/src/components/querybuilder/QBQueryElemMenu.tsx index 9d8ed8c..75d2167 100644 --- a/client/src/components/querybuilder/QBQueryElemMenu.tsx +++ b/client/src/components/querybuilder/QBQueryElemMenu.tsx @@ -1,7 +1,7 @@ import React, { useState } from 'react'; import { Menu, TextField } from '@material-ui/core'; import NestedMenuItem from "material-ui-nested-menu-item"; -import { QueryElem, QueryLeafBy, QueryLeafOp } from '../../lib/Query'; +import { QueryElem, QueryLeafBy, QueryLeafOp } from '../../lib/query/Query'; import Autocomplete from '@material-ui/lab/Autocomplete' export interface FreeSoloInputProps { diff --git a/client/src/components/querybuilder/QBQueryLeafElem.tsx b/client/src/components/querybuilder/QBQueryLeafElem.tsx index c53f5d3..01f83cb 100644 --- a/client/src/components/querybuilder/QBQueryLeafElem.tsx +++ b/client/src/components/querybuilder/QBQueryLeafElem.tsx @@ -1,57 +1,108 @@ import React from 'react'; -import { QueryLeafElem, QueryLeafBy, QueryLeafOp, QueryElem } from '../../lib/Query'; -import { Chip, Typography } from '@material-ui/core'; +import { QueryLeafElem, QueryLeafBy, QueryLeafOp, QueryElem } from '../../lib/query/Query'; +import { Chip, Typography, IconButton, Box } from '@material-ui/core'; import { QBQueryPlaceholder } from './QBQueryPlaceholder'; +import CloseIcon from '@material-ui/icons/Close'; export interface ElemChipProps { - label: any + label: any, + extraElements?: any, } export function LabeledElemChip(props: ElemChipProps) { - const label = {props.label} + const label = + {props.label} + {props.extraElements} + return } export interface LeafProps { - elem: QueryLeafElem - onReplace: (q: QueryElem) => void + elem: QueryLeafElem, + onReplace: (q: QueryElem) => void, + extraElements?: any, } export function QBQueryElemArtistEquals(props: LeafProps) { - return + return } export function QBQueryElemArtistLike(props: LeafProps) { - return + return } export function QBQueryElemTitleEquals(props: LeafProps) { - return + return } export function QBQueryElemTitleLike(props: LeafProps) { - return + return } -export function QBQueryLeafElem(props: LeafProps) { +export interface DeleteButtonProps { + onClick?: (e: any) => void, +} + +export function QBQueryElemDeleteButton(props: DeleteButtonProps) { + return + + +} + +export interface IProps { + elem: QueryLeafElem, + onReplace: (q: QueryElem | null) => void, + editingQuery: boolean, +} + +export function QBQueryLeafElem(props: IProps) { let e = props.elem; + const extraElements = props.editingQuery ? + props.onReplace(null)} + /> + : undefined; + if (e.a == QueryLeafBy.ArtistName && e.leafOp == QueryLeafOp.Equals && typeof e.b == "string") { - return + return } else if (e.a == QueryLeafBy.ArtistName && e.leafOp == QueryLeafOp.Like && typeof e.b == "string") { - return + return } if (e.a == QueryLeafBy.SongTitle && e.leafOp == QueryLeafOp.Equals && typeof e.b == "string") { - return + return } else if (e.a == QueryLeafBy.SongTitle && e.leafOp == QueryLeafOp.Like && typeof e.b == "string") { - return + return } else if (e.leafOp == QueryLeafOp.Placeholder) { return void, + onReplace: (q: QueryElem | null) => void, + editingQuery: boolean, } export function QBQueryNodeElem(props: NodeProps) { let e = props.elem; - const onReplace = (idx: number, q: QueryElem) => { + const onReplace = (idx: number, q: QueryElem | null) => { var ops = e.operands; - ops[idx] = q; - let newNode = { operands: ops, nodeOp: e.nodeOp }; + if (q) { + ops[idx] = q; + } else { + ops.splice(idx, 1); + } + let newNode = simplify({ operands: ops, nodeOp: e.nodeOp }); props.onReplace(newNode); } const children = e.operands.map((o: any, idx: number) => { - if (isNodeElem(o)) { - return onReplace(idx, q)} - /> - } - return onReplace(idx, q)} + onReplace={(q: QueryElem | null) => onReplace(idx, q)} + editingQuery={props.editingQuery} /> }); diff --git a/client/src/components/querybuilder/QBQueryPlaceholder.tsx b/client/src/components/querybuilder/QBQueryPlaceholder.tsx index 23ea5f7..4497e41 100644 --- a/client/src/components/querybuilder/QBQueryPlaceholder.tsx +++ b/client/src/components/querybuilder/QBQueryPlaceholder.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { Chip } from '@material-ui/core'; import { QBQueryElemMenu } from './QBQueryElemMenu'; -import { QueryElem } from '../../lib/Query'; +import { QueryElem } from '../../lib/query/Query'; export interface IProps { onReplace: (q: QueryElem) => void, diff --git a/client/src/components/querybuilder/QueryBuilder.tsx b/client/src/components/querybuilder/QueryBuilder.tsx index f8574e6..f14fb9e 100644 --- a/client/src/components/querybuilder/QueryBuilder.tsx +++ b/client/src/components/querybuilder/QueryBuilder.tsx @@ -2,7 +2,7 @@ import React, { useState } from 'react'; import { Box } from '@material-ui/core'; import QBQueryButton from './QBQueryButton'; import { QBQueryElem } from './QBQueryElem'; -import { QueryElem, addPlaceholders, removePlaceholders, simplify } from '../../lib/Query'; +import { QueryElem, addPlaceholders, removePlaceholders, simplify } from '../../lib/query/Query'; export interface IProps { query: QueryElem | null, @@ -33,6 +33,7 @@ export default function QueryBuilder(props: IProps) { {showQuery && } diff --git a/client/src/lib/Query.tsx b/client/src/lib/query/Query.tsx similarity index 100% rename from client/src/lib/Query.tsx rename to client/src/lib/query/Query.tsx -- 2.36.1 From 9c9d1b6dd8766067d212bc1689629ae8b29eeafb Mon Sep 17 00:00:00 2001 From: Sander Vocke Date: Mon, 14 Sep 2020 13:20:52 +0200 Subject: [PATCH 09/15] Interactive queries seem to work! --- client/src/components/Mockup.tsx | 4 +- client/src/components/Window.tsx | 78 ++++++++++- .../components/querybuilder/QBAddElemMenu.tsx | 63 +++++++++ .../{QBQueryButton.tsx => QBEditButton.tsx} | 4 +- .../{QBQueryLeafElem.tsx => QBLeafElem.tsx} | 9 +- .../{QBQueryNodeElem.tsx => QBNodeElem.tsx} | 7 +- ...QueryPlaceholder.tsx => QBPlaceholder.tsx} | 9 +- .../components/querybuilder/QBQueryElem.tsx | 14 +- .../querybuilder/QBQueryElemMenu.tsx | 108 ---------------- .../querybuilder/QBSelectWithRequest.tsx | 121 ++++++++++++++++++ .../components/querybuilder/QueryBuilder.tsx | 10 +- client/src/lib/query/Query.tsx | 2 - client/tsconfig.json | 2 +- server/endpoints/QueryEndpointHandler.ts | 4 +- 14 files changed, 303 insertions(+), 132 deletions(-) create mode 100644 client/src/components/querybuilder/QBAddElemMenu.tsx rename client/src/components/querybuilder/{QBQueryButton.tsx => QBEditButton.tsx} (80%) rename client/src/components/querybuilder/{QBQueryLeafElem.tsx => QBLeafElem.tsx} (92%) rename client/src/components/querybuilder/{QBQueryNodeElem.tsx => QBNodeElem.tsx} (84%) rename client/src/components/querybuilder/{QBQueryPlaceholder.tsx => QBPlaceholder.tsx} (76%) delete mode 100644 client/src/components/querybuilder/QBQueryElemMenu.tsx create mode 100644 client/src/components/querybuilder/QBSelectWithRequest.tsx diff --git a/client/src/components/Mockup.tsx b/client/src/components/Mockup.tsx index d27f363..3d71382 100644 --- a/client/src/components/Mockup.tsx +++ b/client/src/components/Mockup.tsx @@ -9,7 +9,7 @@ import TableRow from '@material-ui/core/TableRow'; import Paper from '@material-ui/core/Paper'; import Box from '@material-ui/core/Box'; import SearchIcon from '@material-ui/icons/Search'; -import { IconButton, Icon, Typography, Chip, Link, AppBar, CssBaseline } from '@material-ui/core'; +import { IconButton, Typography, Chip, Link, AppBar, CssBaseline } from '@material-ui/core'; export function MockTable() { @@ -108,7 +108,7 @@ function MockupBody() { return <> - + error diff --git a/client/src/components/Window.tsx b/client/src/components/Window.tsx index 61c0678..0df4391 100644 --- a/client/src/components/Window.tsx +++ b/client/src/components/Window.tsx @@ -2,6 +2,8 @@ import React, { useState } from 'react'; import { ThemeProvider, CssBaseline, createMuiTheme } from '@material-ui/core'; import { QueryElem } from '../lib/query/Query'; import QueryBuilder from './querybuilder/QueryBuilder'; +import * as serverApi from '../api'; +import { queryAllByRole } from '@testing-library/react'; const darkTheme = createMuiTheme({ palette: { @@ -9,16 +11,88 @@ const darkTheme = createMuiTheme({ }, }); +export async function getArtists(filter: string) { + const query = filter.length > 0 ? { + prop: serverApi.QueryElemProperty.artistName, + propOperand: filter, + propOperator: serverApi.QueryFilterOp.Like, + } : {}; + + var q: serverApi.QueryRequest = { + query: query, + offsetsLimits: { + artistOffset: 0, + artistLimit: 100, + }, + ordering: { + orderBy: { + type: serverApi.OrderByType.Name, + }, + ascending: true, + }, + }; + + const requestOpts = { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(q), + }; + + return (async () => { + const response = await fetch((process.env.REACT_APP_BACKEND || "") + serverApi.QueryEndpoint, requestOpts) + let json: any = await response.json(); + const names: string[] = json.artists.map((elem: any) => { return elem.name; }); + return [...new Set(names)]; + })(); +} + +export async function getSongTitles(filter: string) { + const query = filter.length > 0 ? { + prop: serverApi.QueryElemProperty.songTitle, + propOperand: filter, + propOperator: serverApi.QueryFilterOp.Like, + } : {}; + + var q: serverApi.QueryRequest = { + query: query, + offsetsLimits: { + songOffset: 0, + songLimit: 100, + }, + ordering: { + orderBy: { + type: serverApi.OrderByType.Name, + }, + ascending: true, + }, + }; + + const requestOpts = { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(q), + }; + + return (async () => { + const response = await fetch((process.env.REACT_APP_BACKEND || "") + serverApi.QueryEndpoint, requestOpts) + let json: any = await response.json(); + const titles: string[] = json.songs.map((elem: any) => { return elem.title; }); + return [...new Set(titles)]; + })(); +} + export default function Window(props: any) { const [query, setQuery] = useState(null); - console.log("Query:", query); - return } \ No newline at end of file diff --git a/client/src/components/querybuilder/QBAddElemMenu.tsx b/client/src/components/querybuilder/QBAddElemMenu.tsx new file mode 100644 index 0000000..b0fc151 --- /dev/null +++ b/client/src/components/querybuilder/QBAddElemMenu.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import { Menu, MenuItem } from '@material-ui/core'; +import NestedMenuItem from "material-ui-nested-menu-item"; +import { QueryElem, QueryLeafBy, QueryLeafOp } from '../../lib/query/Query'; +import QBSelectWithRequest from './QBSelectWithRequest'; +import { Requests } from './QueryBuilder'; + +export interface MenuProps { + anchorEl: null | HTMLElement, + onClose: () => void, + onCreateQuery: (q: QueryElem) => void, + requestFunctions: Requests, +} + +export function QBAddElemMenu(props: MenuProps) { + let anchorEl = props.anchorEl; + let onClose = props.onClose; + + return + New query element + + { + onClose(); + props.onCreateQuery({ + a: QueryLeafBy.SongTitle, + leafOp: exact ? QueryLeafOp.Equals : QueryLeafOp.Like, + b: s + }); + }} + style={{ width: 300 }} + /> + + + { + onClose(); + props.onCreateQuery({ + a: QueryLeafBy.ArtistName, + leafOp: exact ? QueryLeafOp.Equals : QueryLeafOp.Like, + b: s + }); + }} + style={{ width: 300 }} + /> + + +} diff --git a/client/src/components/querybuilder/QBQueryButton.tsx b/client/src/components/querybuilder/QBEditButton.tsx similarity index 80% rename from client/src/components/querybuilder/QBQueryButton.tsx rename to client/src/components/querybuilder/QBEditButton.tsx index 91430eb..2692c29 100644 --- a/client/src/components/querybuilder/QBQueryButton.tsx +++ b/client/src/components/querybuilder/QBEditButton.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React from 'react'; import { IconButton } from '@material-ui/core'; import SearchIcon from '@material-ui/icons/Search'; import CheckIcon from '@material-ui/icons/Check'; @@ -7,7 +7,7 @@ export interface IProps { editing: boolean } -export default function QBQueryButton(props: any) { +export default function QBEditButton(props: any) { return {(!props.editing) && } {(props.editing) && } diff --git a/client/src/components/querybuilder/QBQueryLeafElem.tsx b/client/src/components/querybuilder/QBLeafElem.tsx similarity index 92% rename from client/src/components/querybuilder/QBQueryLeafElem.tsx rename to client/src/components/querybuilder/QBLeafElem.tsx index 01f83cb..59a5a94 100644 --- a/client/src/components/querybuilder/QBQueryLeafElem.tsx +++ b/client/src/components/querybuilder/QBLeafElem.tsx @@ -1,8 +1,9 @@ import React from 'react'; import { QueryLeafElem, QueryLeafBy, QueryLeafOp, QueryElem } from '../../lib/query/Query'; import { Chip, Typography, IconButton, Box } from '@material-ui/core'; -import { QBQueryPlaceholder } from './QBQueryPlaceholder'; +import { QBPlaceholder } from './QBPlaceholder'; import CloseIcon from '@material-ui/icons/Close'; +import { Requests } from './QueryBuilder'; export interface ElemChipProps { label: any, @@ -64,9 +65,10 @@ export interface IProps { elem: QueryLeafElem, onReplace: (q: QueryElem | null) => void, editingQuery: boolean, + requestFunctions: Requests, } -export function QBQueryLeafElem(props: IProps) { +export function QBLeafElem(props: IProps) { let e = props.elem; const extraElements = props.editingQuery ? @@ -104,8 +106,9 @@ export function QBQueryLeafElem(props: IProps) { extraElements={extraElements} /> } else if (e.leafOp == QueryLeafOp.Placeholder) { - return } diff --git a/client/src/components/querybuilder/QBQueryNodeElem.tsx b/client/src/components/querybuilder/QBNodeElem.tsx similarity index 84% rename from client/src/components/querybuilder/QBQueryNodeElem.tsx rename to client/src/components/querybuilder/QBNodeElem.tsx index 1d0855b..5551390 100644 --- a/client/src/components/querybuilder/QBQueryNodeElem.tsx +++ b/client/src/components/querybuilder/QBNodeElem.tsx @@ -2,17 +2,19 @@ import React from 'react'; import QBOrBlock from './QBOrBlock'; import QBAndBlock from './QBAndBlock'; import { QueryNodeElem, QueryNodeOp, QueryElem, isNodeElem, simplify } from '../../lib/query/Query'; -import { QBQueryLeafElem } from './QBQueryLeafElem'; +import { QBLeafElem } from './QBLeafElem'; import { QBQueryElem } from './QBQueryElem'; import { O_APPEND } from 'constants'; +import { Requests } from './QueryBuilder'; export interface NodeProps { elem: QueryNodeElem, onReplace: (q: QueryElem | null) => void, editingQuery: boolean, + requestFunctions: Requests, } -export function QBQueryNodeElem(props: NodeProps) { +export function QBNodeElem(props: NodeProps) { let e = props.elem; const onReplace = (idx: number, q: QueryElem | null) => { @@ -31,6 +33,7 @@ export function QBQueryNodeElem(props: NodeProps) { elem={o} onReplace={(q: QueryElem | null) => onReplace(idx, q)} editingQuery={props.editingQuery} + requestFunctions={props.requestFunctions} /> }); diff --git a/client/src/components/querybuilder/QBQueryPlaceholder.tsx b/client/src/components/querybuilder/QBPlaceholder.tsx similarity index 76% rename from client/src/components/querybuilder/QBQueryPlaceholder.tsx rename to client/src/components/querybuilder/QBPlaceholder.tsx index 4497e41..00847c0 100644 --- a/client/src/components/querybuilder/QBQueryPlaceholder.tsx +++ b/client/src/components/querybuilder/QBPlaceholder.tsx @@ -1,13 +1,15 @@ import React from 'react'; import { Chip } from '@material-ui/core'; -import { QBQueryElemMenu } from './QBQueryElemMenu'; +import { QBAddElemMenu } from './QBAddElemMenu'; import { QueryElem } from '../../lib/query/Query'; +import { Requests } from './QueryBuilder'; export interface IProps { onReplace: (q: QueryElem) => void, + requestFunctions: Requests, } -export function QBQueryPlaceholder(props: IProps) { +export function QBPlaceholder(props: IProps & any) { const [anchorEl, setAnchorEl] = React.useState(null); const onOpen = (event: any) => { @@ -29,10 +31,11 @@ export function QBQueryPlaceholder(props: IProps) { onClick={onOpen} component="div" /> - } \ No newline at end of file diff --git a/client/src/components/querybuilder/QBQueryElem.tsx b/client/src/components/querybuilder/QBQueryElem.tsx index 1655ec8..8fafebf 100644 --- a/client/src/components/querybuilder/QBQueryElem.tsx +++ b/client/src/components/querybuilder/QBQueryElem.tsx @@ -1,30 +1,34 @@ import React from 'react'; import { QueryLeafElem, QueryNodeElem, QueryElem, isLeafElem, isNodeElem } from '../../lib/query/Query'; -import { QBQueryLeafElem } from './QBQueryLeafElem'; -import { QBQueryNodeElem } from './QBQueryNodeElem'; +import { QBLeafElem } from './QBLeafElem'; +import { QBNodeElem } from './QBNodeElem'; +import { Requests } from './QueryBuilder'; export interface IProps { elem: QueryLeafElem | QueryNodeElem, onReplace: (q: QueryElem | null) => void, editingQuery: boolean, + requestFunctions: Requests, } export function QBQueryElem(props: IProps) { let e = props.elem; if (isLeafElem(e)) { - return } else if (isNodeElem(e)) { - return } - throw "Unsupported query element"; + throw new Error("Unsupported query element"); } \ No newline at end of file diff --git a/client/src/components/querybuilder/QBQueryElemMenu.tsx b/client/src/components/querybuilder/QBQueryElemMenu.tsx deleted file mode 100644 index 75d2167..0000000 --- a/client/src/components/querybuilder/QBQueryElemMenu.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import React, { useState } from 'react'; -import { Menu, TextField } from '@material-ui/core'; -import NestedMenuItem from "material-ui-nested-menu-item"; -import { QueryElem, QueryLeafBy, QueryLeafOp } from '../../lib/query/Query'; -import Autocomplete from '@material-ui/lab/Autocomplete' - -export interface FreeSoloInputProps { - label: string, - options: string[], - onSubmit: (s: string, exactMatch: boolean) => void, -} - -export function QBMenuFreeSoloInput(props: FreeSoloInputProps) { - const [value, setValue] = useState(''); - - const onInputChange = (event: any, _val: any, reason: any) => { - if (reason === 'reset') { - // User selected a preset option. - props.onSubmit(_val, true); - } else { - setValue(_val); - } - } - - return { - if (e.key === 'Enter') { - // User submitted free-form value. - props.onSubmit(value, props.options.includes(value)); - } - }} - renderInput={(params: any) => } - /> -} - - -export interface MenuProps { - anchorEl: null | HTMLElement, - onClose: () => void, - onCreateQuery: (q: QueryElem) => void, -} - -export function QBQueryElemMenu(props: MenuProps) { - let anchorEl = props.anchorEl; - let onClose = props.onClose; - - const artists = [ - 'Queens', - 'Muse', - 'Triggerfinger' - ]; - - const songs = [ - 'Drop It Like Its Hot', - 'Bla', - 'Stuff' - ]; - - return - - { - onClose(); - props.onCreateQuery({ - a: QueryLeafBy.SongTitle, - leafOp: exact ? QueryLeafOp.Equals : QueryLeafOp.Like, - b: s - }); - }} - /> - - - { - onClose(); - props.onCreateQuery({ - a: QueryLeafBy.ArtistName, - leafOp: exact ? QueryLeafOp.Equals : QueryLeafOp.Like, - b: s - }); - }} - /> - - -} diff --git a/client/src/components/querybuilder/QBSelectWithRequest.tsx b/client/src/components/querybuilder/QBSelectWithRequest.tsx new file mode 100644 index 0000000..b4caac1 --- /dev/null +++ b/client/src/components/querybuilder/QBSelectWithRequest.tsx @@ -0,0 +1,121 @@ +import React, { useState, useEffect } from 'react'; +import TextField from '@material-ui/core/TextField'; +import Autocomplete from '@material-ui/lab/Autocomplete'; +import CircularProgress from '@material-ui/core/CircularProgress'; + +interface IProps { + getNewOptions: (textInput: string) => Promise, + label: string, + onSubmit: (s: string, exactMatch: boolean) => void, +} + +// Autocompleted combo box which can make asynchronous requests +// to get new options. +// Based on Material UI example: https://material-ui.com/components/autocomplete/ +export default function QBSelectWithRequest(props: IProps & any) { + interface OptionsFor { + forInput: string, + options: string[], + }; + + const [open, setOpen] = useState(false); + const [options, setOptions] = useState(null); + const [input, setInput] = useState(""); + + const { getNewOptions, label, onSubmit, ...restProps } = props; + + const loading: boolean = !options || options.forInput !== input; + + const updateOptions = (forInput: string, options: any[]) => { + if (forInput === input) { + console.log("setting options."); + setOptions({ + forInput: forInput, + options: options, + }); + } + } + + const startRequest = (_input: string) => { + console.log('starting req', _input); + setInput(_input); + (async () => { + const newOptions = await getNewOptions(_input); + console.log('new options', newOptions); + updateOptions(_input, newOptions); + })(); + }; + + // // Ensure a new request is made whenever the loading option is enabled. + // useEffect(() => { + // startRequest(input); + // }, []); + + // Ensure options are cleared whenever the element is closed. + // useEffect(() => { + // if (!open) { + // setOptions(null); + // } + // }, [open]); + + useEffect(() => { + startRequest(input); + }, [input]); + + const onInputChange = (e: any, val: any, reason: any) => { + if (reason === 'reset') { + // User selected a preset option. + props.onSubmit(val, true); + } else { + // User changed text, start a new request. + setInput(val); + } + } + + console.log("Render props:", props); + + return ( + { + setOpen(true); + }} + onClose={() => { + setOpen(false); + }} + getOptionSelected={(option, value) => option === value} + getOptionLabel={(option) => option} + options={options ? options.options : null} + loading={loading} + freeSolo={true} + value={input} + onInputChange={onInputChange} + onKeyDown={(e: any) => { + // Prevent the event from propagating, because + // that would trigger keyboard navigation of the menu. + e.stopPropagation(); + if (e.key === 'Enter') { + // User submitted free-form value. + props.onSubmit(input, options && options.options.includes(input)); + } + }} + renderInput={(params) => ( + + {loading ? : null} + {params.InputProps.endAdornment} + + ), + }} + /> + )} + /> + ); +} diff --git a/client/src/components/querybuilder/QueryBuilder.tsx b/client/src/components/querybuilder/QueryBuilder.tsx index f14fb9e..4c0ad2f 100644 --- a/client/src/components/querybuilder/QueryBuilder.tsx +++ b/client/src/components/querybuilder/QueryBuilder.tsx @@ -1,12 +1,19 @@ import React, { useState } from 'react'; import { Box } from '@material-ui/core'; -import QBQueryButton from './QBQueryButton'; +import QBQueryButton from './QBEditButton'; import { QBQueryElem } from './QBQueryElem'; import { QueryElem, addPlaceholders, removePlaceholders, simplify } from '../../lib/query/Query'; + +export interface Requests { + getArtists: (filter: string) => Promise, + getSongTitles: (filter: string) => Promise, +} + export interface IProps { query: QueryElem | null, onChangeQuery: (q: QueryElem | null) => void, + requestFunctions: Requests, } export default function QueryBuilder(props: IProps) { @@ -34,6 +41,7 @@ export default function QueryBuilder(props: IProps) { elem={showQuery} onReplace={onReplace} editingQuery={editing} + requestFunctions={props.requestFunctions} />} diff --git a/client/src/lib/query/Query.tsx b/client/src/lib/query/Query.tsx index 0171e63..09197ca 100644 --- a/client/src/lib/query/Query.tsx +++ b/client/src/lib/query/Query.tsx @@ -1,5 +1,3 @@ -import { inflateRawSync } from "zlib"; - export enum QueryLeafBy { ArtistName = 0, AlbumName, diff --git a/client/tsconfig.json b/client/tsconfig.json index f2850b7..2254446 100644 --- a/client/tsconfig.json +++ b/client/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "es5", + "target": "es6", "lib": [ "dom", "dom.iterable", diff --git a/server/endpoints/QueryEndpointHandler.ts b/server/endpoints/QueryEndpointHandler.ts index 4707b32..7eedd3f 100644 --- a/server/endpoints/QueryEndpointHandler.ts +++ b/server/endpoints/QueryEndpointHandler.ts @@ -104,7 +104,9 @@ function addLeafWhere(knexQuery: any, queryElem: api.QueryElem, type: WhereType) if (!queryElem.propOperator) throw "Cannot create where clause without an operator."; const operator = queryElem.propOperator || api.QueryFilterOp.Eq; const a = queryElem.prop && propertyKeys[queryElem.prop]; - const b = queryElem.propOperand || ""; + const b = operator === api.QueryFilterOp.Like ? + '%' + (queryElem.propOperand || "") + '%' + : (queryElem.propOperand || ""); if (Object.keys(simpleLeafOps).includes(operator)) { if (type == WhereType.And) { -- 2.36.1 From ce2d814506087c8c142e3eb4a6d6ca32567e1ac3 Mon Sep 17 00:00:00 2001 From: Sander Vocke Date: Mon, 14 Sep 2020 15:50:31 +0200 Subject: [PATCH 10/15] Got basic queries in result table working. --- client/src/components/Window.tsx | 103 ++++++++++++++++-- .../components/querybuilder/QBLeafElem.tsx | 18 ++- client/src/components/tables/ResultsTable.tsx | 49 +++++++++ client/src/lib/query/Query.tsx | 34 ++++++ 4 files changed, 186 insertions(+), 18 deletions(-) create mode 100644 client/src/components/tables/ResultsTable.tsx diff --git a/client/src/components/Window.tsx b/client/src/components/Window.tsx index 0df4391..e6d3013 100644 --- a/client/src/components/Window.tsx +++ b/client/src/components/Window.tsx @@ -1,9 +1,10 @@ -import React, { useState } from 'react'; -import { ThemeProvider, CssBaseline, createMuiTheme } from '@material-ui/core'; -import { QueryElem } from '../lib/query/Query'; +import React, { useState, useEffect } from 'react'; +import { ThemeProvider, CssBaseline, createMuiTheme, AppBar, Box } from '@material-ui/core'; +import { QueryElem, toApiQuery } from '../lib/query/Query'; import QueryBuilder from './querybuilder/QueryBuilder'; import * as serverApi from '../api'; -import { queryAllByRole } from '@testing-library/react'; +import { SongTable } from './tables/ResultsTable'; +var _ = require('lodash'); const darkTheme = createMuiTheme({ palette: { @@ -82,17 +83,95 @@ export async function getSongTitles(filter: string) { } export default function Window(props: any) { + interface ResultsFor { + for: QueryElem, + results: any[], + }; + const [query, setQuery] = useState(null); + const [resultsFor, setResultsFor] = useState(null); + + const loading = query && (!resultsFor || !_.isEqual(resultsFor.for, query)); + const showResults = (query && resultsFor && query == resultsFor.for) ? resultsFor.results : []; + + const songGetters = { + getTitle: (song: any) => song.title, + getArtist: (song: any) => "Artist", + getAlbum: (song: any) => "Album", + } + + const doQuery = async (_query: QueryElem) => { + var q: serverApi.QueryRequest = { + query: toApiQuery(_query), + offsetsLimits: { + songOffset: 0, + songLimit: 100, + }, + ordering: { + orderBy: { + type: serverApi.OrderByType.Name, + }, + ascending: true, + }, + }; + + const requestOpts = { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(q), + }; + + return (async () => { + const response = await fetch((process.env.REACT_APP_BACKEND || "") + serverApi.QueryEndpoint, requestOpts) + let json: any = await response.json(); + if(_.isEqual(query, _query)) { + setResultsFor({ + for: _query, + results: json.songs, + }) + } + })(); + } + + useEffect(() => { + if (query) { + doQuery(query); + } else { + setResultsFor(null); + } + }, [query]); return - + + + error + + + + + + + + + + + } \ No newline at end of file diff --git a/client/src/components/querybuilder/QBLeafElem.tsx b/client/src/components/querybuilder/QBLeafElem.tsx index 59a5a94..f4904ea 100644 --- a/client/src/components/querybuilder/QBLeafElem.tsx +++ b/client/src/components/querybuilder/QBLeafElem.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { QueryLeafElem, QueryLeafBy, QueryLeafOp, QueryElem } from '../../lib/query/Query'; import { Chip, Typography, IconButton, Box } from '@material-ui/core'; import { QBPlaceholder } from './QBPlaceholder'; -import CloseIcon from '@material-ui/icons/Close'; +import DeleteIcon from '@material-ui/icons/Delete'; import { Requests } from './QueryBuilder'; export interface ElemChipProps { @@ -56,8 +56,12 @@ export interface DeleteButtonProps { } export function QBQueryElemDeleteButton(props: DeleteButtonProps) { - return - + return + } @@ -72,9 +76,11 @@ export function QBLeafElem(props: IProps) { let e = props.elem; const extraElements = props.editingQuery ? - props.onReplace(null)} - /> + + props.onReplace(null)} + /> + : undefined; if (e.a == QueryLeafBy.ArtistName && diff --git a/client/src/components/tables/ResultsTable.tsx b/client/src/components/tables/ResultsTable.tsx new file mode 100644 index 0000000..b701222 --- /dev/null +++ b/client/src/components/tables/ResultsTable.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import { TableContainer, Table, TableHead, TableRow, TableCell, Paper, makeStyles, TableBody } from '@material-ui/core'; + +export interface SongGetters { + getTitle: (song: any) => string, + getArtist: (song: any) => string, + getAlbum: (song: any) => string, +} + +export interface IProps { + songs: any[], + songGetters: SongGetters, +} + +export function SongTable(props: IProps) { + const useTableStyles = makeStyles({ + table: { + minWidth: 650, + }, + }); + const classes = useTableStyles(); + + return ( + + + + + Title + Artist + Album + + + + {props.songs.map((song:any) => { + const title = props.songGetters.getTitle(song); + const artist = props.songGetters.getArtist(song); + const album = props.songGetters.getAlbum(song); + + return + {title} + {artist} + {album} + + })} + +
+
+ ); +} \ No newline at end of file diff --git a/client/src/lib/query/Query.tsx b/client/src/lib/query/Query.tsx index 09197ca..ad617b3 100644 --- a/client/src/lib/query/Query.tsx +++ b/client/src/lib/query/Query.tsx @@ -1,3 +1,5 @@ +import * as serverApi from '../../api'; + export enum QueryLeafBy { ArtistName = 0, AlbumName, @@ -149,4 +151,36 @@ export function simplify(q: QueryElem | null): QueryElem | null { } return q; +} + +export function toApiQuery(q: QueryElem) : serverApi.Query { + const propsMapping: any = { + [QueryLeafBy.SongTitle]: serverApi.QueryElemProperty.songTitle, + [QueryLeafBy.ArtistName]: serverApi.QueryElemProperty.artistName, + } + const leafOpsMapping: any = { + [QueryLeafOp.Equals]: serverApi.QueryFilterOp.Eq, + [QueryLeafOp.Like]: serverApi.QueryFilterOp.Like, + } + const nodeOpsMapping: any = { + [QueryNodeOp.And]: serverApi.QueryElemOp.And, + [QueryNodeOp.Or]: serverApi.QueryElemOp.Or, + } + + if(isLeafElem(q)) { + const r: serverApi.QueryElem = { + prop: propsMapping[q.a], + propOperator: leafOpsMapping[q.leafOp], + propOperand: q.b, + } + return r; + } else if(isNodeElem(q)) { + const r = { + children: q.operands.map((op: any) => toApiQuery(op)), + childrenOperator: nodeOpsMapping[q.nodeOp] + } + return r; + } + + return {}; } \ No newline at end of file -- 2.36.1 From f861abe0afb6dfe1437f5a012963e79f53ae1447 Mon Sep 17 00:00:00 2001 From: Sander Vocke Date: Mon, 14 Sep 2020 16:04:01 +0200 Subject: [PATCH 11/15] Server supports album included in songs query. Correct display on client. --- client/src/api.ts | 9 ++ client/src/components/Window.tsx | 5 +- client/src/lib/stringifyList.tsx | 13 +++ server/endpoints/QueryEndpointHandler.ts | 107 ++++------------------- 4 files changed, 40 insertions(+), 94 deletions(-) create mode 100644 client/src/lib/stringifyList.tsx diff --git a/client/src/api.ts b/client/src/api.ts index 748c80c..812ad32 100644 --- a/client/src/api.ts +++ b/client/src/api.ts @@ -22,6 +22,14 @@ export interface ArtistDetails { export function isArtistDetails(q: any): q is ArtistDetails { return 'artistId' in q; } +export interface AlbumDetails { + albumId: number, + name: string, + storeLinks?: string[], +} +export function isAlbumDetails(q: any): q is ArtistDetails { + return 'albumId' in q; +} export interface TagDetails { tagId: number, name: string, @@ -45,6 +53,7 @@ export interface SongDetails { songId: number, title: string, artists?: ArtistDetails[], + albums?: AlbumDetails[], tags?: TagDetails[], storeLinks?: string[], rankings?: RankingDetails[], diff --git a/client/src/components/Window.tsx b/client/src/components/Window.tsx index e6d3013..52856da 100644 --- a/client/src/components/Window.tsx +++ b/client/src/components/Window.tsx @@ -4,6 +4,7 @@ import { QueryElem, toApiQuery } from '../lib/query/Query'; import QueryBuilder from './querybuilder/QueryBuilder'; import * as serverApi from '../api'; import { SongTable } from './tables/ResultsTable'; +import stringifyList from '../lib/stringifyList'; var _ = require('lodash'); const darkTheme = createMuiTheme({ @@ -96,8 +97,8 @@ export default function Window(props: any) { const songGetters = { getTitle: (song: any) => song.title, - getArtist: (song: any) => "Artist", - getAlbum: (song: any) => "Album", + getArtist: (song: any) => stringifyList(song.artists, (a: any) => a.name), + getAlbum: (song: any) => stringifyList(song.albums, (a: any) => a.name), } const doQuery = async (_query: QueryElem) => { diff --git a/client/src/lib/stringifyList.tsx b/client/src/lib/stringifyList.tsx new file mode 100644 index 0000000..dc883ba --- /dev/null +++ b/client/src/lib/stringifyList.tsx @@ -0,0 +1,13 @@ +export default function stringifyList( + s: any[], + stringifyElem?: (e: any) => string, +) { + const stringify = stringifyElem || ((e: any) => e); + var r = ""; + if (s.length > 0) { r += stringify(s[0]) } + for (let i = 1; i < s.length; i++) { + r += ", " + stringify(s[i]); + } + + return r; +} \ No newline at end of file diff --git a/server/endpoints/QueryEndpointHandler.ts b/server/endpoints/QueryEndpointHandler.ts index 7eedd3f..3be1197 100644 --- a/server/endpoints/QueryEndpointHandler.ts +++ b/server/endpoints/QueryEndpointHandler.ts @@ -297,13 +297,19 @@ export const QueryEndpointHandler: EndpointHandler = async (req: any, res: any, return await getLinkedObjects(knex, ObjectType.Song, ObjectType.Tag, await songIdsPromise); })() : (async () => { return {}; })(); + const songsAlbumsPromise: Promise> = (songLimit && songLimit > 0) ? + (async () => { + return await getLinkedObjects(knex, ObjectType.Song, ObjectType.Album, await songIdsPromise); + })() : + (async () => { return {}; })(); const [ songs, artists, tags, songsArtists, - songsTags + songsTags, + songsAlbums, ] = await Promise.all([ songsPromise, @@ -311,6 +317,7 @@ export const QueryEndpointHandler: EndpointHandler = async (req: any, res: any, tagsPromise, songsArtistsPromise, songsTagsPromise, + songsAlbumsPromise, ]); const response: api.QueryResponse = { @@ -332,7 +339,13 @@ export const QueryEndpointHandler: EndpointHandler = async (req: any, res: any, name: tag['tags.name'], }; }), - albums: [], //FIXME + albums: songsAlbums[song['songs.id']].map((album: any) => { + return { + albumId: album['albums.id'], + name: album['albums.name'], + storeLinks: asJson(album['albums.storeLinks']), + }; + }), } }), artists: artists.map((artist: any) => { @@ -355,94 +368,4 @@ export const QueryEndpointHandler: EndpointHandler = async (req: any, res: any, } catch (e) { catchUnhandledErrors(e); } - - // try { - // const songLimit = reqObject.offsetsLimits.songLimit; - // const songOffset = reqObject.offsetsLimits.songOffset; - // const tagLimit = reqObject.offsetsLimits.tagLimit; - // const tagOffset = reqObject.offsetsLimits.tagOffset; - // const artistLimit = reqObject.offsetsLimits.artistLimit; - // const artistOffset = reqObject.offsetsLimits.artistOffset; - - // const songs = (songLimit && songLimit > 0) && 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, QueryType.Song), - // order: getSequelizeOrder(reqObject.ordering, QueryType.Song), - // include: [ models.Artist, models.Album, models.Tag, models.Ranking ], - // //limit: reqObject.limit, - // //offset: reqObject.offset, - // }) - // const artists = (artistLimit && artistLimit > 0) && await models.Artist.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, QueryType.Artist), - // order: getSequelizeOrder(reqObject.ordering, QueryType.Artist), - // include: [models.Song, models.Album, models.Tag], - // //limit: reqObject.limit, - // //offset: reqObject.offset, - // }) - // const tags = (tagLimit && tagLimit > 0) && await models.Tag.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, QueryType.Tag), - // order: getSequelizeOrder(reqObject.ordering, QueryType.Tag), - // include: [models.Song, models.Album, models.Artist], - // //limit: reqObject.limit, - // //offset: reqObject.offset, - // }) - - // const response: api.QueryResponse = { - // songs: ((songLimit || -1) <= 0) ? [] : await Promise.all(songs.map(async (song: any) => { - // const artists = song.getArtists(); - // const tags = song.getTags(); - // const rankings = song.getRankings(); - // return { - // songId: song.id, - // title: song.title, - // storeLinks: song.storeLinks, - // artists: (await artists).map((artist: any) => { - // return { - // artistId: artist.id, - // name: artist.name, - // } - // }), - // tags: (await tags).map((tag: any) => { - // return { - // tagId: tag.id, - // name: tag.name, - // } - // }), - // rankings: await (await rankings).map(async (ranking: any) => { - // const maybeTagContext: api.TagDetails | undefined = await ranking.getTagContext(); - // const maybeArtistContext: api.ArtistDetails | undefined = await ranking.getArtistContext(); - // const maybeContext = maybeTagContext || maybeArtistContext; - // return { - // rankingId: ranking.id, - // type: api.ItemType.Song, - // rankedId: song.id, - // context: maybeContext, - // value: ranking.value, - // } - // }) - // }; - // }).slice(songOffset || 0, (songOffset || 0) + (songLimit || 10))), - // // TODO: custom pagination due to bug mentioned above - // artists: ((artistLimit || -1) <= 0) ? [] : await Promise.all(artists.map(async (artist: any) => { - // return { - // artistId: artist.id, - // name: artist.name, - // }; - // }).slice(artistOffset || 0, (artistOffset || 0) + (artistLimit || 10))), - // tags: ((tagLimit || -1) <= 0) ? [] : await Promise.all(tags.map(async (tag: any) => { - // return { - // tagId: tag.id, - // name: tag.name, - // }; - // }).slice(tagOffset || 0, (tagOffset || 0) + (tagLimit || 10))), - // }; - // res.send(response); - // } catch (e) { - // catchUnhandledErrors(e); - // } } \ No newline at end of file -- 2.36.1 From 981393b7a4a2bb075d5b18d40bc24ac37cf03b63 Mon Sep 17 00:00:00 2001 From: Sander Vocke Date: Mon, 14 Sep 2020 16:11:56 +0200 Subject: [PATCH 12/15] Try combinatorial logic. --- server/endpoints/QueryEndpointHandler.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/endpoints/QueryEndpointHandler.ts b/server/endpoints/QueryEndpointHandler.ts index 3be1197..0a6a850 100644 --- a/server/endpoints/QueryEndpointHandler.ts +++ b/server/endpoints/QueryEndpointHandler.ts @@ -116,13 +116,13 @@ function addLeafWhere(knexQuery: any, queryElem: api.QueryElem, type: WhereType) } } else if (operator == api.QueryFilterOp.In) { if (type == WhereType.And) { - return knexQuery.whereIn(a, b); + return knexQuery.andWhereIn(a, b); } else if (type == WhereType.Or) { return knexQuery.orWhereIn(a, b); } } else if (operator == api.QueryFilterOp.NotIn) { if (type == WhereType.And) { - return knexQuery.whereNotIn(a, b); + return knexQuery.andWhereNotIn(a, b); } else if (type == WhereType.Or) { return knexQuery.orWhereNotIn(a, b); } -- 2.36.1 From c0c6768cbb923ff520c0eeaa8abef77d445d29a5 Mon Sep 17 00:00:00 2001 From: Sander Vocke Date: Mon, 14 Sep 2020 16:15:54 +0200 Subject: [PATCH 13/15] Remove mock-up. --- client/src/App.tsx | 4 - client/src/components/Mockup.tsx | 149 ------------------------------- 2 files changed, 153 deletions(-) delete mode 100644 client/src/components/Mockup.tsx diff --git a/client/src/App.tsx b/client/src/App.tsx index bbe4c9c..53a1c55 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -8,7 +8,6 @@ import { Switch, Route } from "react-router-dom"; -import Mockup from './components/Mockup'; import Window from './components/Window'; function App() { @@ -16,9 +15,6 @@ function App() { - - - diff --git a/client/src/components/Mockup.tsx b/client/src/components/Mockup.tsx deleted file mode 100644 index 3d71382..0000000 --- a/client/src/components/Mockup.tsx +++ /dev/null @@ -1,149 +0,0 @@ -import React from 'react'; -import { makeStyles, createMuiTheme, ThemeProvider } from '@material-ui/core/styles'; -import Table from '@material-ui/core/Table'; -import TableBody from '@material-ui/core/TableBody'; -import TableCell from '@material-ui/core/TableCell'; -import TableContainer from '@material-ui/core/TableContainer'; -import TableHead from '@material-ui/core/TableHead'; -import TableRow from '@material-ui/core/TableRow'; -import Paper from '@material-ui/core/Paper'; -import Box from '@material-ui/core/Box'; -import SearchIcon from '@material-ui/icons/Search'; -import { IconButton, Typography, Chip, Link, AppBar, CssBaseline } from '@material-ui/core'; - -export function MockTable() { - - const rows = [ - ['No One Knows', "Queens of the Stone Age", "Blabla", "Tag"], - ['Ice cream sandwich', "My man", "What?", 37], - ['Eclair', "George Baker", "Bakeroo", 24], - ['Cupcake', "The Sweetie Pies", "Bla", "Bloo"], - ['Gingerbread', "Is ahead", "But you'll get", "Vury fet"], - ]; - - const useTableStyles = makeStyles({ - table: { - minWidth: 650, - }, - }); - const classes = useTableStyles(); - - return ( - - - - - Title - Artist - Album - Tags - - - - {rows.map((row) => ( - - {row[0]} - {row[1]} - {row[2]} - {row[3]} - - ))} - -
-
- ); -} - -export function QueryContainer(props: any) { - return - {props.children} - -} - -export function QueryElem(props: any) { - return <> - - -} - -export function QueryBar() { - return - - - - - - - - - - - - By Queens of the Stone Age - - - - - Released before 2020 - - - - - - - - Or - - - - - Genre: "Awesome" - - - - -} - -function MockupBody() { - return <> - - - error - - - - - {/* */} - - {/* */} - - - - - - - - -} - - -const darkTheme = createMuiTheme({ - palette: { - type: 'dark' - }, -}); - -export default function Mockup() { - return <> - - - - - -} \ No newline at end of file -- 2.36.1 From 112047f8601910ef1791a89723e064914e946f1c Mon Sep 17 00:00:00 2001 From: Sander Vocke Date: Mon, 14 Sep 2020 16:19:56 +0200 Subject: [PATCH 14/15] Query stops editing when an element is added. --- client/src/components/querybuilder/QueryBuilder.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/client/src/components/querybuilder/QueryBuilder.tsx b/client/src/components/querybuilder/QueryBuilder.tsx index 4c0ad2f..dc73d57 100644 --- a/client/src/components/querybuilder/QueryBuilder.tsx +++ b/client/src/components/querybuilder/QueryBuilder.tsx @@ -25,6 +25,7 @@ export default function QueryBuilder(props: IProps) { const onReplace = (q: any) => { const newQ = removePlaceholders(q); + setEditing(false); props.onChangeQuery(newQ); } -- 2.36.1 From e852fc2c4a8e0838db28a13a21bd44d5b011cb44 Mon Sep 17 00:00:00 2001 From: Sander Vocke Date: Mon, 14 Sep 2020 16:24:28 +0200 Subject: [PATCH 15/15] Fix knex error. --- server/endpoints/QueryEndpointHandler.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/endpoints/QueryEndpointHandler.ts b/server/endpoints/QueryEndpointHandler.ts index 0a6a850..3be1197 100644 --- a/server/endpoints/QueryEndpointHandler.ts +++ b/server/endpoints/QueryEndpointHandler.ts @@ -116,13 +116,13 @@ function addLeafWhere(knexQuery: any, queryElem: api.QueryElem, type: WhereType) } } else if (operator == api.QueryFilterOp.In) { if (type == WhereType.And) { - return knexQuery.andWhereIn(a, b); + return knexQuery.whereIn(a, b); } else if (type == WhereType.Or) { return knexQuery.orWhereIn(a, b); } } else if (operator == api.QueryFilterOp.NotIn) { if (type == WhereType.And) { - return knexQuery.andWhereNotIn(a, b); + return knexQuery.whereNotIn(a, b); } else if (type == WhereType.Or) { return knexQuery.orWhereNotIn(a, b); } -- 2.36.1