parent
a36830802f
commit
9c9d1b6dd8
14 changed files with 303 additions and 132 deletions
@ -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 <Menu |
||||||
|
anchorEl={anchorEl} |
||||||
|
keepMounted |
||||||
|
open={Boolean(anchorEl)} |
||||||
|
onClose={onClose} |
||||||
|
> |
||||||
|
<MenuItem disabled={true}>New query element</MenuItem> |
||||||
|
<NestedMenuItem |
||||||
|
label="Song" |
||||||
|
parentMenuOpen={Boolean(anchorEl)} |
||||||
|
> |
||||||
|
<QBSelectWithRequest |
||||||
|
label="Title" |
||||||
|
getNewOptions={props.requestFunctions.getSongTitles} |
||||||
|
onSubmit={(s: string, exact: boolean) => { |
||||||
|
onClose(); |
||||||
|
props.onCreateQuery({ |
||||||
|
a: QueryLeafBy.SongTitle, |
||||||
|
leafOp: exact ? QueryLeafOp.Equals : QueryLeafOp.Like, |
||||||
|
b: s |
||||||
|
}); |
||||||
|
}} |
||||||
|
style={{ width: 300 }} |
||||||
|
/> |
||||||
|
</NestedMenuItem> |
||||||
|
<NestedMenuItem |
||||||
|
label="Artist" |
||||||
|
parentMenuOpen={Boolean(anchorEl)} |
||||||
|
> |
||||||
|
<QBSelectWithRequest |
||||||
|
label="Name" |
||||||
|
getNewOptions={props.requestFunctions.getArtists} |
||||||
|
onSubmit={(s: string, exact: boolean) => { |
||||||
|
onClose(); |
||||||
|
props.onCreateQuery({ |
||||||
|
a: QueryLeafBy.ArtistName, |
||||||
|
leafOp: exact ? QueryLeafOp.Equals : QueryLeafOp.Like, |
||||||
|
b: s |
||||||
|
}); |
||||||
|
}} |
||||||
|
style={{ width: 300 }} |
||||||
|
/> |
||||||
|
</NestedMenuItem> |
||||||
|
</Menu > |
||||||
|
} |
@ -1,30 +1,34 @@ |
|||||||
import React from 'react'; |
import React from 'react'; |
||||||
import { QueryLeafElem, QueryNodeElem, QueryElem, isLeafElem, isNodeElem } from '../../lib/query/Query'; |
import { QueryLeafElem, QueryNodeElem, QueryElem, isLeafElem, isNodeElem } from '../../lib/query/Query'; |
||||||
import { QBQueryLeafElem } from './QBQueryLeafElem'; |
import { QBLeafElem } from './QBLeafElem'; |
||||||
import { QBQueryNodeElem } from './QBQueryNodeElem'; |
import { QBNodeElem } from './QBNodeElem'; |
||||||
|
import { Requests } from './QueryBuilder'; |
||||||
|
|
||||||
export interface IProps { |
export interface IProps { |
||||||
elem: QueryLeafElem | QueryNodeElem, |
elem: QueryLeafElem | QueryNodeElem, |
||||||
onReplace: (q: QueryElem | null) => void, |
onReplace: (q: QueryElem | null) => void, |
||||||
editingQuery: boolean, |
editingQuery: boolean, |
||||||
|
requestFunctions: Requests, |
||||||
} |
} |
||||||
|
|
||||||
export function QBQueryElem(props: IProps) { |
export function QBQueryElem(props: IProps) { |
||||||
let e = props.elem; |
let e = props.elem; |
||||||
|
|
||||||
if (isLeafElem(e)) { |
if (isLeafElem(e)) { |
||||||
return <QBQueryLeafElem |
return <QBLeafElem |
||||||
elem={e} |
elem={e} |
||||||
onReplace={props.onReplace} |
onReplace={props.onReplace} |
||||||
editingQuery={props.editingQuery} |
editingQuery={props.editingQuery} |
||||||
|
requestFunctions={props.requestFunctions} |
||||||
/> |
/> |
||||||
} else if (isNodeElem(e)) { |
} else if (isNodeElem(e)) { |
||||||
return <QBQueryNodeElem |
return <QBNodeElem |
||||||
elem={e} |
elem={e} |
||||||
onReplace={props.onReplace} |
onReplace={props.onReplace} |
||||||
editingQuery={props.editingQuery} |
editingQuery={props.editingQuery} |
||||||
|
requestFunctions={props.requestFunctions} |
||||||
/> |
/> |
||||||
} |
} |
||||||
|
|
||||||
throw "Unsupported query element"; |
throw new Error("Unsupported query element"); |
||||||
} |
} |
@ -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<string>(''); |
|
||||||
|
|
||||||
const onInputChange = (event: any, _val: any, reason: any) => { |
|
||||||
if (reason === 'reset') { |
|
||||||
// User selected a preset option.
|
|
||||||
props.onSubmit(_val, true); |
|
||||||
} else { |
|
||||||
setValue(_val); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
return <Autocomplete |
|
||||||
style={{ width: 300 }} |
|
||||||
options={props.options} |
|
||||||
freeSolo={true} |
|
||||||
onInputChange={onInputChange} |
|
||||||
onKeyDown={(e: any) => { |
|
||||||
if (e.key === 'Enter') { |
|
||||||
// User submitted free-form value.
|
|
||||||
props.onSubmit(value, props.options.includes(value)); |
|
||||||
} |
|
||||||
}} |
|
||||||
renderInput={(params: any) => <TextField |
|
||||||
{...params} |
|
||||||
label={props.label} |
|
||||||
/>} |
|
||||||
/> |
|
||||||
} |
|
||||||
|
|
||||||
|
|
||||||
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 <Menu |
|
||||||
id="simple-menu" |
|
||||||
anchorEl={anchorEl} |
|
||||||
keepMounted |
|
||||||
open={Boolean(anchorEl)} |
|
||||||
onClose={onClose} |
|
||||||
> |
|
||||||
<NestedMenuItem |
|
||||||
label="Song" |
|
||||||
parentMenuOpen={Boolean(anchorEl)} |
|
||||||
> |
|
||||||
<QBMenuFreeSoloInput |
|
||||||
options={songs} |
|
||||||
label="Title" |
|
||||||
onSubmit={(s: string, exact: boolean) => { |
|
||||||
onClose(); |
|
||||||
props.onCreateQuery({ |
|
||||||
a: QueryLeafBy.SongTitle, |
|
||||||
leafOp: exact ? QueryLeafOp.Equals : QueryLeafOp.Like, |
|
||||||
b: s |
|
||||||
}); |
|
||||||
}} |
|
||||||
/> |
|
||||||
</NestedMenuItem> |
|
||||||
<NestedMenuItem |
|
||||||
label="Artist" |
|
||||||
parentMenuOpen={Boolean(anchorEl)} |
|
||||||
> |
|
||||||
<QBMenuFreeSoloInput |
|
||||||
options={artists} |
|
||||||
label="Artist" |
|
||||||
onSubmit={(s: string, exact: boolean) => { |
|
||||||
onClose(); |
|
||||||
props.onCreateQuery({ |
|
||||||
a: QueryLeafBy.ArtistName, |
|
||||||
leafOp: exact ? QueryLeafOp.Equals : QueryLeafOp.Like, |
|
||||||
b: s |
|
||||||
}); |
|
||||||
}} |
|
||||||
/> |
|
||||||
</NestedMenuItem> |
|
||||||
</Menu > |
|
||||||
} |
|
@ -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<string[]>, |
||||||
|
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<OptionsFor | null>(null); |
||||||
|
const [input, setInput] = useState<string>(""); |
||||||
|
|
||||||
|
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 ( |
||||||
|
<Autocomplete |
||||||
|
{...restProps} |
||||||
|
open={open} |
||||||
|
onOpen={() => { |
||||||
|
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) => ( |
||||||
|
<TextField |
||||||
|
{...params} |
||||||
|
label={label} |
||||||
|
variant="outlined" |
||||||
|
InputProps={{ |
||||||
|
...params.InputProps, |
||||||
|
endAdornment: ( |
||||||
|
<React.Fragment> |
||||||
|
{loading ? <CircularProgress color="inherit" size={20} /> : null} |
||||||
|
{params.InputProps.endAdornment} |
||||||
|
</React.Fragment> |
||||||
|
), |
||||||
|
}} |
||||||
|
/> |
||||||
|
)} |
||||||
|
/> |
||||||
|
); |
||||||
|
} |
Loading…
Reference in new issue