diff --git a/package.json b/package.json index 683440c..ee1516d 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "@material-ui/pickers": "^3.2.10", "@material-ui/styles": "^4.6.0", "etesync": "^0.3.1", + "fuse.js": "^5.0.9-beta", "ical.js": "^1.2.2", "immutable": "^4.0.0-rc.12", "localforage": "^1.7.3", @@ -22,6 +23,7 @@ "react-router": "^4.3.1", "react-router-dom": "^4.3.1", "react-scripts": "3.3.0", + "react-transition-group": "^4.3.0", "react-virtualized": "^9.21.2", "redux": "^4.0.1", "redux-actions": "^2.6.4", diff --git a/src/components/Tasks/TaskList.tsx b/src/components/Tasks/TaskList.tsx index bfa93d8..0cc9ae7 100644 --- a/src/components/Tasks/TaskList.tsx +++ b/src/components/Tasks/TaskList.tsx @@ -10,13 +10,16 @@ import { List } from '../../widgets/List'; import { TaskType, PimType } from '../../pim-types'; import Divider from '@material-ui/core/Divider'; 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 Fuse from 'fuse.js'; + import TaskListItem from './TaskListItem'; import Sidebar from './Sidebar'; import Toolbar from './Toolbar'; +import QuickAdd from './QuickAdd'; import { StoreState } from '../../store'; @@ -87,6 +90,12 @@ function getSortFunction(sortOrder: string) { }; } +const useStyles = makeStyles((theme) => ({ + topBar: { + backgroundColor: theme.palette.primary[500], + }, +})); + interface PropsType { entries: TaskType[]; collections: EteSync.CollectionInfo[]; @@ -96,13 +105,31 @@ interface PropsType { export default function TaskList(props: PropsType) { const [showCompleted, setShowCompleted] = React.useState(false); + const [searchTerm, setSearchTerm] = React.useState(''); const settings = useSelector((state: StoreState) => state.settings.taskSettings); const { filterBy, sortBy } = settings; const theme = useTheme(); + const classes = useStyles(); 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; @@ -134,20 +161,31 @@ export default function TaskList(props: PropsType) { return ( - - - + + {/* spacer */} - + + + + + + + + + + {props.collections?.[0] && } + {itemList} diff --git a/src/components/Tasks/Toolbar.tsx b/src/components/Tasks/Toolbar.tsx index 301b15e..115e820 100644 --- a/src/components/Tasks/Toolbar.tsx +++ b/src/components/Tasks/Toolbar.tsx @@ -8,8 +8,12 @@ import IconButton from '@material-ui/core/IconButton'; import MoreVertIcon from '@material-ui/icons/MoreVert'; import MenuItem from '@material-ui/core/MenuItem'; import SortIcon from '@material-ui/icons/Sort'; - -import QuickAdd from './QuickAdd'; +import SearchIcon from '@material-ui/icons/Search'; +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'; @@ -20,23 +24,54 @@ import { StoreState } from '../../store'; 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 { defaultCollection: EteSync.CollectionInfo; onItemSave: (item: PimType, journalUid: string, originalItem?: PimType) => Promise; showCompleted: boolean; setShowCompleted: (completed: boolean) => void; + searchTerm: string; + setSearchTerm: (term: string) => void; } 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); const [optionsAnchorEl, setOptionsAnchorEl] = React.useState(null); + const classes = useStyles(); + const dispatch = useDispatch(); const taskSettings = useSelector((state: StoreState) => state.settings.taskSettings); const { sortBy } = taskSettings; + const toggleSearchField = () => { + if (showSearchField) { + setSearchTerm(''); + } + setShowSearchField(!showSearchField); + }; + const handleSortChange = (sort: string) => { dispatch(setSettings({ taskSettings: { ...taskSettings, sortBy: sort } })); setSortAnchorEl(null); @@ -49,11 +84,38 @@ export default function Toolbar(props: PropsType) { }); return ( -
- {defaultCollection && } +
+ + {(state) => ( + setSearchTerm(e.target.value)} + InputProps={{ + startAdornment: ( + + + + ), + }} + /> + )} + + +
+ + {showSearchField ? : } + +
-
+
- -
+