Merge Contacts: Add basic group editing functionality

Merge of #176
master
Tom Hacohen 4 years ago committed by GitHub
commit e9cbb9bf37
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -41,10 +41,16 @@ export const routeResolver = new RouteResolver({
contacts: { contacts: {
_id: { _id: {
_base: ":itemUid", _base: ":itemUid",
edit: "edit", edit: {
contact: "contact",
group: "group",
},
log: "log", log: "log",
}, },
new: "new", new: {
contact: "contact",
group: "group",
},
}, },
events: { events: {
_id: { _id: {

@ -27,6 +27,7 @@ import * as ICAL from "ical.js";
import { ContactType } from "../pim-types"; import { ContactType } from "../pim-types";
import { History } from "history"; import { History } from "history";
import Autocomplete from "@material-ui/lab/Autocomplete";
const telTypes = [ const telTypes = [
{ type: "Home" }, { type: "Home" },
@ -124,6 +125,7 @@ interface PropsType {
onDelete: (contact: ContactType, collectionUid: string) => void; onDelete: (contact: ContactType, collectionUid: string) => void;
onCancel: () => void; onCancel: () => void;
history: History<any>; history: History<any>;
allGroups: ContactType[];
} }
class ContactEdit extends React.PureComponent<PropsType> { class ContactEdit extends React.PureComponent<PropsType> {
@ -145,6 +147,9 @@ class ContactEdit extends React.PureComponent<PropsType> {
collectionUid: string; collectionUid: string;
showDeleteDialog: boolean; showDeleteDialog: boolean;
collectionGroups: {};
newGroups: string[];
originalGroups: string[];
}; };
constructor(props: PropsType) { constructor(props: PropsType) {
@ -167,6 +172,9 @@ class ContactEdit extends React.PureComponent<PropsType> {
collectionUid: "", collectionUid: "",
showDeleteDialog: false, showDeleteDialog: false,
collectionGroups: {},
newGroups: [],
originalGroups: [],
}; };
if (this.props.item !== undefined) { if (this.props.item !== undefined) {
@ -231,9 +239,22 @@ class ContactEdit extends React.PureComponent<PropsType> {
} else if (props.collections[0]) { } else if (props.collections[0]) {
this.state.collectionUid = props.collections[0].collection.uid; this.state.collectionUid = props.collections[0].collection.uid;
} }
this.state.collectionGroups = this.getCollectionGroups(this.state.collectionUid);
Object.values(this.state.collectionGroups).forEach((group: ContactType) => {
if (group.members.includes(this.state.uid)) {
this.state.newGroups.push(group.fn);
this.state.originalGroups.push(group.fn);
}
});
this.onSubmit = this.onSubmit.bind(this); this.onSubmit = this.onSubmit.bind(this);
this.addMetadata = this.addMetadata.bind(this);
this.getCollectionGroups = this.getCollectionGroups.bind(this);
this.handleChange = this.handleChange.bind(this); this.handleChange = this.handleChange.bind(this);
this.handleInputChange = this.handleInputChange.bind(this); this.handleInputChange = this.handleInputChange.bind(this);
this.handleCollectionChange = this.handleCollectionChange.bind(this);
this.reloadGroupSuggestions = this.reloadGroupSuggestions.bind(this);
this.handleValueTypeChange = this.handleValueTypeChange.bind(this); this.handleValueTypeChange = this.handleValueTypeChange.bind(this);
this.addValueType = this.addValueType.bind(this); this.addValueType = this.addValueType.bind(this);
this.removeValueType = this.removeValueType.bind(this); this.removeValueType = this.removeValueType.bind(this);
@ -274,11 +295,34 @@ class ContactEdit extends React.PureComponent<PropsType> {
}); });
} }
public handleChange(name: string, value: string) { public handleChange(name: string, value: string | string[]) {
this.setState({ this.setState({
[name]: value, [name]: value,
}); });
}
public getCollectionGroups(collectionUid: string) {
const groups = {};
this.props.allGroups.forEach((group) => {
if (collectionUid === group.collectionUid) {
groups[group.fn] = group;
}
});
return groups;
}
public reloadGroupSuggestions(collectionUid: string) {
this.setState({
collectionGroups: this.getCollectionGroups(collectionUid),
newGroups: [],
});
}
public handleCollectionChange(contact: any) {
const name = contact.target.name;
const value = contact.target.value;
this.reloadGroupSuggestions(value);
this.handleChange(name, value);
} }
public handleInputChange(contact: any) { public handleInputChange(contact: any) {
@ -287,6 +331,17 @@ class ContactEdit extends React.PureComponent<PropsType> {
this.handleChange(name, value); this.handleChange(name, value);
} }
public addMetadata(item: ContactType, uid: string, isGroup: boolean) {
const comp = item.comp;
comp.updatePropertyWithValue("prodid", "-//iCal.js EteSync Web");
comp.updatePropertyWithValue("version", "4.0");
comp.updatePropertyWithValue("uid", uid);
comp.updatePropertyWithValue("rev", ICAL.Time.now());
if (isGroup) {
comp.updatePropertyWithValue("kind", "group");
}
}
public onSubmit(e: React.FormEvent<any>) { public onSubmit(e: React.FormEvent<any>) {
e.preventDefault(); e.preventDefault();
@ -297,10 +352,33 @@ class ContactEdit extends React.PureComponent<PropsType> {
; ;
const comp = contact.comp; const comp = contact.comp;
comp.updatePropertyWithValue("prodid", "-//iCal.js EteSync Web"); this.addMetadata(contact, this.state.uid, false);
comp.updatePropertyWithValue("version", "4.0");
comp.updatePropertyWithValue("uid", this.state.uid); // Add new groups
comp.updatePropertyWithValue("rev", ICAL.Time.now()); this.state.newGroups.forEach((group) => {
if (!this.state.collectionGroups[group]) {
const newGroup = new ContactType(new ICAL.Component(["vcard", [], []]));
this.addMetadata(newGroup, uuid.v4(), true);
newGroup.comp.updatePropertyWithValue("fn", group.trim());
newGroup.comp.updatePropertyWithValue("member", `urn:uuid:${this.state.uid}`);
this.props.onSave(newGroup, this.state.collectionUid, undefined);
} else if (!this.state.originalGroups[group]) {
const oldGroup = this.state.collectionGroups[group];
const updatedGroup = oldGroup.clone();
updatedGroup.comp.addPropertyWithValue("member", `urn:uuid:${this.state.uid}`);
this.props.onSave(updatedGroup, this.state.collectionUid, oldGroup);
}
});
// Remove deleted groups
this.state.originalGroups.filter((x) => !this.state.newGroups.includes(x)).forEach((removed) => {
const deletedGroup = this.state.collectionGroups[removed];
const updatedGroup = deletedGroup.clone();
const members = updatedGroup.members.filter((uid: string) => uid !== this.state.uid);
updatedGroup.comp.removeAllProperties("member");
members.forEach((m: string) => updatedGroup.comp.addPropertyWithValue("member", `urn:uuid:${m}`));
this.props.onSave(updatedGroup, this.state.collectionUid, deletedGroup);
});
const lastName = this.state.lastName.trim(); const lastName = this.state.lastName.trim();
const firstName = this.state.firstName.trim(); const firstName = this.state.firstName.trim();
@ -359,7 +437,6 @@ class ContactEdit extends React.PureComponent<PropsType> {
setProperty("title", this.state.title); setProperty("title", this.state.title);
setProperty("note", this.state.note); setProperty("note", this.state.note);
this.props.onSave(contact, this.state.collectionUid, this.props.item) this.props.onSave(contact, this.state.collectionUid, this.props.item)
.then(() => { .then(() => {
this.props.history.goBack(); this.props.history.goBack();
@ -400,7 +477,7 @@ class ContactEdit extends React.PureComponent<PropsType> {
<Select <Select
name="collectionUid" name="collectionUid"
value={this.state.collectionUid} value={this.state.collectionUid}
onChange={this.handleInputChange} onChange={this.handleCollectionChange}
> >
{this.props.collections.map((x) => ( {this.props.collections.map((x) => (
<MenuItem key={x.collection.uid} value={x.collection.uid}>{x.metadata.name}</MenuItem> <MenuItem key={x.collection.uid} value={x.collection.uid}>{x.metadata.name}</MenuItem>
@ -565,6 +642,24 @@ class ContactEdit extends React.PureComponent<PropsType> {
value={this.state.note} value={this.state.note}
onChange={this.handleInputChange} onChange={this.handleInputChange}
/> />
<Autocomplete
style={styles.fullWidth}
freeSolo
multiple
clearOnBlur
selectOnFocus
options={Object.keys(this.state.collectionGroups)}
value={this.state.newGroups}
onChange={(_e, value) => this.handleChange("newGroups", value)}
renderInput={(params) => (
<TextField
{...params}
variant="standard"
label="Groups"
fullWidth
/>
)}
/>
<div style={styles.submit}> <div style={styles.submit}>
<Button <Button

@ -0,0 +1,288 @@
// SPDX-FileCopyrightText: © 2017 EteSync Authors
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from "react";
import Button from "@material-ui/core/Button";
import TextField from "@material-ui/core/TextField";
import Select from "@material-ui/core/Select";
import MenuItem from "@material-ui/core/MenuItem";
import FormControl from "@material-ui/core/FormControl";
import InputLabel from "@material-ui/core/InputLabel";
import * as colors from "@material-ui/core/colors";
import IconDelete from "@material-ui/icons/Delete";
import IconCancel from "@material-ui/icons/Clear";
import IconSave from "@material-ui/icons/Save";
import ConfirmationDialog from "../widgets/ConfirmationDialog";
import { CachedCollection } from "../Pim/helpers";
import * as uuid from "uuid";
import * as ICAL from "ical.js";
import { ContactType } from "../pim-types";
import { History } from "history";
class ValueType {
public type: string;
public value: string;
constructor(type?: string, value?: string) {
this.type = type ? type : "home";
this.value = value ? value : "";
}
}
interface PropsType {
collections: CachedCollection[];
initialCollection?: string;
item?: ContactType;
onSave: (contact: ContactType, collectionUid: string, originalContact?: ContactType) => Promise<void>;
onDelete: (contact: ContactType, collectionUid: string) => void;
onCancel: () => void;
history: History<any>;
allGroups: ContactType[];
}
class GroupEdit extends React.PureComponent<PropsType> {
public state: {
uid: string;
fn: string;
collectionUid: string;
showDeleteDialog: boolean;
collectionGroups: {};
showError: boolean;
};
constructor(props: PropsType) {
super(props);
this.state = {
uid: "",
fn: "",
collectionUid: "",
showDeleteDialog: false,
collectionGroups: {},
showError: false,
};
if (this.props.item !== undefined) {
const group = this.props.item;
this.state.uid = group.uid;
this.state.fn = group.fn;
} else {
this.state.uid = uuid.v4();
}
if (props.initialCollection) {
this.state.collectionUid = props.initialCollection;
} else if (props.collections[0]) {
this.state.collectionUid = props.collections[0].collection.uid;
}
this.state.collectionGroups = this.getCollectionGroups(this.state.collectionUid);
this.onSubmit = this.onSubmit.bind(this);
this.getCollectionGroups = this.getCollectionGroups.bind(this);
this.handleChange = this.handleChange.bind(this);
this.handleCollectionChange = this.handleCollectionChange.bind(this);
this.handleInputChange = this.handleInputChange.bind(this);
this.handleValueTypeChange = this.handleValueTypeChange.bind(this);
this.addValueType = this.addValueType.bind(this);
this.removeValueType = this.removeValueType.bind(this);
this.onDeleteRequest = this.onDeleteRequest.bind(this);
}
public addValueType(name: string, _type?: string) {
const type = _type ? _type : "home";
this.setState((prevState) => {
const newArray = prevState[name].slice(0);
newArray.push(new ValueType(type));
return {
...prevState,
[name]: newArray,
};
});
}
public removeValueType(name: string, idx: number) {
this.setState((prevState) => {
const newArray = prevState[name].slice(0);
newArray.splice(idx, 1);
return {
...prevState,
[name]: newArray,
};
});
}
public handleValueTypeChange(name: string, idx: number, value: ValueType) {
this.setState((prevState) => {
const newArray = prevState[name].slice(0);
newArray[idx] = value;
return {
...prevState,
[name]: newArray,
};
});
}
public handleChange(name: string, value: string) {
this.setState({
[name]: value,
});
}
public getCollectionGroups(collectionUid: string) {
const groups = {};
this.props.allGroups.forEach((group) => {
if (collectionUid === group.collectionUid) {
groups[group.fn] = null;
}
});
return groups;
}
public handleCollectionChange(contact: any) {
const name = contact.target.name;
const collectionUid: string = contact.target.value;
this.handleChange(name, collectionUid);
this.setState({ "collectionGroups": this.getCollectionGroups(collectionUid) });
}
public handleInputChange(contact: any) {
const name = contact.target.name;
const value = contact.target.value;
this.handleChange(name, value);
}
public onSubmit(e: React.FormEvent<any>) {
e.preventDefault();
const nameUsed = this.state.fn in this.state.collectionGroups;
if ((this.props.item && this.state.fn !== this.props.item.fn && nameUsed) || (!this.props.item && nameUsed)) {
this.setState({ showError: true });
return;
}
const group = (this.props.item) ?
this.props.item.clone()
:
new ContactType(new ICAL.Component(["vcard", [], []]))
;
const comp = group.comp;
comp.updatePropertyWithValue("prodid", "-//iCal.js EteSync Web");
comp.updatePropertyWithValue("version", "4.0");
comp.updatePropertyWithValue("uid", this.state.uid);
comp.updatePropertyWithValue("rev", ICAL.Time.now());
comp.updatePropertyWithValue("kind", "group");
comp.updatePropertyWithValue("fn", this.state.fn.trim());
this.props.onSave(group, this.state.collectionUid, this.props.item)
.then(() => {
this.props.history.goBack();
});
}
public onDeleteRequest() {
this.setState({
showDeleteDialog: true,
});
}
public render() {
const styles = {
form: {
},
fullWidth: {
width: "100%",
boxSizing: "border-box" as any,
},
submit: {
marginTop: 40,
marginBottom: 20,
textAlign: "right" as any,
},
};
return (
<React.Fragment>
<h2>
{this.props.item ? "Edit Group" : "New Group"}
</h2>
<form style={styles.form} onSubmit={this.onSubmit}>
<FormControl disabled={this.props.item !== undefined} style={styles.fullWidth}>
<InputLabel>
Saving to
</InputLabel>
<Select
name="collectionUid"
value={this.state.collectionUid}
onChange={this.handleCollectionChange}
>
{this.props.collections.map((x) => (
<MenuItem key={x.collection.uid} value={x.collection.uid}>{x.metadata.name}</MenuItem>
))}
</Select>
</FormControl>
<TextField
name="fn"
placeholder="Name"
error={this.state.showError}
helperText="Group names must be unique"
style={{ marginTop: "2rem", ...styles.fullWidth }}
value={this.state.fn}
onChange={this.handleInputChange}
/>
<div style={styles.submit}>
<Button
variant="contained"
onClick={this.props.onCancel}
>
<IconCancel style={{ marginRight: 8 }} />
Cancel
</Button>
{this.props.item &&
<Button
variant="contained"
style={{ marginLeft: 15, backgroundColor: colors.red[500], color: "white" }}
onClick={this.onDeleteRequest}
>
<IconDelete style={{ marginRight: 8 }} />
Delete
</Button>
}
<Button
type="submit"
variant="contained"
color="secondary"
style={{ marginLeft: 15 }}
disabled={this.state.fn.length === 0}
>
<IconSave style={{ marginRight: 8 }} />
Save
</Button>
</div>
</form>
<ConfirmationDialog
title="Delete Confirmation"
labelOk="Delete"
open={this.state.showDeleteDialog}
onOk={() => this.props.onDelete(this.props.item!, this.props.initialCollection!)}
onCancel={() => this.setState({ showDeleteDialog: false })}
>
Are you sure you would like to delete this group?
</ConfirmationDialog>
</React.Fragment>
);
}
}
export default GroupEdit;

@ -18,6 +18,7 @@ import SearchableAddressBook from "./SearchableAddressBook";
import Contact from "./Contact"; import Contact from "./Contact";
import LoadingIndicator from "../widgets/LoadingIndicator"; import LoadingIndicator from "../widgets/LoadingIndicator";
import ContactEdit from "./ContactEdit"; import ContactEdit from "./ContactEdit";
import GroupEdit from "./GroupEdit";
import PageNotFound, { PageNotFoundRoute } from "../PageNotFound"; import PageNotFound, { PageNotFoundRoute } from "../PageNotFound";
import { CachedCollection, getItemNavigationUid, getDecryptCollectionsFunction, getDecryptItemsFunction, PimFab, itemSave, itemDelete } from "../Pim/helpers"; import { CachedCollection, getItemNavigationUid, getDecryptCollectionsFunction, getDecryptItemsFunction, PimFab, itemSave, itemDelete } from "../Pim/helpers";
@ -79,6 +80,8 @@ export default function ContactsMain() {
} }
} }
const allGroups = flatEntries.filter((x) => x.group);
const styles = { const styles = {
button: { button: {
marginLeft: theme.spacing(1), marginLeft: theme.spacing(1),
@ -99,15 +102,31 @@ export default function ContactsMain() {
onItemClick={(item) => history.push( onItemClick={(item) => history.push(
routeResolver.getRoute("pim.contacts._id", { itemUid: getItemNavigationUid(item) }) routeResolver.getRoute("pim.contacts._id", { itemUid: getItemNavigationUid(item) })
)} )}
onNewGroupClick={() => history.push(
routeResolver.getRoute("pim.contacts.new.group")
)}
/> />
<PimFab <PimFab
onClick={() => history.push( onClick={() => history.push(
routeResolver.getRoute("pim.contacts.new") routeResolver.getRoute("pim.contacts.new.contact")
)} )}
/> />
</Route> </Route>
<Route <Route
path={routeResolver.getRoute("pim.contacts.new")} path={routeResolver.getRoute("pim.contacts.new.group")}
exact
>
<GroupEdit
collections={cachedCollections}
onSave={onItemSave}
onDelete={onItemDelete}
onCancel={onCancel}
history={history}
allGroups={allGroups}
/>
</Route>
<Route
path={routeResolver.getRoute("pim.contacts.new.contact")}
exact exact
> >
<ContactEdit <ContactEdit
@ -116,6 +135,7 @@ export default function ContactsMain() {
onDelete={onItemDelete} onDelete={onItemDelete}
onCancel={onCancel} onCancel={onCancel}
history={history} history={history}
allGroups={allGroups}
/> />
</Route> </Route>
<Route <Route
@ -144,23 +164,39 @@ export default function ContactsMain() {
const collection = collections!.find((x) => x.uid === colUid)!; const collection = collections!.find((x) => x.uid === colUid)!;
const readOnly = collection.accessLevel === Etebase.CollectionAccessLevel.ReadOnly; const readOnly = collection.accessLevel === Etebase.CollectionAccessLevel.ReadOnly;
const path = `pim.contacts._id.edit.${item.group ? "group" : "contact"}`;
return ( return (
<Switch> <Switch>
<Route <Route
path={routeResolver.getRoute("pim.contacts._id.edit")} path={routeResolver.getRoute(path)}
exact exact
> >
<ContactEdit {item.group ?
key={itemUid} <GroupEdit
initialCollection={item.collectionUid} key={itemUid}
item={item} initialCollection={item.collectionUid}
collections={cachedCollections} item={item}
onSave={onItemSave} collections={cachedCollections}
onDelete={onItemDelete} onSave={onItemSave}
onCancel={onCancel} onDelete={onItemDelete}
history={history} onCancel={onCancel}
/> history={history}
allGroups={allGroups}
/>
:
<ContactEdit
key={itemUid}
initialCollection={item.collectionUid}
item={item}
collections={cachedCollections}
onSave={onItemSave}
onDelete={onItemDelete}
onCancel={onCancel}
history={history}
allGroups={allGroups}
/>
}
</Route> </Route>
<Route <Route
path={routeResolver.getRoute("pim.contacts._id")} path={routeResolver.getRoute("pim.contacts._id")}
@ -184,7 +220,7 @@ export default function ContactsMain() {
disabled={readOnly} disabled={readOnly}
style={{ ...styles.button, marginLeft: 15 }} style={{ ...styles.button, marginLeft: 15 }}
onClick={() => onClick={() =>
history.push(routeResolver.getRoute("pim.contacts._id.edit", { itemUid: getItemNavigationUid(item) })) history.push(routeResolver.getRoute(path, { itemUid: getItemNavigationUid(item) }))
} }
> >
<IconEdit style={styles.leftIcon} /> <IconEdit style={styles.leftIcon} />

@ -22,6 +22,7 @@ const useStyles = makeStyles((theme) => ({
interface PropsType { interface PropsType {
entries: ContactType[]; entries: ContactType[];
onItemClick: (contact: ContactType) => void; onItemClick: (contact: ContactType) => void;
onNewGroupClick: () => void;
} }
export default function SearchableAddressBook(props: PropsType) { export default function SearchableAddressBook(props: PropsType) {
@ -66,6 +67,8 @@ export default function SearchableAddressBook(props: PropsType) {
groups={groups} groups={groups}
filterByGroup={filterByGroup} filterByGroup={filterByGroup}
setFilterByGroup={setFilterByGroup} setFilterByGroup={setFilterByGroup}
newGroup={props.onNewGroupClick}
editGroup={props.onItemClick}
/> />
</Grid> </Grid>

@ -2,6 +2,10 @@ import * as React from "react";
import InboxIcon from "@material-ui/icons/Inbox"; import InboxIcon from "@material-ui/icons/Inbox";
import LabelIcon from "@material-ui/icons/LabelOutlined"; import LabelIcon from "@material-ui/icons/LabelOutlined";
import AddIcon from "@material-ui/icons/Add";
import EditIcon from "@material-ui/icons/EditOutlined";
import IconButton from "@material-ui/core/IconButton";
import { List, ListItem, ListSubheader } from "../widgets/List"; import { List, ListItem, ListSubheader } from "../widgets/List";
import { ContactType } from "../pim-types"; import { ContactType } from "../pim-types";
@ -12,19 +16,27 @@ interface ListItemPropsType {
primaryText: string; primaryText: string;
filterByGroup: string | undefined; filterByGroup: string | undefined;
setFilterByGroup: (group: string | undefined) => void; setFilterByGroup: (group: string | undefined) => void;
editGroup: () => void;
} }
function SidebarListItem(props: ListItemPropsType) { function SidebarListItem(props: ListItemPropsType) {
const { name, icon, primaryText, filterByGroup } = props; const { name, icon, primaryText, filterByGroup, editGroup } = props;
const handleClick = () => props.setFilterByGroup(name); const handleClick = () => props.setFilterByGroup(name);
const selected = name === filterByGroup;
return ( return (
<ListItem <ListItem
onClick={handleClick} onClick={handleClick}
selected={name === filterByGroup} selected={selected}
leftIcon={icon} leftIcon={icon}
primaryText={primaryText} primaryText={primaryText}
secondaryAction={name && selected &&
<IconButton onClick={editGroup}>
<EditIcon />
</IconButton>
}
/> />
); );
} }
@ -33,10 +45,12 @@ interface PropsType {
groups: ContactType[]; groups: ContactType[];
filterByGroup: string | undefined; filterByGroup: string | undefined;
setFilterByGroup: (group: string | undefined) => void; setFilterByGroup: (group: string | undefined) => void;
newGroup: () => void;
editGroup: (group: ContactType) => void;
} }
export default React.memo(function Sidebar(props: PropsType) { export default React.memo(function Sidebar(props: PropsType) {
const { groups, filterByGroup, setFilterByGroup } = props; const { groups, filterByGroup, setFilterByGroup, newGroup, editGroup } = props;
const groupList = [...groups].sort((a, b) => a.fn.localeCompare(b.fn)).map((group) => ( const groupList = [...groups].sort((a, b) => a.fn.localeCompare(b.fn)).map((group) => (
<SidebarListItem <SidebarListItem
@ -46,6 +60,7 @@ export default React.memo(function Sidebar(props: PropsType) {
icon={<LabelIcon />} icon={<LabelIcon />}
filterByGroup={filterByGroup} filterByGroup={filterByGroup}
setFilterByGroup={setFilterByGroup} setFilterByGroup={setFilterByGroup}
editGroup={() => editGroup(group)}
/> />
)); ));
@ -57,9 +72,21 @@ export default React.memo(function Sidebar(props: PropsType) {
icon={<InboxIcon />} icon={<InboxIcon />}
filterByGroup={filterByGroup} filterByGroup={filterByGroup}
setFilterByGroup={setFilterByGroup} setFilterByGroup={setFilterByGroup}
editGroup={newGroup}
/> />
<ListSubheader>Groups</ListSubheader> <div style={{ display: "flex", justifyContent: "space-between" }}>
<ListSubheader>
Groups
</ListSubheader>
<IconButton
edge="end"
onClick={newGroup}
>
<AddIcon />
</IconButton>
</div>
{groupList} {groupList}
</List> </List>
); );

@ -6,6 +6,7 @@ import * as React from "react";
import { createStyles, makeStyles } from "@material-ui/core/styles"; import { createStyles, makeStyles } from "@material-ui/core/styles";
import MuiList from "@material-ui/core/List"; import MuiList from "@material-ui/core/List";
import MuiListItem from "@material-ui/core/ListItem"; import MuiListItem from "@material-ui/core/ListItem";
import MuiListItemSecondaryAction from "@material-ui/core/ListItemSecondaryAction";
import MuiListSubheader from "@material-ui/core/ListSubheader"; import MuiListSubheader from "@material-ui/core/ListSubheader";
import ListItemIcon from "@material-ui/core/ListItemIcon"; import ListItemIcon from "@material-ui/core/ListItemIcon";
import ListItemText from "@material-ui/core/ListItemText"; import ListItemText from "@material-ui/core/ListItemText";
@ -47,6 +48,7 @@ interface ListItemPropsType {
nestedItems?: React.ReactNode[]; nestedItems?: React.ReactNode[];
selected?: boolean; selected?: boolean;
secondaryTextColor?: "initial" | "inherit" | "primary" | "secondary" | "textPrimary" | "textSecondary" | "error"; secondaryTextColor?: "initial" | "inherit" | "primary" | "secondary" | "textPrimary" | "textSecondary" | "error";
secondaryAction?: React.ReactNode;
} }
export const ListItem = React.memo(function ListItem(_props: ListItemPropsType) { export const ListItem = React.memo(function ListItem(_props: ListItemPropsType) {
@ -64,6 +66,7 @@ export const ListItem = React.memo(function ListItem(_props: ListItemPropsType)
nestedItems, nestedItems,
selected, selected,
secondaryTextColor, secondaryTextColor,
secondaryAction,
} = _props; } = _props;
const extraProps = (onClick || href) ? { const extraProps = (onClick || href) ? {
@ -94,6 +97,11 @@ export const ListItem = React.memo(function ListItem(_props: ListItemPropsType)
{rightIcon} {rightIcon}
</ListItemIcon> </ListItemIcon>
)} )}
{secondaryAction && (
<MuiListItemSecondaryAction>
{secondaryAction}
</MuiListItemSecondaryAction>
)}
</MuiListItem> </MuiListItem>
{nestedItems && ( {nestedItems && (
<List className={classes.nested} disablePadding> <List className={classes.nested} disablePadding>

Loading…
Cancel
Save