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 <>
-
+
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
+}
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
-}
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) {