diff --git a/src/App.tsx b/src/App.tsx index f2b499a..6d63893 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -41,10 +41,16 @@ export const routeResolver = new RouteResolver({ contacts: { _id: { _base: ":itemUid", - edit: "edit", + edit: { + contact: "contact", + group: "group", + }, log: "log", }, - new: "new", + new: { + contact: "contact", + group: "group", + }, }, events: { _id: { diff --git a/src/Contacts/ContactEdit.tsx b/src/Contacts/ContactEdit.tsx index ef023b8..c307ed1 100644 --- a/src/Contacts/ContactEdit.tsx +++ b/src/Contacts/ContactEdit.tsx @@ -27,6 +27,7 @@ import * as ICAL from "ical.js"; import { ContactType } from "../pim-types"; import { History } from "history"; +import Autocomplete from "@material-ui/lab/Autocomplete"; const telTypes = [ { type: "Home" }, @@ -124,6 +125,7 @@ interface PropsType { onDelete: (contact: ContactType, collectionUid: string) => void; onCancel: () => void; history: History; + allGroups: ContactType[]; } class ContactEdit extends React.PureComponent { @@ -145,6 +147,9 @@ class ContactEdit extends React.PureComponent { collectionUid: string; showDeleteDialog: boolean; + collectionGroups: {}; + newGroups: string[]; + originalGroups: string[]; }; constructor(props: PropsType) { @@ -167,6 +172,9 @@ class ContactEdit extends React.PureComponent { collectionUid: "", showDeleteDialog: false, + collectionGroups: {}, + newGroups: [], + originalGroups: [], }; if (this.props.item !== undefined) { @@ -231,9 +239,22 @@ class ContactEdit extends React.PureComponent { } else if (props.collections[0]) { 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.addMetadata = this.addMetadata.bind(this); + this.getCollectionGroups = this.getCollectionGroups.bind(this); this.handleChange = this.handleChange.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.addValueType = this.addValueType.bind(this); this.removeValueType = this.removeValueType.bind(this); @@ -274,11 +295,34 @@ class ContactEdit extends React.PureComponent { }); } - public handleChange(name: string, value: string) { + public handleChange(name: string, value: string | string[]) { this.setState({ [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) { @@ -287,6 +331,17 @@ class ContactEdit extends React.PureComponent { 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) { e.preventDefault(); @@ -297,10 +352,33 @@ class ContactEdit extends React.PureComponent { ; const comp = contact.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()); + this.addMetadata(contact, this.state.uid, false); + + // Add new groups + 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 firstName = this.state.firstName.trim(); @@ -359,7 +437,6 @@ class ContactEdit extends React.PureComponent { setProperty("title", this.state.title); setProperty("note", this.state.note); - this.props.onSave(contact, this.state.collectionUid, this.props.item) .then(() => { this.props.history.goBack(); @@ -400,7 +477,7 @@ class ContactEdit extends React.PureComponent { + {this.props.collections.map((x) => ( + {x.metadata.name} + ))} + + + + + +
+ + + {this.props.item && + + } + + +
+ + + this.props.onDelete(this.props.item!, this.props.initialCollection!)} + onCancel={() => this.setState({ showDeleteDialog: false })} + > + Are you sure you would like to delete this group? + + + ); + } +} + +export default GroupEdit; diff --git a/src/Contacts/Main.tsx b/src/Contacts/Main.tsx index 8e528e5..478bf02 100644 --- a/src/Contacts/Main.tsx +++ b/src/Contacts/Main.tsx @@ -18,6 +18,7 @@ import SearchableAddressBook from "./SearchableAddressBook"; import Contact from "./Contact"; import LoadingIndicator from "../widgets/LoadingIndicator"; import ContactEdit from "./ContactEdit"; +import GroupEdit from "./GroupEdit"; import PageNotFound, { PageNotFoundRoute } from "../PageNotFound"; 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 = { button: { marginLeft: theme.spacing(1), @@ -99,15 +102,31 @@ export default function ContactsMain() { onItemClick={(item) => history.push( routeResolver.getRoute("pim.contacts._id", { itemUid: getItemNavigationUid(item) }) )} + onNewGroupClick={() => history.push( + routeResolver.getRoute("pim.contacts.new.group") + )} /> history.push( - routeResolver.getRoute("pim.contacts.new") + routeResolver.getRoute("pim.contacts.new.contact") )} /> + + + x.uid === colUid)!; const readOnly = collection.accessLevel === Etebase.CollectionAccessLevel.ReadOnly; + const path = `pim.contacts._id.edit.${item.group ? "group" : "contact"}`; return ( - + {item.group ? + + : + + } - history.push(routeResolver.getRoute("pim.contacts._id.edit", { itemUid: getItemNavigationUid(item) })) + history.push(routeResolver.getRoute(path, { itemUid: getItemNavigationUid(item) })) } > diff --git a/src/Contacts/SearchableAddressBook.tsx b/src/Contacts/SearchableAddressBook.tsx index 8193337..9fb894b 100644 --- a/src/Contacts/SearchableAddressBook.tsx +++ b/src/Contacts/SearchableAddressBook.tsx @@ -22,6 +22,7 @@ const useStyles = makeStyles((theme) => ({ interface PropsType { entries: ContactType[]; onItemClick: (contact: ContactType) => void; + onNewGroupClick: () => void; } export default function SearchableAddressBook(props: PropsType) { @@ -66,6 +67,8 @@ export default function SearchableAddressBook(props: PropsType) { groups={groups} filterByGroup={filterByGroup} setFilterByGroup={setFilterByGroup} + newGroup={props.onNewGroupClick} + editGroup={props.onItemClick} /> diff --git a/src/Contacts/Sidebar.tsx b/src/Contacts/Sidebar.tsx index 8737c78..a7a8eeb 100644 --- a/src/Contacts/Sidebar.tsx +++ b/src/Contacts/Sidebar.tsx @@ -2,6 +2,10 @@ import * as React from "react"; import InboxIcon from "@material-ui/icons/Inbox"; 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 { ContactType } from "../pim-types"; @@ -12,19 +16,27 @@ interface ListItemPropsType { primaryText: string; filterByGroup: string | undefined; setFilterByGroup: (group: string | undefined) => void; + editGroup: () => void; } function SidebarListItem(props: ListItemPropsType) { - const { name, icon, primaryText, filterByGroup } = props; + const { name, icon, primaryText, filterByGroup, editGroup } = props; const handleClick = () => props.setFilterByGroup(name); + const selected = name === filterByGroup; + return ( + + + } /> ); } @@ -33,10 +45,12 @@ interface PropsType { groups: ContactType[]; filterByGroup: string | undefined; setFilterByGroup: (group: string | undefined) => void; + newGroup: () => void; + editGroup: (group: ContactType) => void; } 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) => ( } filterByGroup={filterByGroup} setFilterByGroup={setFilterByGroup} + editGroup={() => editGroup(group)} /> )); @@ -57,9 +72,21 @@ export default React.memo(function Sidebar(props: PropsType) { icon={} filterByGroup={filterByGroup} setFilterByGroup={setFilterByGroup} + editGroup={newGroup} /> - Groups +
+ + Groups + + + + +
+ {groupList} ); diff --git a/src/widgets/List.tsx b/src/widgets/List.tsx index f496ed6..fa4b590 100644 --- a/src/widgets/List.tsx +++ b/src/widgets/List.tsx @@ -6,6 +6,7 @@ import * as React from "react"; import { createStyles, makeStyles } from "@material-ui/core/styles"; import MuiList from "@material-ui/core/List"; import MuiListItem from "@material-ui/core/ListItem"; +import MuiListItemSecondaryAction from "@material-ui/core/ListItemSecondaryAction"; import MuiListSubheader from "@material-ui/core/ListSubheader"; import ListItemIcon from "@material-ui/core/ListItemIcon"; import ListItemText from "@material-ui/core/ListItemText"; @@ -47,6 +48,7 @@ interface ListItemPropsType { nestedItems?: React.ReactNode[]; selected?: boolean; secondaryTextColor?: "initial" | "inherit" | "primary" | "secondary" | "textPrimary" | "textSecondary" | "error"; + secondaryAction?: React.ReactNode; } export const ListItem = React.memo(function ListItem(_props: ListItemPropsType) { @@ -64,6 +66,7 @@ export const ListItem = React.memo(function ListItem(_props: ListItemPropsType) nestedItems, selected, secondaryTextColor, + secondaryAction, } = _props; const extraProps = (onClick || href) ? { @@ -94,6 +97,11 @@ export const ListItem = React.memo(function ListItem(_props: ListItemPropsType) {rightIcon} )} + {secondaryAction && ( + + {secondaryAction} + + )} {nestedItems && (