Tasks: add search functionality
parent
ba00be300e
commit
212dfc7095
|
@ -10,6 +10,7 @@
|
||||||
"@material-ui/pickers": "^3.2.10",
|
"@material-ui/pickers": "^3.2.10",
|
||||||
"@material-ui/styles": "^4.6.0",
|
"@material-ui/styles": "^4.6.0",
|
||||||
"etesync": "^0.3.1",
|
"etesync": "^0.3.1",
|
||||||
|
"fuse.js": "^5.0.9-beta",
|
||||||
"ical.js": "^1.2.2",
|
"ical.js": "^1.2.2",
|
||||||
"immutable": "^4.0.0-rc.12",
|
"immutable": "^4.0.0-rc.12",
|
||||||
"localforage": "^1.7.3",
|
"localforage": "^1.7.3",
|
||||||
|
@ -22,6 +23,7 @@
|
||||||
"react-router": "^4.3.1",
|
"react-router": "^4.3.1",
|
||||||
"react-router-dom": "^4.3.1",
|
"react-router-dom": "^4.3.1",
|
||||||
"react-scripts": "3.3.0",
|
"react-scripts": "3.3.0",
|
||||||
|
"react-transition-group": "^4.3.0",
|
||||||
"react-virtualized": "^9.21.2",
|
"react-virtualized": "^9.21.2",
|
||||||
"redux": "^4.0.1",
|
"redux": "^4.0.1",
|
||||||
"redux-actions": "^2.6.4",
|
"redux-actions": "^2.6.4",
|
||||||
|
|
|
@ -10,13 +10,16 @@ import { List } from '../../widgets/List';
|
||||||
import { TaskType, PimType } from '../../pim-types';
|
import { TaskType, PimType } from '../../pim-types';
|
||||||
import Divider from '@material-ui/core/Divider';
|
import Divider from '@material-ui/core/Divider';
|
||||||
import Grid from '@material-ui/core/Grid';
|
import Grid from '@material-ui/core/Grid';
|
||||||
import { useTheme } from '@material-ui/core/styles';
|
import { useTheme, makeStyles } from '@material-ui/core/styles';
|
||||||
|
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
|
|
||||||
|
import Fuse from 'fuse.js';
|
||||||
|
|
||||||
import TaskListItem from './TaskListItem';
|
import TaskListItem from './TaskListItem';
|
||||||
import Sidebar from './Sidebar';
|
import Sidebar from './Sidebar';
|
||||||
import Toolbar from './Toolbar';
|
import Toolbar from './Toolbar';
|
||||||
|
import QuickAdd from './QuickAdd';
|
||||||
|
|
||||||
import { StoreState } from '../../store';
|
import { StoreState } from '../../store';
|
||||||
|
|
||||||
|
@ -87,6 +90,12 @@ function getSortFunction(sortOrder: string) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme) => ({
|
||||||
|
topBar: {
|
||||||
|
backgroundColor: theme.palette.primary[500],
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
interface PropsType {
|
interface PropsType {
|
||||||
entries: TaskType[];
|
entries: TaskType[];
|
||||||
collections: EteSync.CollectionInfo[];
|
collections: EteSync.CollectionInfo[];
|
||||||
|
@ -96,13 +105,31 @@ interface PropsType {
|
||||||
|
|
||||||
export default function TaskList(props: PropsType) {
|
export default function TaskList(props: PropsType) {
|
||||||
const [showCompleted, setShowCompleted] = React.useState(false);
|
const [showCompleted, setShowCompleted] = React.useState(false);
|
||||||
|
const [searchTerm, setSearchTerm] = React.useState('');
|
||||||
const settings = useSelector((state: StoreState) => state.settings.taskSettings);
|
const settings = useSelector((state: StoreState) => state.settings.taskSettings);
|
||||||
const { filterBy, sortBy } = settings;
|
const { filterBy, sortBy } = settings;
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
const classes = useStyles();
|
||||||
|
|
||||||
const potentialEntries = React.useMemo(
|
const potentialEntries = React.useMemo(
|
||||||
() => props.entries.filter((x) => showCompleted || !x.finished),
|
() => {
|
||||||
[showCompleted, props.entries]
|
if (searchTerm) {
|
||||||
|
const result = new Fuse(props.entries, {
|
||||||
|
shouldSort: true,
|
||||||
|
threshold: 0.6,
|
||||||
|
maxPatternLength: 32,
|
||||||
|
minMatchCharLength: 2,
|
||||||
|
keys: [
|
||||||
|
'title',
|
||||||
|
'desc',
|
||||||
|
],
|
||||||
|
}).search(searchTerm);
|
||||||
|
return result.map((x) => x.item);
|
||||||
|
} else {
|
||||||
|
return props.entries.filter((x) => showCompleted || !x.finished);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[showCompleted, props.entries, searchTerm]
|
||||||
);
|
);
|
||||||
|
|
||||||
let entries;
|
let entries;
|
||||||
|
@ -134,20 +161,31 @@ export default function TaskList(props: PropsType) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid container spacing={4}>
|
<Grid container spacing={4}>
|
||||||
|
<Grid item xs={3} className={classes.topBar}>
|
||||||
|
{/* spacer */}
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item xs={9} className={classes.topBar}>
|
||||||
|
<Toolbar
|
||||||
|
defaultCollection={props.collections?.[0]}
|
||||||
|
onItemSave={props.onItemSave}
|
||||||
|
showCompleted={showCompleted}
|
||||||
|
setShowCompleted={setShowCompleted}
|
||||||
|
searchTerm={searchTerm}
|
||||||
|
setSearchTerm={setSearchTerm}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
<Grid item xs={3} style={{ borderRight: `1px solid ${theme.palette.divider}` }}>
|
<Grid item xs={3} style={{ borderRight: `1px solid ${theme.palette.divider}` }}>
|
||||||
<Sidebar tasks={potentialEntries} />
|
<Sidebar tasks={potentialEntries} />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid item xs>
|
<Grid item xs>
|
||||||
<Toolbar
|
|
||||||
defaultCollection={props.collections?.[0]}
|
{props.collections?.[0] && <QuickAdd style={{ flexGrow: 1, marginRight: '0.75em' }} onSubmit={props.onItemSave} defaultCollection={props.collections?.[0]} />}
|
||||||
onItemSave={props.onItemSave}
|
|
||||||
showCompleted={showCompleted}
|
|
||||||
setShowCompleted={setShowCompleted}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Divider style={{ marginTop: '1em' }} />
|
<Divider style={{ marginTop: '1em' }} />
|
||||||
|
|
||||||
<List>
|
<List>
|
||||||
{itemList}
|
{itemList}
|
||||||
</List>
|
</List>
|
||||||
|
|
|
@ -8,8 +8,12 @@ import IconButton from '@material-ui/core/IconButton';
|
||||||
import MoreVertIcon from '@material-ui/icons/MoreVert';
|
import MoreVertIcon from '@material-ui/icons/MoreVert';
|
||||||
import MenuItem from '@material-ui/core/MenuItem';
|
import MenuItem from '@material-ui/core/MenuItem';
|
||||||
import SortIcon from '@material-ui/icons/Sort';
|
import SortIcon from '@material-ui/icons/Sort';
|
||||||
|
import SearchIcon from '@material-ui/icons/Search';
|
||||||
import QuickAdd from './QuickAdd';
|
import CloseIcon from '@material-ui/icons/Close';
|
||||||
|
import TextField from '@material-ui/core/TextField';
|
||||||
|
import { makeStyles } from '@material-ui/core/styles';
|
||||||
|
import { Transition } from 'react-transition-group';
|
||||||
|
import InputAdornment from '@material-ui/core/InputAdornment';
|
||||||
|
|
||||||
import { PimType } from '../../pim-types';
|
import { PimType } from '../../pim-types';
|
||||||
|
|
||||||
|
@ -20,23 +24,54 @@ import { StoreState } from '../../store';
|
||||||
|
|
||||||
import Menu from '../../widgets/Menu';
|
import Menu from '../../widgets/Menu';
|
||||||
|
|
||||||
|
const transitionTimeout = 300;
|
||||||
|
|
||||||
|
const transitionStyles = {
|
||||||
|
entering: { visibility: 'visible', width: '100%', overflow: 'hidden' },
|
||||||
|
entered: { visibility: 'visible', width: '100%' },
|
||||||
|
exiting: { visibility: 'visible', width: '0%', overflow: 'hidden' },
|
||||||
|
exited: { visibility: 'hidden', width: '0%' },
|
||||||
|
};
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme) => ({
|
||||||
|
button: {
|
||||||
|
marginRight: theme.spacing(1),
|
||||||
|
},
|
||||||
|
textField: {
|
||||||
|
transition: `width ${transitionTimeout}ms`,
|
||||||
|
marginRight: theme.spacing(1),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
interface PropsType {
|
interface PropsType {
|
||||||
defaultCollection: EteSync.CollectionInfo;
|
defaultCollection: EteSync.CollectionInfo;
|
||||||
onItemSave: (item: PimType, journalUid: string, originalItem?: PimType) => Promise<void>;
|
onItemSave: (item: PimType, journalUid: string, originalItem?: PimType) => Promise<void>;
|
||||||
showCompleted: boolean;
|
showCompleted: boolean;
|
||||||
setShowCompleted: (completed: boolean) => void;
|
setShowCompleted: (completed: boolean) => void;
|
||||||
|
searchTerm: string;
|
||||||
|
setSearchTerm: (term: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Toolbar(props: PropsType) {
|
export default function Toolbar(props: PropsType) {
|
||||||
const { defaultCollection, onItemSave, showCompleted, setShowCompleted } = props;
|
const { showCompleted, setShowCompleted, searchTerm, setSearchTerm } = props;
|
||||||
|
|
||||||
|
const [showSearchField, setShowSearchField] = React.useState(false);
|
||||||
const [sortAnchorEl, setSortAnchorEl] = React.useState<null | HTMLElement>(null);
|
const [sortAnchorEl, setSortAnchorEl] = React.useState<null | HTMLElement>(null);
|
||||||
const [optionsAnchorEl, setOptionsAnchorEl] = React.useState<null | HTMLElement>(null);
|
const [optionsAnchorEl, setOptionsAnchorEl] = React.useState<null | HTMLElement>(null);
|
||||||
|
|
||||||
|
const classes = useStyles();
|
||||||
|
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const taskSettings = useSelector((state: StoreState) => state.settings.taskSettings);
|
const taskSettings = useSelector((state: StoreState) => state.settings.taskSettings);
|
||||||
const { sortBy } = taskSettings;
|
const { sortBy } = taskSettings;
|
||||||
|
|
||||||
|
const toggleSearchField = () => {
|
||||||
|
if (showSearchField) {
|
||||||
|
setSearchTerm('');
|
||||||
|
}
|
||||||
|
setShowSearchField(!showSearchField);
|
||||||
|
};
|
||||||
|
|
||||||
const handleSortChange = (sort: string) => {
|
const handleSortChange = (sort: string) => {
|
||||||
dispatch(setSettings({ taskSettings: { ...taskSettings, sortBy: sort } }));
|
dispatch(setSettings({ taskSettings: { ...taskSettings, sortBy: sort } }));
|
||||||
setSortAnchorEl(null);
|
setSortAnchorEl(null);
|
||||||
|
@ -49,11 +84,38 @@ export default function Toolbar(props: PropsType) {
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
<div style={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center' }}>
|
||||||
{defaultCollection && <QuickAdd style={{ flexGrow: 1, marginRight: '0.75em' }} onSubmit={onItemSave} defaultCollection={defaultCollection} />}
|
<Transition in={showSearchField} timeout={transitionTimeout}>
|
||||||
|
{(state) => (
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
placeholder="Search"
|
||||||
|
value={searchTerm}
|
||||||
|
color="secondary"
|
||||||
|
variant="standard"
|
||||||
|
className={classes.textField}
|
||||||
|
style={transitionStyles[state]}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
InputProps={{
|
||||||
|
startAdornment: (
|
||||||
|
<InputAdornment position="start">
|
||||||
|
<SearchIcon />
|
||||||
|
</InputAdornment>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Transition>
|
||||||
|
|
||||||
<div>
|
<div className={classes.button}>
|
||||||
|
<IconButton size="small" onClick={toggleSearchField}>
|
||||||
|
{showSearchField ? <CloseIcon /> : <SearchIcon />}
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={classes.button}>
|
||||||
<IconButton
|
<IconButton
|
||||||
|
size="small"
|
||||||
aria-label="more"
|
aria-label="more"
|
||||||
aria-controls="long-menu"
|
aria-controls="long-menu"
|
||||||
aria-haspopup="true"
|
aria-haspopup="true"
|
||||||
|
@ -75,9 +137,9 @@ export default function Toolbar(props: PropsType) {
|
||||||
</Menu>
|
</Menu>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className={classes.button}>
|
||||||
<div>
|
|
||||||
<IconButton
|
<IconButton
|
||||||
|
size="small"
|
||||||
aria-label="more"
|
aria-label="more"
|
||||||
aria-controls="long-menu"
|
aria-controls="long-menu"
|
||||||
aria-haspopup="true"
|
aria-haspopup="true"
|
||||||
|
|
|
@ -5430,6 +5430,11 @@ functional-red-black-tree@^1.0.1:
|
||||||
resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
|
resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
|
||||||
integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=
|
integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=
|
||||||
|
|
||||||
|
fuse.js@^5.0.9-beta:
|
||||||
|
version "5.0.9-beta"
|
||||||
|
resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-5.0.9-beta.tgz#5122612868bf0a65f451085f5bd134b879502729"
|
||||||
|
integrity sha512-gx62Ba4GPfAaPJhE6ubfPk2OeVVMhPFmNCrLeLRVZybgDUxcgHz+tTNLC42yGs79DVQCSl17TN8fI8i9tyPv/g==
|
||||||
|
|
||||||
gauge@~2.7.3:
|
gauge@~2.7.3:
|
||||||
version "2.7.4"
|
version "2.7.4"
|
||||||
resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"
|
resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"
|
||||||
|
|
Loading…
Reference in New Issue