From effd74e0b216e5725ccdfc96e6b3f09c6909b9db Mon Sep 17 00:00:00 2001 From: Andrew P Maney Date: Tue, 17 Mar 2020 13:24:46 +0200 Subject: [PATCH] Tasks: adds filter by tag feature Squash of #94 --- src/components/Tasks/Sidebar.tsx | 62 +++++++++++++++++++++++++++ src/components/Tasks/TaskList.tsx | 70 ++++++++++++++++++++++--------- src/store/reducers.ts | 16 +++++-- src/widgets/List.tsx | 3 ++ 4 files changed, 128 insertions(+), 23 deletions(-) create mode 100644 src/components/Tasks/Sidebar.tsx diff --git a/src/components/Tasks/Sidebar.tsx b/src/components/Tasks/Sidebar.tsx new file mode 100644 index 0000000..69fff91 --- /dev/null +++ b/src/components/Tasks/Sidebar.tsx @@ -0,0 +1,62 @@ +import * as React from 'react'; + +import { useSelector, useDispatch } from 'react-redux'; + +import InboxIcon from '@material-ui/icons/Inbox'; +import LabelIcon from '@material-ui/icons/LabelOutlined'; + +import { setSettings } from '../../store/actions'; +import { StoreState } from '../../store'; + +import { List, ListItem, ListSubheader } from '../../widgets/List'; + +interface ListItemPropsType { + name: string | null; + icon?: React.ReactElement; + primaryText: string; + amount?: number; +} + +function SidebarListItem(props: ListItemPropsType) { + const { name, icon, primaryText, amount } = props; + const dispatch = useDispatch(); + const taskSettings = useSelector((state: StoreState) => state.settings.taskSettings); + const { filterBy } = taskSettings; + + const handleClick = () => { + dispatch(setSettings({ taskSettings: { ...taskSettings, filterBy: name } })); + }; + + return ( + {amount}} + primaryText={primaryText} + /> + ); +} + +export default function Sidebar(props: { tags: Map, totalTasks: number }) { + const { tags, totalTasks } = props; + + const tagsList = [...tags].sort(([a], [b]) => a.localeCompare(b)).map(([tag, amount]) => ( + } + amount={amount} + /> + )); + + return ( + + } amount={totalTasks} /> + + Tags + {tagsList} + + ); +} \ No newline at end of file diff --git a/src/components/Tasks/TaskList.tsx b/src/components/Tasks/TaskList.tsx index 1b3e065..3b5559c 100644 --- a/src/components/Tasks/TaskList.tsx +++ b/src/components/Tasks/TaskList.tsx @@ -13,9 +13,16 @@ import { TaskType, PimType } from '../../pim-types'; import FormControlLabel from '@material-ui/core/FormControlLabel'; import Checkbox from '@material-ui/core/Checkbox'; import Divider from '@material-ui/core/Divider'; +import Grid from '@material-ui/core/Grid'; +import { useTheme } from '@material-ui/core/styles'; + +import { useSelector } from 'react-redux'; import TaskListItem from './TaskListItem'; import QuickAdd from './QuickAdd'; +import Sidebar from './Sidebar'; + +import { StoreState } from '../../store'; const sortSelector = createSelector( (entries: TaskType[]) => entries, @@ -31,7 +38,26 @@ interface PropsType { export default React.memo(function TaskList(props: PropsType) { const [showCompleted, setShowCompleted] = React.useState(false); - const entries = props.entries.filter((x) => showCompleted || !x.finished); + const settings = useSelector((state: StoreState) => state.settings.taskSettings); + const { filterBy } = settings; + const theme = useTheme(); + + const potentialEntries = props.entries.filter((x) => showCompleted || !x.finished); + let entries; + const tagPrefix = 'tag:'; + if (filterBy?.startsWith(tagPrefix)) { + const tag = filterBy.slice(tagPrefix.length); + entries = potentialEntries.filter((x) => x.tags.includes(tag)); + } else { + entries = potentialEntries; + } + + // TODO: memoize + const tags = new Map(); + potentialEntries.forEach((entry) => entry.tags.forEach((tag) => { + tags.set(tag, (tags.get(tag) ?? 0) + 1); + })); + const sortedEntries = sortSelector(entries); const itemList = sortedEntries.map((entry) => { @@ -48,23 +74,29 @@ export default React.memo(function TaskList(props: PropsType) { }); return ( - <> - -
- {props.collections && } - - setShowCompleted(!showCompleted)} /> - } - label="Show Completed" - /> -
- - - - {itemList} - - + + + + + + + +
+ {props.collections && } + + setShowCompleted(!showCompleted)} /> + } + label="Show Completed" + /> +
+ + + + {itemList} + +
+
); }); diff --git a/src/store/reducers.ts b/src/store/reducers.ts index 59e8c15..9480459 100644 --- a/src/store/reducers.ts +++ b/src/store/reducers.ts @@ -28,13 +28,13 @@ export type UserInfoData = EteSync.UserInfo; export const encryptionKeyReducer = handleActions( { - [actions.deriveKey.toString()]: (_state: {key: string | null}, action: any) => ( + [actions.deriveKey.toString()]: (_state: { key: string | null }, action: any) => ( { key: action.payload } ), - [actions.resetKey.toString()]: (_state: {key: string | null}, _action: any) => ( + [actions.resetKey.toString()]: (_state: { key: string | null }, _action: any) => ( { key: null } ), - [actions.logout.toString()]: (_state: {key: string | null}, _action: any) => { + [actions.logout.toString()]: (_state: { key: string | null }, _action: any) => { return { out: true, key: null }; }, }, @@ -272,6 +272,9 @@ export const errorsReducer = handleActions( // FIXME Move all the below (potentially the fetchCount ones too) to their own file export interface SettingsType { locale: string; + taskSettings: { + filterBy: string | null; + }; } export const settingsReducer = handleActions( @@ -280,5 +283,10 @@ export const settingsReducer = handleActions( { ...state, ...action.payload } ), }, - { locale: 'en-gb' } + { + locale: 'en-gb', + taskSettings: { + filterBy: null, + }, + } ); diff --git a/src/widgets/List.tsx b/src/widgets/List.tsx index 53e96c2..d9af26e 100644 --- a/src/widgets/List.tsx +++ b/src/widgets/List.tsx @@ -45,6 +45,7 @@ interface ListItemPropsType { href?: string; insetChildren?: boolean; nestedItems?: React.ReactNode[]; + selected?: boolean; } export const ListItem = React.memo(function ListItem(_props: ListItemPropsType) { @@ -60,6 +61,7 @@ export const ListItem = React.memo(function ListItem(_props: ListItemPropsType) style, insetChildren, nestedItems, + selected, } = _props; const extraProps = (onClick || href) ? { @@ -74,6 +76,7 @@ export const ListItem = React.memo(function ListItem(_props: ListItemPropsType) {leftIcon && (