Remove payments API routing initialization
[ProtonMail-WebClient.git] / packages / components / containers / contacts / group / ContactGroupEditModal.tsx
blobf8d31da85065b2e8958d817e499fb8c20244d4d2
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';
7 import {
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))
57         : [];
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,
64     });
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 = () => {
77         if (!isValidEmail) {
78             return;
79         }
80         setModel((model) => ({
81             ...model,
82             contactEmails: [...model.contactEmails, { Name: value, Email: value } as ContactEmail],
83         }));
84         setValue('');
85     };
87     const handleSelect = (newContactEmail: AddressesAutocompleteItem | string) => {
88         if (!canAddMoreContacts) {
89             setError(true);
90             return;
91         }
93         if (typeof newContactEmail === 'string' || newContactEmail.type === 'major') {
94             handleAdd();
95         } else {
96             const newContact = contactEmails.find((contact: ContactEmail) => contact.ID === newContactEmail.value.ID);
97             if (newContact) {
98                 setModel((model) => ({
99                     ...model,
100                     contactEmails: [...model.contactEmails, newContact],
101                 }));
102             }
103             setValue('');
104         }
105         setError(false);
106     };
108     const handleAddContact = () => {
109         if (!canAddMoreContacts) {
110             setError(true);
111             return;
112         }
114         handleAdd();
115         setError(false);
116     };
118     const handleDeleteEmail = (contactEmail: string) => {
119         const index = model.contactEmails.findIndex(({ Email }: ContactEmail) => Email === contactEmail);
121         if (index > -1) {
122             const copy = [...model.contactEmails];
123             copy.splice(index, 1);
124             setModel({ ...model, contactEmails: copy });
125         }
126     };
128     const handleSubmit = async (event: FormEvent) => {
129         event.preventDefault();
130         event.stopPropagation();
132         try {
133             setLoading(true);
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)
140                   );
141             const toRemove = contactGroupID ? diff(existingContactEmails, toAdd) : [];
143             await updateGroup({
144                 groupID: contactGroupID,
145                 name: model.name,
146                 color: model.color,
147                 toAdd,
148                 toRemove,
149                 toCreate,
150                 onDelayedSave,
151             });
153             rest.onClose?.();
154         } catch (error: any) {
155             setLoading(false);
156             throw error;
157         }
158     };
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`,
166         maxContacts
167     );
169     return (
170         <ModalTwo size="large" className="contacts-modal" as="form" onSubmit={handleSubmit} {...rest}>
171             <ModalTwoHeader title={title} />
172             <ModalTwoContent>
173                 <Row>
174                     <Label htmlFor="contactGroupName">{c('Label for contact group name').t`Name`}</Label>
175                     <Field className="md:flex-1">
176                         <Input
177                             id="contactGroupName"
178                             placeholder={c('Placeholder for contact group name').t`Name`}
179                             value={model.name}
180                             onChange={handleChangeName}
181                         />
182                     </Field>
183                 </Row>
184                 <Row>
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} />
188                     </Field>
189                 </Row>
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>
193                         <div>
194                             <div className="flex flex-column md:flex-row">
195                                 <Field className="md:flex-1">
196                                     <Autocomplete
197                                         id="contactGroupEmail"
198                                         options={contactsAutocompleteItems}
199                                         limit={6}
200                                         value={value}
201                                         onChange={setValue}
202                                         getData={(value) => value.label}
203                                         type="search"
204                                         placeholder={c('Placeholder').t`Start typing an email address`}
205                                         onSelect={handleSelect}
206                                         autoComplete="off"
207                                     />
208                                 </Field>
209                                 <Button
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"
214                                 >
215                                     {c('Action').t`Add`}
216                                 </Button>
217                             </div>
218                             {!canAddMoreContacts && error && (
219                                 <Alert className="mb-4 mt-2" type="error">
220                                     {cannotAddMoreContactText}
221                                 </Alert>
222                             )}
223                         </div>
224                     </div>
225                 ) : null}
227                 <ContactGroupTable contactEmails={model.contactEmails} onDelete={handleDeleteEmail} />
229                 {contactEmailsLength ? (
230                     <div className="text-center color-weak">
231                         {c('Info').ngettext(
232                             msgid`${contactEmailsLength} Member`,
233                             `${contactEmailsLength} Members`,
234                             contactEmailsLength
235                         )}
236                     </div>
237                 ) : null}
238             </ModalTwoContent>
239             <ModalTwoFooter>
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`}
243                 </Button>
244             </ModalTwoFooter>
245         </ModalTwo>
246     );
249 export default ContactGroupEditModal;