1 import type { ChangeEvent, FormEvent } from 'react';
2 import { useMemo, useState } from 'react';
4 import { c, msgid } from 'ttag';
6 import { Button, Input } from '@proton/atoms';
8 type AddressesAutocompleteItem,
9 getContactsAutocompleteItems,
10 } from '@proton/components/components/addressesAutocomplete/helper';
11 import Alert from '@proton/components/components/alert/Alert';
12 import Autocomplete from '@proton/components/components/autocomplete/Autocomplete';
13 import Field from '@proton/components/components/container/Field';
14 import Row from '@proton/components/components/container/Row';
15 import ColorPicker from '@proton/components/components/input/ColorPicker';
16 import Label from '@proton/components/components/label/Label';
17 import type { ModalProps } from '@proton/components/components/modalTwo/Modal';
18 import ModalTwo from '@proton/components/components/modalTwo/Modal';
19 import ModalTwoContent from '@proton/components/components/modalTwo/ModalContent';
20 import ModalTwoFooter from '@proton/components/components/modalTwo/ModalFooter';
21 import ModalTwoHeader from '@proton/components/components/modalTwo/ModalHeader';
22 import { useContactGroups } from '@proton/mail';
23 import { useContactEmails } from '@proton/mail/contactEmails/hooks';
24 import { useMailSettings } from '@proton/mail/mailSettings/hooks';
25 import { getRandomAccentColor } from '@proton/shared/lib/colors';
26 import { hasReachedContactGroupMembersLimit } from '@proton/shared/lib/contacts/helpers/contactGroup';
27 import { validateEmailAddress } from '@proton/shared/lib/helpers/email';
28 import type { ContactEmail } from '@proton/shared/lib/interfaces/contacts/Contact';
29 import { DEFAULT_MAILSETTINGS } from '@proton/shared/lib/mail/mailSettings';
30 import diff from '@proton/utils/diff';
31 import isTruthy from '@proton/utils/isTruthy';
33 import useUpdateGroup from '../hooks/useUpdateGroup';
34 import ContactGroupTable from './ContactGroupTable';
36 export interface ContactGroupEditProps {
37 contactGroupID?: string;
38 selectedContactEmails?: ContactEmail[];
39 onDelayedSave?: (groupID: string) => void;
42 type Props = ContactGroupEditProps & ModalProps;
44 const ContactGroupEditModal = ({ contactGroupID, selectedContactEmails = [], onDelayedSave, ...rest }: Props) => {
45 const [mailSettings] = useMailSettings();
46 const [loading, setLoading] = useState(false);
47 const [error, setError] = useState(false);
48 const [contactGroups = []] = useContactGroups();
49 const contactEmails = useContactEmails()[0] || [];
50 const [value, setValue] = useState('');
51 const updateGroup = useUpdateGroup();
52 const isValidEmail = useMemo(() => validateEmailAddress(value), [value]);
54 const contactGroup = contactGroupID && contactGroups.find(({ ID }) => ID === contactGroupID);
55 const existingContactEmails = contactGroupID
56 ? contactEmails.filter(({ LabelIDs = [] }: { LabelIDs: string[] }) => LabelIDs.includes(contactGroupID))
58 const title = contactGroupID ? c('Title').t`Edit contact group` : c('Title').t`Create new group`;
60 const [model, setModel] = useState<{ name: string; color: string; contactEmails: ContactEmail[] }>({
61 name: contactGroupID && contactGroup ? contactGroup.Name : '',
62 color: contactGroupID && contactGroup ? contactGroup.Color : getRandomAccentColor(),
63 contactEmails: contactGroupID ? existingContactEmails : selectedContactEmails,
65 const contactEmailIDs = model.contactEmails.map(({ ID }: ContactEmail) => ID);
67 const canAddMoreContacts = hasReachedContactGroupMembersLimit(model.contactEmails.length, mailSettings);
69 const contactsAutocompleteItems = useMemo(() => {
70 return [...getContactsAutocompleteItems(contactEmails, ({ ID }) => !contactEmailIDs.includes(ID))];
71 }, [contactEmails, contactEmailIDs]);
73 const handleChangeName = ({ target }: ChangeEvent<HTMLInputElement>) => setModel({ ...model, name: target.value });
74 const handleChangeColor = (color: string) => setModel({ ...model, color });
76 const handleAdd = () => {
80 setModel((model) => ({
82 contactEmails: [...model.contactEmails, { Name: value, Email: value } as ContactEmail],
87 const handleSelect = (newContactEmail: AddressesAutocompleteItem | string) => {
88 if (!canAddMoreContacts) {
93 if (typeof newContactEmail === 'string' || newContactEmail.type === 'major') {
96 const newContact = contactEmails.find((contact: ContactEmail) => contact.ID === newContactEmail.value.ID);
98 setModel((model) => ({
100 contactEmails: [...model.contactEmails, newContact],
108 const handleAddContact = () => {
109 if (!canAddMoreContacts) {
118 const handleDeleteEmail = (contactEmail: string) => {
119 const index = model.contactEmails.findIndex(({ Email }: ContactEmail) => Email === contactEmail);
122 const copy = [...model.contactEmails];
123 copy.splice(index, 1);
124 setModel({ ...model, contactEmails: copy });
128 const handleSubmit = async (event: FormEvent) => {
129 event.preventDefault();
130 event.stopPropagation();
134 const toAdd = model.contactEmails.filter(({ ID }) => isTruthy(ID));
135 const toCreate = !onDelayedSave
136 ? model.contactEmails.filter(({ ID }) => !isTruthy(ID))
137 : // If delayed save, the contact we are editing does not really exists yet, so we need to remove it from the to create
138 model.contactEmails.filter(
139 (contactEmail) => !isTruthy(contactEmail.ID) && !selectedContactEmails?.includes(contactEmail)
141 const toRemove = contactGroupID ? diff(existingContactEmails, toAdd) : [];
144 groupID: contactGroupID,
154 } catch (error: any) {
160 const contactEmailsLength = model.contactEmails.length;
162 const maxContacts = mailSettings?.RecipientLimit || DEFAULT_MAILSETTINGS.RecipientLimit;
163 const cannotAddMoreContactText = c('Action').ngettext(
164 msgid`At most ${maxContacts} contact is allowed per contact group`,
165 `At most ${maxContacts} contacts are allowed per contact group`,
170 <ModalTwo size="large" className="contacts-modal" as="form" onSubmit={handleSubmit} {...rest}>
171 <ModalTwoHeader title={title} />
174 <Label htmlFor="contactGroupName">{c('Label for contact group name').t`Name`}</Label>
175 <Field className="md:flex-1">
177 id="contactGroupName"
178 placeholder={c('Placeholder for contact group name').t`Name`}
180 onChange={handleChangeName}
185 <Label htmlFor="contactGroupColor">{c('Label for contact group color').t`Color`}</Label>
186 <Field className="w-full">
187 <ColorPicker id="contactGroupColor" color={model.color} onChange={handleChangeColor} />
190 {contactsAutocompleteItems.length ? (
191 <div className="flex flex-nowrap mb-4 flex-column md:flex-row">
192 <Label htmlFor="contactGroupEmail">{c('Label').t`Add email address`}</Label>
194 <div className="flex flex-column md:flex-row">
195 <Field className="md:flex-1">
197 id="contactGroupEmail"
198 options={contactsAutocompleteItems}
202 getData={(value) => value.label}
204 placeholder={c('Placeholder').t`Start typing an email address`}
205 onSelect={handleSelect}
210 className="ml-0 md:ml-4 mt-2 md:mt-0"
211 onClick={handleAddContact}
212 disabled={!isValidEmail}
213 data-testid="create-group:add-email"
218 {!canAddMoreContacts && error && (
219 <Alert className="mb-4 mt-2" type="error">
220 {cannotAddMoreContactText}
227 <ContactGroupTable contactEmails={model.contactEmails} onDelete={handleDeleteEmail} />
229 {contactEmailsLength ? (
230 <div className="text-center color-weak">
232 msgid`${contactEmailsLength} Member`,
233 `${contactEmailsLength} Members`,
240 <Button onClick={rest.onClose}>{c('Action').t`Close`}</Button>
241 <Button color="norm" type="submit" disabled={loading} data-testid="create-group:save">
242 {c('Action').t`Save`}
249 export default ContactGroupEditModal;