Contacts: redesign the address book view and add filtering by group.

This is the first step towards fixing #136.
master
Tom Hacohen 4 years ago
parent 78e91abcb4
commit 55cae0962d

@ -96,7 +96,7 @@ class AddressBook extends React.PureComponent<PropsType> {
: sortedEntries; : sortedEntries;
return ( return (
<List style={{ height: "calc(100vh - 300px)" }}> <List style={{ height: "calc(100vh - 350px)" }}>
<AutoSizer> <AutoSizer>
{({ height, width }) => ( {({ height, width }) => (
<VirtualizedList <VirtualizedList

@ -3,16 +3,22 @@
import * as React from "react"; import * as React from "react";
import TextField from "@material-ui/core/TextField"; import { makeStyles, useTheme } from "@material-ui/core/styles";
import Divider from "@material-ui/core/Divider";
import IconButton from "@material-ui/core/IconButton"; import Grid from "@material-ui/core/Grid";
import IconSearch from "@material-ui/icons/Search";
import IconClear from "@material-ui/icons/Clear";
import { ContactType } from "../pim-types"; import { ContactType } from "../pim-types";
import Sidebar from "./Sidebar";
import Toolbar from "./Toolbar";
import AddressBook from "./AddressBook"; import AddressBook from "./AddressBook";
const useStyles = makeStyles((theme) => ({
topBar: {
backgroundColor: theme.palette.primary[500],
},
}));
interface PropsType { interface PropsType {
entries: ContactType[]; entries: ContactType[];
onItemClick: (contact: ContactType) => void; onItemClick: (contact: ContactType) => void;
@ -20,25 +26,54 @@ interface PropsType {
export default function SearchableAddressBook(props: PropsType) { export default function SearchableAddressBook(props: PropsType) {
const [searchQuery, setSearchQuery] = React.useState(""); const [searchQuery, setSearchQuery] = React.useState("");
const [filterByGroup, setFilterByGroup] = React.useState<string>();
const theme = useTheme();
const classes = useStyles();
const groups = React.useMemo(
(() => props.entries.filter((x) => x.group)),
[props.entries]
);
const group = React.useMemo(
(() => groups.find((x) => x.uid === filterByGroup)),
[groups, filterByGroup]
);
function filterFunc(ent: ContactType) {
return (
(!group || (group.members.includes(ent.uid))) &&
ent.fn?.match(reg)
);
}
const reg = new RegExp(searchQuery, "i"); const reg = new RegExp(searchQuery, "i");
return ( return (
<React.Fragment> <Grid container spacing={4}>
<TextField <Grid item xs={3} className={classes.topBar}>
name="searchQuery" {/* spacer */}
value={searchQuery} </Grid>
style={{ fontSize: "120%", marginLeft: 20 }}
placeholder="Find Contacts" <Grid item xs={9} className={classes.topBar}>
onChange={(event) => setSearchQuery(event.target.value)} <Toolbar
searchTerm={searchQuery}
setSearchTerm={setSearchQuery}
/> />
{searchQuery && </Grid>
<IconButton onClick={() => setSearchQuery("")}>
<IconClear /> <Grid item xs={3} style={{ borderRight: `1px solid ${theme.palette.divider}` }}>
</IconButton> <Sidebar
} groups={groups}
<IconSearch /> filterByGroup={filterByGroup}
<AddressBook filter={(ent: ContactType) => ent.fn?.match(reg)} {...props} /> setFilterByGroup={setFilterByGroup}
</React.Fragment> />
</Grid>
<Grid item xs>
<Divider style={{ marginTop: "1em" }} />
<AddressBook filter={filterFunc} {...props} />
</Grid>
</Grid>
); );
} }

@ -0,0 +1,66 @@
import * as React from "react";
import InboxIcon from "@material-ui/icons/Inbox";
import LabelIcon from "@material-ui/icons/LabelOutlined";
import { List, ListItem, ListSubheader } from "../widgets/List";
import { ContactType } from "../pim-types";
interface ListItemPropsType {
name: string | undefined;
icon?: React.ReactElement;
primaryText: string;
filterByGroup: string | undefined;
setFilterByGroup: (group: string | undefined) => void;
}
function SidebarListItem(props: ListItemPropsType) {
const { name, icon, primaryText, filterByGroup } = props;
const handleClick = () => props.setFilterByGroup(name);
return (
<ListItem
onClick={handleClick}
selected={name === filterByGroup}
leftIcon={icon}
primaryText={primaryText}
/>
);
}
interface PropsType {
groups: ContactType[];
filterByGroup: string | undefined;
setFilterByGroup: (group: string | undefined) => void;
}
export default React.memo(function Sidebar(props: PropsType) {
const { groups, filterByGroup, setFilterByGroup } = props;
const groupList = [...groups].sort((a, b) => a.fn.localeCompare(b.fn)).map((group) => (
<SidebarListItem
key={group.uid}
name={group.uid}
primaryText={group.fn}
icon={<LabelIcon />}
filterByGroup={filterByGroup}
setFilterByGroup={setFilterByGroup}
/>
));
return (
<List dense>
<SidebarListItem
name={undefined}
primaryText="All"
icon={<InboxIcon />}
filterByGroup={filterByGroup}
setFilterByGroup={setFilterByGroup}
/>
<ListSubheader>Groups</ListSubheader>
{groupList}
</List>
);
});

@ -0,0 +1,64 @@
import * as React from "react";
import SearchIcon from "@material-ui/icons/Search";
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";
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 {
searchTerm: string;
setSearchTerm: (term: string) => void;
}
export default function Toolbar(props: PropsType) {
const { searchTerm, setSearchTerm } = props;
const showSearchField = true;
const classes = useStyles();
return (
<div style={{ display: "flex", justifyContent: "flex-end", alignItems: "center" }}>
<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>
);
}
Loading…
Cancel
Save