1 import type { ChangeEvent, Dispatch, FormEvent, SetStateAction } from 'react';
3 import { c, msgid } from 'ttag';
5 import { Button, Input } from '@proton/atoms';
6 import Alert from '@proton/components/components/alert/Alert';
7 import Option from '@proton/components/components/option/Option';
8 import { FORBIDDEN_LABEL_NAMES } from '@proton/shared/lib/constants';
9 import { omit } from '@proton/shared/lib/helpers/object';
10 import { normalize } from '@proton/shared/lib/helpers/string';
11 import type { ContactGroup, ImportContactsModel } from '@proton/shared/lib/interfaces/contacts';
12 import { IMPORT_GROUPS_ACTION } from '@proton/shared/lib/interfaces/contacts';
13 import isTruthy from '@proton/utils/isTruthy';
15 import { ModalTwoContent, ModalTwoFooter, ModalTwoHeader, SelectTwo } from '../../../../components';
16 import { useApi, useEventManager } from '../../../../hooks';
17 import { submitCategories } from '../encryptAndSubmit';
19 interface SelectGroupActionProps {
20 action: IMPORT_GROUPS_ACTION;
23 onChange: (action: IMPORT_GROUPS_ACTION, index: number) => void;
26 const SelectGroupAction = ({ action, index, canMerge, onChange }: SelectGroupActionProps) => {
27 const actionOptions = [
28 canMerge && { text: c('Option').t`Add to existing group`, value: IMPORT_GROUPS_ACTION.MERGE },
29 { text: c('Option').t`Create new group`, value: IMPORT_GROUPS_ACTION.CREATE },
30 { text: c('Option').t`Ignore group`, value: IMPORT_GROUPS_ACTION.IGNORE },
35 id="contact-group-action-select"
37 onChange={({ value }) => onChange(value as IMPORT_GROUPS_ACTION, index)}
38 title={c('Title').t`Select action to take on contact group`}
40 {actionOptions.map(({ text, value }) => (
41 <Option key={value} value={value} title={text} />
47 interface SelectGroupProps {
48 targetGroup: ContactGroup;
50 contactGroups?: ContactGroup[];
51 action: IMPORT_GROUPS_ACTION;
54 onChangeTargetGroup: (targetGroup: ContactGroup, index: number) => void;
55 onChangeTargetName: (targetName: string, index: number) => void;
56 onError: (error: string, index: number) => void;
59 const SelectGroup = ({
69 }: SelectGroupProps) => {
70 const groupNames = contactGroups.map(({ Name }) => Name);
71 const groupsOptions = contactGroups.map((group) => ({
76 const handleChangeGroupName = ({ target }: ChangeEvent<HTMLInputElement>) => {
77 // Clear previous errors
79 const name = target.value;
81 onError(c('Error').t`You must set a name`, index);
82 } else if (groupNames.includes(name)) {
83 onError(c('Error').t`A group with this name already exists`, index);
84 } else if (FORBIDDEN_LABEL_NAMES.includes(normalize(name))) {
85 onError(c('Error').t`Invalid name`, index);
87 onChangeTargetName(target.value, index);
90 if (action === IMPORT_GROUPS_ACTION.CREATE) {
93 id="contact-group-create"
94 placeholder={c('Placeholder').t`Name`}
96 title={c('Title').t`Add contact group name`}
99 onChange={handleChangeGroupName}
104 if (action === IMPORT_GROUPS_ACTION.MERGE) {
107 id="contact-group-select"
109 onChange={({ value }) => onChangeTargetGroup(value, index)}
110 title={c('Title').t`Select contact group`}
112 {groupsOptions.map(({ text, value }) => (
113 <Option key={value.Name} value={value} title={text} />
122 model: ImportContactsModel;
123 setModel: Dispatch<SetStateAction<ImportContactsModel>>;
124 onClose?: () => void;
127 const ContactImportGroups = ({ model, setModel, onClose }: Props) => {
128 const api = useApi();
129 const { call } = useEventManager();
131 const { categories } = model;
133 const cannotSave = categories.some(
134 ({ error, action, targetName }) => !!error || (action === IMPORT_GROUPS_ACTION.CREATE && !targetName)
137 const handleChangeAction = (action: IMPORT_GROUPS_ACTION, index: number) => {
138 setModel((model) => ({
140 categories: model.categories.map((category, j) => {
144 return { ...omit(category, ['error']), action };
149 const handleChangeTargetGroup = (targetGroup: ContactGroup, index: number) => {
150 setModel((model) => ({
152 categories: model.categories.map((category, j) => {
156 return { ...category, targetGroup };
161 const handleChangeTargetName = (targetName: string, index: number) => {
162 setModel((model) => ({
164 categories: model.categories.map((category, j) => {
168 return { ...category, targetName };
173 const handleSetError = (error: string, index: number) => {
174 setModel((model) => ({
176 categories: model.categories.map((category, j) => {
180 return { ...category, error };
185 const handleSubmit = async (event: FormEvent) => {
186 event.preventDefault();
187 event.stopPropagation();
189 setModel((model) => ({ ...model, loading: true }));
190 await submitCategories(model.categories, api);
192 setModel((model) => ({ ...model, loading: false }));
196 const rows = categories.map(({ name, totalContacts, action, targetGroup, targetName, error }, index) => {
197 const totalContactsString = c('Import contact groups info').ngettext(
198 msgid`${totalContacts} contact`,
199 `${totalContacts} contacts`,
202 const categoryString = `${name} (${totalContactsString})`;
206 className="flex flex-nowrap flex-1 items-stretch sm:items-center flex-column sm:flex-row mb-4 gap-2"
208 <div className="sm:flex-1 text-ellipsis" title={categoryString}>
211 <div className="sm:flex-1">
215 canMerge={!!model.contactGroups?.length}
216 onChange={handleChangeAction}
219 <div className="sm:flex-1 sm:w-3/10">
221 contactGroups={model.contactGroups}
223 targetGroup={targetGroup}
224 targetName={targetName}
227 onChangeTargetGroup={handleChangeTargetGroup}
228 onChangeTargetName={handleChangeTargetName}
229 onError={handleSetError}
237 <form className="modal-two-dialog-container h-full" onSubmit={handleSubmit}>
238 <ModalTwoHeader title={c('Title').t`Import groups`} />
240 <Alert className="mb-4">
242 .t`It looks like the contact list you are importing contains some groups. Please review how these groups should be imported.`}
247 <Button onClick={onClose}>{c('Action').t`Cancel`}</Button>
248 <Button color="norm" disabled={cannotSave} loading={model.loading} type="submit">
249 {c('Action').t`Save`}
256 export default ContactImportGroups;