etesync/src/components/Tasks/TaskList.tsx

200 lines
5.5 KiB
TypeScript

// SPDX-FileCopyrightText: © 2017 EteSync Authors
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import * as EteSync from 'etesync';
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, 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';
function sortCompleted(a: TaskType, b: TaskType) {
return (!!a.finished === !!b.finished) ? 0 : (a.finished) ? 1 : -1;
}
function sortLastModifiedDate(aIn: TaskType, bIn: TaskType) {
const a = aIn.lastModified?.toJSDate() ?? new Date(0);
const b = bIn.lastModified?.toJSDate() ?? new Date(0);
return (a > b) ? -1 : (a < b) ? 1 : 0;
}
function sortDueDate(aIn: TaskType, bIn: TaskType) {
const impossiblyLargeDate = 8640000000000000;
const a = aIn.dueDate?.toJSDate() ?? new Date(impossiblyLargeDate);
const b = bIn.dueDate?.toJSDate() ?? new Date(impossiblyLargeDate);
return (a < b) ? -1 : (a > b) ? 1 : 0;
}
function sortPriority(aIn: TaskType, bIn: TaskType) {
// Intentionally converts 0/undefined to 10 (1 more than lowest priority) to sort to back of the list
const a = aIn.priority || 10;
const b = bIn.priority || 10;
return a - b;
}
function sortTitle(aIn: TaskType, bIn: TaskType) {
const a = aIn.title ?? '';
const b = bIn.title ?? '';
return a.localeCompare(b);
}
function getSortFunction(sortOrder: string) {
const sortFunctions: (typeof sortTitle)[] = [sortCompleted];
switch (sortOrder) {
case 'smart':
sortFunctions.push(sortPriority);
sortFunctions.push(sortDueDate);
sortFunctions.push(sortTitle);
break;
case 'dueDate':
sortFunctions.push(sortDueDate);
break;
case 'priority':
sortFunctions.push(sortPriority);
sortFunctions.push(sortDueDate);
break;
case 'title':
sortFunctions.push(sortTitle);
break;
case 'lastModifiedDate':
// Do nothing because it's the last sort function anyway
break;
}
sortFunctions.push(sortLastModifiedDate);
return (a: TaskType, b: TaskType) => {
for (const sortFunction of sortFunctions) {
const ret = sortFunction(a, b);
if (ret !== 0) {
return ret;
}
}
return 0;
};
}
const useStyles = makeStyles((theme) => ({
topBar: {
backgroundColor: theme.palette.primary[500],
},
}));
interface PropsType {
entries: TaskType[];
collections: EteSync.CollectionInfo[];
onItemClick: (entry: TaskType) => void;
onItemSave: (item: PimType, journalUid: string, originalItem?: PimType) => Promise<void>;
}
export default function TaskList(props: PropsType) {
const [showCompleted, setShowCompleted] = React.useState(false);
const [showHidden, setShowHidden] = 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(
() => {
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) && (showHidden || !x.hidden));
}
},
[showCompleted, props.entries, searchTerm, showHidden]
);
let entries;
const tagPrefix = 'tag:';
if (filterBy?.startsWith(tagPrefix)) {
const tag = filterBy.slice(tagPrefix.length);
entries = potentialEntries.filter((x) => x.tags.includes(tag));
} else if (filterBy === 'today') {
entries = potentialEntries.filter((x) => x.dueToday);
} else {
entries = potentialEntries;
}
const sortedEntries = entries.sort(getSortFunction(sortBy));
const itemList = sortedEntries.map((entry) => {
const uid = entry.uid;
return (
<TaskListItem
key={uid}
entry={entry}
onClick={props.onItemClick}
onSave={props.onItemSave}
/>
);
});
return (
<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}
showHidden={showHidden}
setShowHidden={setShowHidden}
/>
</Grid>
<Grid item xs={3} style={{ borderRight: `1px solid ${theme.palette.divider}` }}>
<Sidebar tasks={potentialEntries} />
</Grid>
<Grid item xs>
{props.collections?.[0] && <QuickAdd style={{ flexGrow: 1, marginRight: '0.75em' }} onSubmit={props.onItemSave} defaultCollection={props.collections?.[0]} />}
<Divider style={{ marginTop: '1em' }} />
<List>
{itemList}
</List>
</Grid>
</Grid>
);
}