parent
c197e23c04
commit
e3d741d466
11 changed files with 360 additions and 215 deletions
@ -1,122 +1,15 @@ |
|||||||
import React, { useState } from 'react'; |
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 SearchIcon from '@material-ui/icons/Search'; |
||||||
import NestedMenuItem from "material-ui-nested-menu-item"; |
import CheckIcon from '@material-ui/icons/Check'; |
||||||
import { QueryElem, QueryLeafBy, QueryLeafOp } from '../../lib/Query'; |
|
||||||
import Autocomplete from '@material-ui/lab/Autocomplete' |
|
||||||
|
|
||||||
export function QBQueryButtonBase(props: any) { |
|
||||||
return <IconButton {...props}> |
|
||||||
<SearchIcon style={{ fontSize: 80 }} /> |
|
||||||
</IconButton> |
|
||||||
} |
|
||||||
|
|
||||||
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 QBQueryButtonMenu(props: MenuProps) { |
|
||||||
let anchorEl = props.anchorEl; |
|
||||||
let onClose = props.onClose; |
|
||||||
|
|
||||||
const artists = [ |
|
||||||
'Queens', |
|
||||||
'Muse', |
|
||||||
'Triggerfinger' |
|
||||||
]; |
|
||||||
|
|
||||||
return <Menu |
|
||||||
id="simple-menu" |
|
||||||
anchorEl={anchorEl} |
|
||||||
keepMounted |
|
||||||
open={Boolean(anchorEl)} |
|
||||||
onClose={onClose} |
|
||||||
> |
|
||||||
<NestedMenuItem |
|
||||||
label="Artist" |
|
||||||
parentMenuOpen={Boolean(anchorEl)} |
|
||||||
> |
|
||||||
<QBMenuFreeSoloInput |
|
||||||
options={artists} |
|
||||||
label="Artist" |
|
||||||
onSubmit={(s: string, exact: boolean) => { |
|
||||||
onClose(); |
|
||||||
props.onCreateQuery({ |
|
||||||
a: QueryLeafBy.ArtistName, |
|
||||||
op: exact ? QueryLeafOp.Equals : QueryLeafOp.Like, |
|
||||||
b: s |
|
||||||
}); |
|
||||||
}} |
|
||||||
/> |
|
||||||
</NestedMenuItem> |
|
||||||
</Menu > |
|
||||||
} |
|
||||||
|
|
||||||
export interface IProps { |
export interface IProps { |
||||||
|
editing: boolean |
||||||
} |
} |
||||||
|
|
||||||
export default function QBQueryButton(props: IProps) { |
export default function QBQueryButton(props: any) { |
||||||
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null); |
return <IconButton {...props}> |
||||||
const onOpen = (e: React.MouseEvent<HTMLButtonElement>) => { |
{(!props.editing) && <SearchIcon style={{ fontSize: 80 }} />} |
||||||
setAnchorEl(e.currentTarget); |
{(props.editing) && <CheckIcon style={{ fontSize: 80 }} />} |
||||||
} |
</IconButton> |
||||||
const onClose = () => { |
|
||||||
setAnchorEl(null); |
|
||||||
} |
|
||||||
const onCreateQuery = (q: QueryElem) => { |
|
||||||
console.log("created query:", q); |
|
||||||
} |
|
||||||
|
|
||||||
return <> |
|
||||||
<QBQueryButtonBase |
|
||||||
aria-controls="simple-menu" |
|
||||||
aria-haspopup="true" |
|
||||||
onClick={onOpen} |
|
||||||
/> |
|
||||||
<QBQueryButtonMenu |
|
||||||
anchorEl={anchorEl} |
|
||||||
onClose={onClose} |
|
||||||
onCreateQuery={onCreateQuery} |
|
||||||
/> |
|
||||||
</>; |
|
||||||
} |
} |
@ -1,24 +1,23 @@ |
|||||||
import React from 'react'; |
import React from 'react'; |
||||||
import { QueryLeafElem, QueryNodeElem } from '../../lib/Query'; |
import { QueryLeafElem, QueryNodeElem, QueryElem, isLeafElem, isNodeElem } from '../../lib/Query'; |
||||||
import { QBQueryLeafElem } from './QBQueryLeafElem'; |
import { QBQueryLeafElem } from './QBQueryLeafElem'; |
||||||
import { QBQueryNodeElem } from './QBQueryNodeElem'; |
import { QBQueryNodeElem } from './QBQueryNodeElem'; |
||||||
|
|
||||||
export interface IProps { |
export interface IProps { |
||||||
elem: QueryLeafElem | QueryNodeElem, |
elem: QueryLeafElem | QueryNodeElem, |
||||||
|
onReplace: (q: QueryElem) => void, |
||||||
} |
} |
||||||
|
|
||||||
export function QBQueryElem(props: IProps) { |
export function QBQueryElem(props: IProps) { |
||||||
let e = props.elem; |
let e = props.elem; |
||||||
|
|
||||||
let renderLeaf = (l: any) => { |
if (isLeafElem(e)) { |
||||||
return <QBQueryLeafElem elem={l} /> |
return <QBQueryLeafElem elem={e} onReplace={props.onReplace} /> |
||||||
} |
} else if (isNodeElem(e)) { |
||||||
|
return <QBQueryNodeElem |
||||||
if (e instanceof QueryLeafElem) { |
elem={e} |
||||||
return renderLeaf(e); |
onReplace={props.onReplace} |
||||||
} else if (e instanceof QueryNodeElem) { |
/> |
||||||
return <QBQueryNodeElem elem={e} |
|
||||||
renderLeaf={renderLeaf} /> |
|
||||||
} |
} |
||||||
|
|
||||||
throw "Unsupported query element"; |
throw "Unsupported query element"; |
||||||
|
@ -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<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 > |
||||||
|
} |
@ -1,34 +1,41 @@ |
|||||||
import React from 'react'; |
import React from 'react'; |
||||||
import QBOrBlock from './QBOrBlock'; |
import QBOrBlock from './QBOrBlock'; |
||||||
import QBAndBlock from './QBAndBlock'; |
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 { |
export interface NodeProps { |
||||||
elem: QueryNodeElem, |
elem: QueryNodeElem, |
||||||
renderLeaf: (leaf: any) => any, |
onReplace: (q: QueryElem) => void, |
||||||
} |
} |
||||||
|
|
||||||
export function QBQueryNodeElem(props: NodeProps) { |
export function QBQueryNodeElem(props: NodeProps) { |
||||||
let e = props.elem; |
let e = props.elem; |
||||||
|
|
||||||
if (e.op == QueryNodeOp.And) { |
const onReplace = (idx: number, q: QueryElem) => { |
||||||
return <QBAndBlock> |
var ops = e.operands; |
||||||
{e.operands.map((o: any) => { |
ops[idx] = q; |
||||||
if(o instanceof QueryNodeElem) { |
let newNode = { operands: ops, nodeOp: e.nodeOp }; |
||||||
return <QBQueryNodeElem elem={o} renderLeaf={props.renderLeaf}/> |
props.onReplace(newNode); |
||||||
} |
} |
||||||
return props.renderLeaf(o); |
|
||||||
})} |
const children = e.operands.map((o: any, idx: number) => { |
||||||
</QBAndBlock> |
if (isNodeElem(o)) { |
||||||
} else if (e.op == QueryNodeOp.Or) { |
return <QBQueryNodeElem |
||||||
return <QBOrBlock> |
elem={o} |
||||||
{e.operands.map((o: any) => { |
onReplace={(q: QueryElem) => onReplace(idx, q)} |
||||||
if(o instanceof QueryNodeElem) { |
/> |
||||||
return <QBQueryNodeElem elem={o} renderLeaf={props.renderLeaf}/> |
} |
||||||
} |
return <QBQueryLeafElem |
||||||
return props.renderLeaf(o); |
elem={o} |
||||||
})} |
onReplace={(q: QueryElem) => onReplace(idx, q)} |
||||||
</QBOrBlock> |
/> |
||||||
|
}); |
||||||
|
|
||||||
|
if (e.nodeOp == QueryNodeOp.And) { |
||||||
|
return <QBAndBlock>{children}</QBAndBlock> |
||||||
|
} else if (e.nodeOp == QueryNodeOp.Or) { |
||||||
|
return <QBOrBlock>{children}</QBOrBlock> |
||||||
} |
} |
||||||
|
|
||||||
throw "Unsupported node element"; |
throw "Unsupported node element"; |
||||||
|
@ -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 | HTMLElement>(null); |
||||||
|
|
||||||
|
const onOpen = (event: any) => { |
||||||
|
setAnchorEl(event.currentTarget); |
||||||
|
}; |
||||||
|
const onClose = () => { |
||||||
|
setAnchorEl(null); |
||||||
|
}; |
||||||
|
const onCreate = (q: QueryElem) => { |
||||||
|
props.onReplace(q); |
||||||
|
}; |
||||||
|
|
||||||
|
return <> |
||||||
|
<Chip |
||||||
|
variant="outlined" |
||||||
|
label="" |
||||||
|
style={{ width: "50px" }} |
||||||
|
clickable={true} |
||||||
|
onClick={onOpen} |
||||||
|
component="div" |
||||||
|
/> |
||||||
|
<QBQueryElemMenu |
||||||
|
anchorEl={anchorEl} |
||||||
|
onClose={onClose} |
||||||
|
onCreateQuery={onCreate} |
||||||
|
/> |
||||||
|
</> |
||||||
|
} |
@ -1,20 +1,40 @@ |
|||||||
import React from 'react'; |
import React, { useState } from 'react'; |
||||||
import { Box } from '@material-ui/core'; |
import { Box } from '@material-ui/core'; |
||||||
import QBQueryButton from './QBQueryButton'; |
import QBQueryButton from './QBQueryButton'; |
||||||
import { QBQueryElem } from './QBQueryElem'; |
import { QBQueryElem } from './QBQueryElem'; |
||||||
import { QueryElem } from '../../lib/Query'; |
import { QueryElem, addPlaceholders, removePlaceholders, simplify } from '../../lib/Query'; |
||||||
|
|
||||||
export interface IProps { |
export interface IProps { |
||||||
query: QueryElem |
query: QueryElem | null, |
||||||
|
onChangeQuery: (q: QueryElem | null) => void, |
||||||
} |
} |
||||||
|
|
||||||
export default function QueryBuilder(props: IProps) { |
export default function QueryBuilder(props: IProps) { |
||||||
return <Box display="flex" alignItems="center"> |
const [editing, setEditing] = useState<boolean>(false); |
||||||
<Box m={2}> |
|
||||||
<QBQueryButton/> |
const simpleQuery = simplify(props.query); |
||||||
</Box> |
const showQuery = editing ? |
||||||
<Box m={2}> |
addPlaceholders(simpleQuery, null) : simpleQuery; |
||||||
<QBQueryElem elem={props.query}/> |
|
||||||
|
const onReplace = (q: any) => { |
||||||
|
const newQ = removePlaceholders(q); |
||||||
|
props.onChangeQuery(newQ); |
||||||
|
} |
||||||
|
|
||||||
|
return <> |
||||||
|
<Box display="flex" alignItems="center"> |
||||||
|
<Box m={2}> |
||||||
|
<QBQueryButton |
||||||
|
onClick={() => setEditing(!editing)} |
||||||
|
editing={editing} |
||||||
|
/> |
||||||
|
</Box> |
||||||
|
<Box m={2}> |
||||||
|
{showQuery && <QBQueryElem |
||||||
|
elem={showQuery} |
||||||
|
onReplace={onReplace} |
||||||
|
/>} |
||||||
|
</Box> |
||||||
</Box> |
</Box> |
||||||
</Box> |
</> |
||||||
} |
} |
Loading…
Reference in new issue