You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

737 lines
25 KiB

import React, { useEffect } from 'react';
import Switch from '@material-ui/core/Switch';
import Box from '@material-ui/core/Box';
import FormControlLabel from '@material-ui/core/FormControlLabel';
import FormControl from '@material-ui/core/FormControl';
import Button from '@material-ui/core/Button';
import LabelIcon from '@material-ui/icons/Label';
import ImportContactsIcon from '@material-ui/icons/ImportContacts';
import PhotoIcon from '@material-ui/icons/Photo';
import SearchIcon from '@material-ui/icons/Search';
import Menu from '@material-ui/core/Menu';
import MenuItem from '@material-ui/core/MenuItem';
import DialogTitle from '@material-ui/core/DialogTitle';
import Dialog from '@material-ui/core/Dialog';
import Select from '@material-ui/core/Select';
import TextField from '@material-ui/core/TextField';
import ScheduleIcon from '@material-ui/icons/Schedule';
import DateFnsUtils from '@date-io/date-fns';
import { MuiPickersUtilsProvider, DateTimePicker } from "@material-ui/pickers";
import { makeStyles } from '@material-ui/core/styles';
import {
filter_is_const_false, ConstFilter, LogicalOperatorFilter, MatchingFilter,
ResultTypeEnum, LogicalOperatorEnum, MatchTypeEnum, NegationFilter, TimeFilterTypeEnum,
TimeFilter
} from './queries.js'
import { Typography } from '@material-ui/core';
const useStyles = makeStyles(theme => ({
root: {},
filterexpcontrol: {},
logic_op_outer: {
display: "flex",
},
logic_op_sbs: {
"box-sizing": "border-box",
},
logic_op_subexpr: {
float: "right",
},
filtercontrol: {
display: "flex",
},
bordered: {
border: "1px solid black",
"border-radius": "3px",
},
margined: {
margin: "6px",
},
}));
export function EditTimeFilterExpression(props) {
const { onChange, filter } = props;
var _ = require('lodash');
const id = _.uniqueId("time_filter_");
const labelid = _.uniqueId("time_filter_label_");
function handleChangeType(e) {
var newfilter = _.cloneDeep(filter);
newfilter.type = e.target.value;
onChange(newfilter);
}
function handleChangeDate(datetime) {
var newfilter = _.cloneDeep(filter);
newfilter.time = datetime;
onChange(newfilter);
}
return (
<FormControl>
<Select
labelId={labelid}
id={id}
value={filter.operator}
onChange={handleChangeType}
>
<MenuItem value={TimeFilterTypeEnum.BEFORE}>Before</MenuItem>
<MenuItem value={TimeFilterTypeEnum.AFTER}>After</MenuItem>
</Select>
<MuiPickersUtilsProvider utils={DateFnsUtils}>
<DateTimePicker
variant="inline"
label="Date and time"
value={filter.time}
onChange={handleChangeDate}
/>
</MuiPickersUtilsProvider>
</FormControl>
);
}
export function EditConstFilterExpression(props) {
const { onChange, filter } = props;
var _ = require('lodash');
function handleResultToggled() {
var newfilter = _.cloneDeep(filter);
newfilter.constval = !filter.constval;
onChange(newfilter);
}
return (
<FormControlLabel
control={
<Switch
checked={filter.constval}
onChange={handleResultToggled}
color="primary"
/>
}
label={filter.constval ? "TRUE" : "FALSE"}
/>
);
}
export function EditLogicalOperatorFilterExpression(props) {
const { onChange, filter } = props;
var _ = require('lodash');
const id = _.uniqueId("logic_op_");
const labelid = _.uniqueId("logic_op_label_");
function handleChange(e) {
var newfilter = _.cloneDeep(filter);
newfilter.operator = e.target.value;
onChange(newfilter);
}
return (
<FormControl>
<Select
labelId={labelid}
id={id}
value={filter.operator}
onChange={handleChange}
>
<MenuItem value={LogicalOperatorEnum.AND}>AND</MenuItem>
<MenuItem value={LogicalOperatorEnum.OR}>OR</MenuItem>
</Select>
</FormControl>
);
}
export function EditMatchingFilterExpression(props) {
const { onChange, filter } = props;
const classes = useStyles();
function handleTypeChange(e) {
var new_filter = _.cloneDeep(filter);
new_filter.match_type = e.target.value;
onChange(new_filter);
}
function handleMatchChange(e) {
var new_filter = _.cloneDeep(filter);
new_filter.match_from = e.target.value;
onChange(new_filter);
}
var _ = require('lodash');
const typeSelectId = _.uniqueId("select_");
const typeSelectLabelId = _.uniqueId("select_label_");
return (
<FormControl>
<Typography className={classes.margined}>TODO: make nice icons and autocomplete here</Typography>
<Select
className={classes.margined}
labelId={typeSelectLabelId}
id={typeSelectId}
value={filter.match_type}
onChange={handleTypeChange}
>
<MenuItem value={MatchTypeEnum.MATCH_IMAGE_NAME_EQUALS}>Image Name Equals</MenuItem>
<MenuItem value={MatchTypeEnum.MATCH_IMAGE_NAME_NATURAL}>Image Name Natural</MenuItem>
<MenuItem value={MatchTypeEnum.MATCH_ALBUM_EQUALS}>Album Equals</MenuItem>
<MenuItem value={MatchTypeEnum.MATCH_ALBUM_EQUALS_OR_CHILD}>Album Equals Or Child</MenuItem>
<MenuItem value={MatchTypeEnum.MATCH_ALBUM_NATURAL}>Album Natural</MenuItem>
<MenuItem value={MatchTypeEnum.MATCH_ALBUM_NAME_EQUALS}>Album Name Equals</MenuItem>
<MenuItem value={MatchTypeEnum.MATCH_TAG_EQUALS}>Tag Equals</MenuItem>
<MenuItem value={MatchTypeEnum.MATCH_TAG_EQUALS_OR_CHILD}>Tag Equals Or Child</MenuItem>
<MenuItem value={MatchTypeEnum.MATCH_TAG_NATURAL}>Tag Natural</MenuItem>
<MenuItem value={MatchTypeEnum.MATCH_TAG_NAME_EQUALS}>Tag Name Equals</MenuItem>
</Select>
<TextField className={classes.margined} label="Value" type="text" onChange={handleMatchChange} />
</FormControl>
);
}
export function EditFilterExpressionDialog(props) {
const classes = useStyles();
const { onClose, startingFilter, open } = props;
const [filter, setFilter] = React.useState(startingFilter);
const FilterTypeEnum = {
CONST: 0,
NEGATION: 1,
MATCHING: 2,
LOGICAL: 3,
TIME: 4,
};
useEffect(() => {
setFilter(startingFilter);
}, [startingFilter]);
const handleClose = () => {
onClose(filter);
};
const handleTypeChange = e => {
var val = e.target.value;
if (val == FilterTypeEnum.CONST) {
setFilter(new ConstFilter(filter.result_type, true));
} else if (val == FilterTypeEnum.MATCHING) {
setFilter(new MatchingFilter(filter.result_type, "", MatchTypeEnum.MATCH_IMAGE_NAME_NATURAL));
} else if (val == FilterTypeEnum.TIME) {
setFilter(new TimeFilter(filter.result_type, Date.now(), TimeFilterTypeEnum.AFTER));
} else {
throw new Error('Unsupported filter type: ' + val);
}
}
const getFilterType = filter => {
if (filter instanceof ConstFilter) { return FilterTypeEnum.CONST; }
else if (filter instanceof NegationFilter) { return FilterTypeEnum.NEGATION; }
else if (filter instanceof MatchingFilter) { return FilterTypeEnum.MATCHING; }
else if (filter instanceof LogicalOperatorFilter) { return FilterTypeEnum.LOGICAL; }
else if (filter instanceof TimeFilter) { return FilterTypeEnum.TIME; }
else {
throw new Error('Unsupported filter type: ' + filter);
}
}
var _ = require('lodash');
const id = _.uniqueId("simple_dialog_title_");
const selectId = _.uniqueId("select_");
const selectLabelId = _.uniqueId("select_label_");
var control = <></>;
var subprops = {
onChange: setFilter,
filter: filter,
};
if (filter instanceof ConstFilter) {
control = <EditConstFilterExpression {...subprops} />
} else if (filter instanceof LogicalOperatorFilter) {
control = <EditLogicalOperatorFilterExpression {...subprops} />
} else if (filter instanceof MatchingFilter) {
control = <EditMatchingFilterExpression {...subprops} />
} else if (filter instanceof TimeFilter) {
control = <EditTimeFilterExpression {...subprops} />
}
// If this is a "leaf" filter, we will allow changing the filter type in the dialog.
// But if it is a combination filter (i.e. AND / OR / NOT), we won't allow it because
// That throws away all its children.
const allowTypeChange =
(filter instanceof ConstFilter) ||
(filter instanceof MatchingFilter);
return (
<Dialog aria-labelledby={id} open={open}>
<DialogTitle id={id}>Edit expression</DialogTitle>
{allowTypeChange &&
<FormControl className={classes.margined}>
<Select
labelId={selectLabelId}
id={selectId}
value={getFilterType(filter)}
onChange={handleTypeChange}
>
<MenuItem value={FilterTypeEnum.CONST}>Constant</MenuItem>
<MenuItem value={FilterTypeEnum.MATCHING}>Matching</MenuItem>
<MenuItem value={FilterTypeEnum.TIME}>Date / Time</MenuItem>
</Select>
</FormControl>
}
{control}
<Button onClick={handleClose} className={classes.margined}>
Done
</Button>
</Dialog>
);
}
export function TagEqualsExpressionControl(props) {
const classes = useStyles();
const { name, onClick } = props;
return (
<>
<Button
variant="outlined"
className={classes.filterexpcontrol}
aria-controls="simple-menu" aria-haspopup="true"
onClick={onClick}
startIcon={<LabelIcon />}>
{name}
</Button>
</>
);
}
export function AlbumEqualsExpressionControl(props) {
const classes = useStyles();
const { name, onClick } = props;
return (
<>
<Button
variant="outlined"
className={classes.filterexpcontrol}
aria-controls="simple-menu" aria-haspopup="true"
onClick={onClick}
startIcon={<ImportContactsIcon />}>
{name}
</Button>
</>
);
}
export function ImageNameEqualsExpressionControl(props) {
const classes = useStyles();
const { name, onClick } = props;
return (
<>
<Button
variant="outlined"
className={classes.filterexpcontrol}
aria-controls="simple-menu" aria-haspopup="true"
onClick={onClick}
startIcon={<PhotoIcon />}>
{name}
</Button>
</>
);
}
export function ImageNameNaturalMatchExpressionControl(props) {
const classes = useStyles();
const { name, onClick } = props;
return (
<>
<Button
variant="outlined"
className={classes.filterexpcontrol}
aria-controls="simple-menu" aria-haspopup="true"
onClick={onClick}
startIcon={<><PhotoIcon /><SearchIcon /></>}>
{name}
</Button>
</>
);
}
export function MatchingFilterExpressionControl(props) {
const classes = useStyles();
const { expr, onClick } = props;
var pretty_name = expr.match_from
.replace(/^"/g, '')
.replace(/"$/g, '');
if (expr.match_type === MatchTypeEnum.MATCH_TAG_EQUALS) {
return <TagEqualsExpressionControl name={pretty_name} onClick={onClick} />
} else if (expr.match_type === MatchTypeEnum.MATCH_TAG_EQUALS_OR_CHILD) {
return <TagEqualsExpressionControl name={pretty_name + '(/...)'} onClick={onClick} />
} else if (expr.match_type === MatchTypeEnum.MATCH_ALBUM_EQUALS) {
return <AlbumEqualsExpressionControl name={pretty_name} onClick={onClick} />
} else if (expr.match_type === MatchTypeEnum.MATCH_ALBUM_EQUALS_OR_CHILD) {
return <AlbumEqualsExpressionControl name={pretty_name + '(/...)'} onClick={onClick} />
} else if (expr.match_type === MatchTypeEnum.MATCH_IMAGE_NAME_EQUALS) {
return <ImageNameEqualsExpressionControl name={pretty_name} onClick={onClick} />
} else if (expr.match_type === MatchTypeEnum.MATCH_IMAGE_NAME_NATURAL) {
return <ImageNameNaturalMatchExpressionControl name={pretty_name} onClick={onClick} />
} else if (expr.match_type === MatchTypeEnum.MATCH_ALBUM_NAME_EQUALS) {
return <AlbumEqualsExpressionControl name={'(.../)' + pretty_name + '(/...)'} onClick={onClick} />
}
throw new Error('Cannot render matching filter control: unsupported type.');
}
export function LogicalOperatorFilterExpressionControl(props) {
const classes = useStyles();
const { expr, onClick, onChange } = props;
var opstring = "";
if (expr.operator === LogicalOperatorEnum.AND) {
opstring = " AND ";
} else if (expr.operator === LogicalOperatorEnum.OR) {
opstring = " OR ";
}
var _ = require('lodash');
const handleAChanged = (new_a) => {
if (new_a == null) {
onChange(expr.sub_filter_b);
return;
}
var new_me = _.cloneDeep(expr);
new_me.sub_filter_a = new_a;
onChange(new_me);
}
const handleBChanged = (new_b) => {
if (new_b == null) {
onChange(expr.sub_filter_a);
return;
}
var new_me = _.cloneDeep(expr);
new_me.sub_filter_b = new_b;
onChange(new_me);
}
return (
<>
<Box className={classes.logic_op_outer}>
<Box className={classes.logic_op_sbs}>
<Box>
<Box className={classes.logic_op_subexpr}>
<FilterExpressionControl expr={expr.sub_filter_a} onChange={handleAChanged} isRoot={false} />
</Box>
</Box>
<Box>
<Box className={classes.logic_op_subexpr}>
<FilterExpressionControl expr={expr.sub_filter_b} onChange={handleBChanged} isRoot={false} />
</Box>
</Box>
</Box>
<Button
variant="outlined"
className={classes.filterexpcontrol + " " + classes.logic_op_sbs}
aria-controls="simple-menu" aria-haspopup="true"
onClick={onClick}
>
{opstring}
</Button>
</Box>
</>
)
}
export function ConstFilterExpressionControl(props) {
const classes = useStyles();
const { expr, onClick } = props;
return (
<Button
variant="outlined"
className={classes.filterexpcontrol}
aria-controls="simple-menu" aria-haspopup="true"
onClick={onClick}
>
{JSON.stringify(expr.constval)}
</Button>
);
}
export function NegationExpressionControl(props) {
const classes = useStyles();
const { expr, onClick, onChange } = props;
var _ = require('lodash');
function handleBodyChanged(body) {
var new_filter = _.cloneDeep(expr);
new_filter.body = body;
onChange(new_filter);
}
return (
<>
<Box className={classes.logic_op_outer}>
<Box className={classes.logic_op_sbs}>
<Box>
<Box className={classes.logic_op_subexpr}>
<FilterExpressionControl expr={expr.body} onChange={handleBodyChanged} isRoot={false} />
</Box>
</Box>
</Box>
<Button
variant="outlined"
className={classes.filterexpcontrol + " " + classes.logic_op_sbs}
aria-controls="simple-menu" aria-haspopup="true"
onClick={onClick}
>
NOT
</Button>
</Box>
</>
)
}
export function TimeFilterExpressionControl(props) {
const classes = useStyles();
const { expr, onClick, onChange } = props;
const relation = "";
if (expr.type == TimeFilterTypeEnum.BEFORE) {
relation = "Before: ";
} else if (expr.type == TimeFilterTypeEnum.AFTER) {
relation = "After: ";
} else {
throw new Error("Unsupported time filter type.");
}
return (
<Button
variant="outlined"
className={classes.filterexpcontrol}
aria-controls="simple-menu" aria-haspopup="true"
onClick={onClick}
startIcon={<ScheduleIcon />}>
>
{relation + expr.time.toLocaleStr()}
</Button>
);
}
export function FilterExpressionControl(props) {
const { expr, onChange, isRoot } = props;
const [anchorEl, setAnchorEl] = React.useState(null);
const [editDialogOpen, setEditDialogOpen] = React.useState(false);
const [combineDialogOpen, setCombineDialogOpen] = React.useState(false);
const [combineExpr, setCombineExpr] = React.useState(new LogicalOperatorFilter(
expr.result_type,
expr,
new ConstFilter(expr.result_type, true),
LogicalOperatorEnum.AND)
);
var _ = require('lodash');
const menu_id = _.uniqueId("filter_menu_");
const handleClick = event => {
setAnchorEl(event.currentTarget);
};
const handleCloseMenu = () => {
setAnchorEl(null);
};
const handleOpenEditDialog = (filter) => {
handleCloseMenu();
setEditDialogOpen(true);
}
const handleCloseEditFilterDialog = (filter) => {
setEditDialogOpen(false);
onChange(filter.simplify());
};
const handleRemove = () => {
// For negation filters, removal means replacing the negation node by its child.
// In all other cases, removal means complete deletion of the subtree.
if (expr instanceof NegationFilter) {
onChange(expr.body);
return;
}
onChange(null);
}
const handleCloseCombineDialog = (filter) => {
setCombineDialogOpen(false);
var new_filter = _.cloneDeep(combineExpr);
new_filter.sub_filter_b = filter;
onChange(new_filter.simplify());
}
const handleAnd = () => {
handleCloseMenu();
setCombineExpr(new LogicalOperatorFilter(
expr.result_type,
expr,
new ConstFilter(expr.result_type, true), LogicalOperatorEnum.AND
));
setCombineDialogOpen(true);
}
const handleOr = () => {
handleCloseMenu();
setCombineExpr(new LogicalOperatorFilter(
expr.result_type,
expr,
new ConstFilter(expr.result_type, true), LogicalOperatorEnum.OR
));
setCombineDialogOpen(true);
}
const handleNegation = () => {
handleCloseMenu();
var new_filter = new NegationFilter(expr.result_type, expr);
onChange(new_filter.simplify());
}
var filter_elem = false;
if (expr instanceof ConstFilter) {
filter_elem = <ConstFilterExpressionControl {...props} onClick={handleClick} />
} else if (expr instanceof LogicalOperatorFilter) {
filter_elem = <LogicalOperatorFilterExpressionControl {...props} onClick={handleClick} onChange={onChange} />
} else if (expr instanceof MatchingFilter) {
filter_elem = <MatchingFilterExpressionControl {...props} onClick={handleClick} />
} else if (expr instanceof NegationFilter) {
filter_elem = <NegationExpressionControl {...props} onClick={handleClick} onChange={onChange} />
} else if (expr instanceof TimeFilter) {
filter_elem = <TimeFilterExpressionControl {...props} onClick={handleClick} onChange={onChange} />
} else {
throw new Error('Unsupported filter expression');
}
// If this is the root node, removing it is not allowed.
// Other nodes may be removed.
// The only exception is a negation node: removing that will replace it by its child.
var allowRemove = !isRoot || (expr instanceof NegationFilter);
return (
<>
{filter_elem}
<Menu
id={menu_id}
anchorEl={anchorEl}
keepMounted
open={Boolean(anchorEl)}
onClose={handleCloseMenu}
>
<MenuItem onClick={handleOpenEditDialog}>Edit...</MenuItem>
<MenuItem onClick={handleAnd}>And...</MenuItem>
<MenuItem onClick={handleOr}>Or...</MenuItem>
<MenuItem onClick={handleNegation}>Negate</MenuItem>
{allowRemove && <MenuItem onClick={handleRemove}>Remove</MenuItem>}
</Menu>
<EditFilterExpressionDialog
onClose={handleCloseEditFilterDialog}
startingFilter={expr}
open={editDialogOpen}
/>
<EditFilterExpressionDialog
onClose={handleCloseCombineDialog}
startingFilter={combineExpr.sub_filter_b}
open={combineDialogOpen}
/>
</>
);
}
export function FilterControl(props) {
const classes = useStyles();
const { filter, onChange, resultType, resultTypeString } = props;
const enabled = !filter_is_const_false(filter);
function handleResultToggled() {
if (enabled) {
onChange(new ConstFilter(resultType, false));
}
else {
onChange(new ConstFilter(resultType, true));
}
}
return (
<>
<Box className={classes.filtercontrol}>
<FormControlLabel
control={
<Switch
checked={enabled}
onChange={handleResultToggled}
color="primary"
/>
}
label={resultTypeString + ':'}
/>
<FilterExpressionControl expr={filter} onChange={onChange} isRoot={true} />
</Box>
</>
);
}
export function UserQueryWidget(props) {
const { userQuery, onChange } = props;
const classes = useStyles();
var _ = require('lodash');
function handleImageFilterChange(filter) {
var q = _.cloneDeep(userQuery);
q.image_filter = filter;
onChange(q);
}
function handleAlbumFilterChange(filter) {
var q = _.cloneDeep(userQuery);
q.album_filter = filter;
onChange(q);
}
function handleTagFilterChange(filter) {
var q = _.cloneDeep(userQuery);
q.tag_filter = filter;
onChange(q);
}
return (
<>
<Box className={classes.bordered + " " + classes.margined}>
<Box className={classes.margined}>
<FilterControl
filter={userQuery.image_filter}
onChange={handleImageFilterChange}
resultType={ResultTypeEnum.IMAGE}
resultTypeString="Images" />
</Box>
</Box>
<Box className={classes.bordered + " " + classes.margined}>
<Box className={classes.margined}>
<FilterControl
filter={userQuery.album_filter}
onChange={handleAlbumFilterChange}
resultType={ResultTypeEnum.ALBUM}
resultTypeString="Albums" />
</Box>
</Box >
<Box className={classes.bordered + " " + classes.margined}>
<Box className={classes.margined}>
<FilterControl
filter={userQuery.tag_filter}
onChange={handleTagFilterChange}
resultType={ResultTypeEnum.TAG}
resultTypeString="Tags" />
</Box>
</Box >
</>
);
}