fix: start the new group with the selected contacts when Add to group finds no group

This commit is contained in:
Linus Rath
2026-10-28 03:12:17 +01:00
parent 2e86f7af07
commit f6e4aaa254
31 changed files with 72 additions and 3 deletions
@@ -125,4 +125,20 @@ describe('ContactsApp', () => {
expect(screen.queryByRole('alertdialog')).toBeNull();
expect(useContactStore.getState().selectedContactId).toBe('c-bob');
});
it('starts a new group with the contact when "Add to group" finds no group to add to', () => {
renderContacts();
fireEvent.contextMenu(screen.getByText('Alice Smith'));
fireEvent.click(screen.getByText('context_menu.add_to_group'));
expect(screen.getByText('groups.members_label (1)')).toBeInTheDocument();
});
it('offers a new group, with the selection in it, next to the existing groups', () => {
const team: ContactCard = { id: 'g-team', kind: 'group', addressBookIds: {}, name: { full: 'Team' }, members: {} };
renderContacts([alice, bob, team]);
fireEvent.contextMenu(screen.getByText('Bob Jones'));
fireEvent.click(screen.getByText('context_menu.add_to_group'));
fireEvent.click(screen.getByText('bulk.new_group'));
expect(screen.getByText('groups.members_label (1)')).toBeInTheDocument();
});
});
+27 -3
View File
@@ -4,7 +4,7 @@ import { Suspense, useState, useEffect, useCallback, useRef, useMemo } from "rea
import { useTranslations } from "next-intl";
import { useSearchParams } from "next/navigation";
import { useRouter } from "@/i18n/navigation";
import { ArrowLeft, Users, AlertTriangle } from "@/components/icons";
import { ArrowLeft, Users, UsersRound, AlertTriangle } from "@/components/icons";
import { Button } from "@/components/ui/button";
import { ConfirmDialog } from "@/components/ui/confirm-dialog";
import { useConfirmDialog } from "@/hooks/use-confirm-dialog";
@@ -130,6 +130,8 @@ function ContactsAppContent({ linkSegments: routeSegments }: ContactsAppProps =
const [returnToEmail, setReturnToEmail] = useState(false);
const [renamingKeyword, setRenamingKeyword] = useState<string | null>(null);
const [selectedGroupId, setSelectedGroupId] = useState<string | null>(null);
// Who a new group starts with: the selection when it came from "Add to group".
const [newGroupMemberIds, setNewGroupMemberIds] = useState<string[]>([]);
const [directorySelection, setDirectorySelection] = useState<Set<string>>(() => new Set());
const [openDirectoryKey, setOpenDirectoryKey] = useState<string | null>(null);
const directoryPrincipals = useContactStore((s) => s.directoryPrincipals);
@@ -538,6 +540,8 @@ function ContactsAppContent({ linkSegments: routeSegments }: ContactsAppProps =
clearSelection();
toggleContactSelection(id);
if (groups.length === 0) {
// No group to add to yet: a new one, with the contact already in it.
setNewGroupMemberIds([id]);
setView("group-create");
return;
}
@@ -630,6 +634,14 @@ function ContactsAppContent({ linkSegments: routeSegments }: ContactsAppProps =
const handleCreateGroup = () => {
setSelectedGroupId(null);
setNewGroupMemberIds([]);
setView("group-create");
};
// "Add to group": a new group starting with the selected contacts.
const handleCreateGroupFromSelection = () => {
setSelectedGroupId(null);
setNewGroupMemberIds(Array.from(selectedContactIds));
setView("group-create");
};
@@ -769,9 +781,10 @@ function ContactsAppContent({ linkSegments: routeSegments }: ContactsAppProps =
} else {
await createGroup(jmapClient, name, memberIds);
toast.success(t("toast.created"));
clearSelection();
setView("list");
}
}, [view, selectedGroup, selectedGroupMembers, supportsSync, client, createGroup, updateGroup, addMembersToGroup, removeMembersFromGroup, t]);
}, [view, selectedGroup, selectedGroupMembers, supportsSync, client, createGroup, updateGroup, addMembersToGroup, removeMembersFromGroup, clearSelection, t]);
const handleRemoveGroupMember = async (memberId: string) => {
if (!selectedGroup) return;
@@ -815,7 +828,7 @@ function ContactsAppContent({ linkSegments: routeSegments }: ContactsAppProps =
const handleBulkAddToGroup = () => {
if (selectedContactIds.size === 0) return;
if (groups.length === 0) {
setView("group-create");
handleCreateGroupFromSelection();
return;
}
setView("bulk-add-to-group");
@@ -897,6 +910,7 @@ function ContactsAppContent({ linkSegments: routeSegments }: ContactsAppProps =
return (
<ContactGroupForm
individuals={individuals}
currentMemberIds={newGroupMemberIds}
onSave={handleSaveGroup}
onCancel={guard(handleCancel)}
onDirtyChange={setEditorDirty}
@@ -926,6 +940,16 @@ function ContactsAppContent({ linkSegments: routeSegments }: ContactsAppProps =
</p>
</div>
<div className="flex-1 overflow-y-auto divide-y divide-border">
<button
type="button"
onClick={handleCreateGroupFromSelection}
className="w-full flex items-center gap-3 px-6 py-3 text-start hover:bg-muted transition-colors"
>
<div className="w-9 h-9 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0">
<UsersRound className="w-4 h-4 text-primary" />
</div>
<div className="flex-1 min-w-0 text-sm font-medium">{t("bulk.new_group")}</div>
</button>
{groups.map((group) => {
const gName = getContactDisplayName(group);
const memberCount = group.members
+1
View File
@@ -3200,6 +3200,7 @@
"deleted": "{count, plural, one {تم حذف جهة اتصال واحدة} other {تم حذف # جهة اتصال}}",
"add_to_group": "إضافة إلى مجموعة",
"choose_group": "اختر مجموعة",
"new_group": "مجموعة جديدة…",
"adding_contacts": "جارٍ إضافة {count, plural, one {جهة اتصال واحدة} other {# جهة اتصال}}",
"added_to_group": "تمت إضافة جهات الاتصال إلى المجموعة",
"export": "تصدير",
+1
View File
@@ -3200,6 +3200,7 @@
"deleted": "{count, plural, one {1 contacte suprimit} other {# contactes suprimits}}",
"add_to_group": "Afegeix al grup",
"choose_group": "Trieu un grup",
"new_group": "Grup nou…",
"adding_contacts": "Afegint {count, plural, one {1 contacte} other {# contactes}}",
"added_to_group": "Contactes afegits al grup",
"export": "Exporta",
+1
View File
@@ -3199,6 +3199,7 @@
"deleted": "{count, plural, one {Odstraněn 1 kontakt} few {Odstraněny # kontakty} other {Odstraněno # kontaktů}}",
"add_to_group": "Přidat do skupiny",
"choose_group": "Vyberte skupinu",
"new_group": "Nová skupina…",
"adding_contacts": "Přidávání {count, plural, one {1 kontaktu} few {# kontaktů} other {# kontaktů}}",
"added_to_group": "Kontakty byly přidány do skupiny",
"export": "Exportovat",
+1
View File
@@ -3199,6 +3199,7 @@
"deleted": "{count, plural, one {1 kontakt slettet} other {# kontakter slettet}}",
"add_to_group": "Tilføj til gruppe",
"choose_group": "Vælg en gruppe",
"new_group": "Ny gruppe…",
"adding_contacts": "Tilføjer {count, plural, one {1 kontakt} other {# kontakter}}",
"added_to_group": "Kontakter tilføjet til gruppe",
"export": "Eksportér",
+1
View File
@@ -3199,6 +3199,7 @@
"deleted": "{count, plural, one {1 Kontakt gelöscht} other {# Kontakte gelöscht}}",
"add_to_group": "Zur Gruppe hinzufügen",
"choose_group": "Gruppe wählen",
"new_group": "Neue Gruppe…",
"adding_contacts": "{count, plural, one {1 Kontakt} other {# Kontakte}} hinzufügen",
"added_to_group": "Kontakte zur Gruppe hinzugefügt",
"export": "Exportieren",
+1
View File
@@ -3200,6 +3200,7 @@
"deleted": "{count, plural, one {1 contact deleted} other {# contacts deleted}}",
"add_to_group": "Add to group",
"choose_group": "Choose a group",
"new_group": "New group…",
"adding_contacts": "Adding {count, plural, one {1 contact} other {# contacts}}",
"added_to_group": "Contacts added to group",
"export": "Export",
+1
View File
@@ -3199,6 +3199,7 @@
"deleted": "{count, plural, one {1 contacto eliminado} other {# contactos eliminados}}",
"add_to_group": "Agregar al grupo",
"choose_group": "Elegir un grupo",
"new_group": "Nuevo grupo…",
"adding_contacts": "Agregando {count, plural, one {1 contacto} other {# contactos}}",
"added_to_group": "Contactos agregados al grupo",
"export": "Exportar",
+1
View File
@@ -3200,6 +3200,7 @@
"deleted": "{count, plural, one {Kustutati 1 kontakt} other {Kustutati # kontakti}}",
"add_to_group": "Lisa gruppi",
"choose_group": "Vali grupp",
"new_group": "Uus rühm…",
"adding_contacts": "Lisamine: {count, plural, one {1 kontakt} other {# kontakti}}",
"added_to_group": "Kontaktid lisati gruppi",
"export": "Eksport",
+1
View File
@@ -3200,6 +3200,7 @@
"deleted": "{count, plural, one {۱ مخاطب حذف شد} other {# مخاطب حذف شد}}",
"add_to_group": "افزودن به گروه",
"choose_group": "انتخاب گروه",
"new_group": "گروه جدید…",
"adding_contacts": "افزودن {count, plural, one {۱ مخاطب} other {# مخاطب}}",
"added_to_group": "مخاطبین به گروه اضافه شدند",
"export": "خروجی",
+1
View File
@@ -3199,6 +3199,7 @@
"deleted": "{count, plural, one {1 contact supprimé} other {# contacts supprimés}}",
"add_to_group": "Ajouter au groupe",
"choose_group": "Choisir un groupe",
"new_group": "Nouveau groupe…",
"adding_contacts": "Ajout de {count, plural, one {1 contact} other {# contacts}}",
"added_to_group": "Contacts ajoutés au groupe",
"export": "Exporter",
+1
View File
@@ -3103,6 +3103,7 @@
"deleted": "{count, plural, one {נמחק איש קשר אחד} other {נמחקו # אנשי קשר}}",
"add_to_group": "הוסף לקבוצה",
"choose_group": "בחר קבוצה",
"new_group": "קבוצה חדשה…",
"adding_contacts": "מוסיף {count, plural, one {איש קשר אחד} other {# אנשי קשר}}",
"added_to_group": "אנשי קשר נוספו לקבוצה",
"export": "ייצוא",
+1
View File
@@ -3200,6 +3200,7 @@
"deleted": "{count, plural, one {1 névjegy törölve} other {# névjegy törölve}}",
"add_to_group": "Hozzáadás a csoporthoz",
"choose_group": "Csoport kiválasztása",
"new_group": "Új csoport…",
"adding_contacts": "{count, plural, one {1 névjegy} other {# névjegy}} hozzáadása",
"added_to_group": "Névjegyek hozzáadva a csoporthoz",
"export": "Exportálás",
+1
View File
@@ -3199,6 +3199,7 @@
"deleted": "{count, plural, one {1 contatto eliminato} other {# contatti eliminati}}",
"add_to_group": "Aggiungi al gruppo",
"choose_group": "Scegli un gruppo",
"new_group": "Nuovo gruppo…",
"adding_contacts": "Aggiunta di {count, plural, one {1 contatto} other {# contatti}}",
"added_to_group": "Contatti aggiunti al gruppo",
"export": "Esporta",
+1
View File
@@ -3199,6 +3199,7 @@
"deleted": "{count, plural, other {#件の連絡先を削除しました}}",
"add_to_group": "グループに追加",
"choose_group": "グループを選択",
"new_group": "新しいグループ…",
"adding_contacts": "{count, plural, other {#件の連絡先}}を追加",
"added_to_group": "連絡先をグループに追加しました",
"export": "エクスポート",
+1
View File
@@ -3199,6 +3199,7 @@
"deleted": "{count}개의 연락처가 삭제되었어요",
"add_to_group": "그룹에 추가",
"choose_group": "그룹 선택",
"new_group": "새 그룹…",
"adding_contacts": "{count}개의 연락처를 추가하는 중...",
"added_to_group": "연락처가 그룹에 추가되었어요",
"export": "내보내기",
+1
View File
@@ -3195,6 +3195,7 @@
"deleted": "Izdzēsts {count, plural, one {1 kontakts} other {# kontakti}}",
"add_to_group": "Pievienot grupai",
"choose_group": "Izvēlēties grupu",
"new_group": "Jauna grupa…",
"adding_contacts": "Pievieno {count, plural, one {1 kontaktu} other {# kontaktus}}",
"added_to_group": "Kontakti pievienoti grupai",
"export": "Eksportēt",
+1
View File
@@ -3200,6 +3200,7 @@
"deleted": "{count, plural, one {1 контакт устгагдлаа} other {# контакт устгагдлаа}}",
"add_to_group": "Бүлэгт нэмэх",
"choose_group": "Бүлэг сонгох",
"new_group": "Шинэ бүлэг…",
"adding_contacts": "{count, plural, one {1 контакт} other {# контакт}} нэмэж байна",
"added_to_group": "Харилцагчдыг группт нэмсэн",
"export": "Экспорт хийх",
+1
View File
@@ -3200,6 +3200,7 @@
"deleted": "{count, plural, one {1 kontakt slettet} other {# kontakter slettet}}",
"add_to_group": "Legg til i gruppe",
"choose_group": "Velg en gruppe",
"new_group": "Ny gruppe…",
"adding_contacts": "Legger til {count, plural, one {1 kontakt} other {# kontakter}}",
"added_to_group": "Kontaktene er lagt til i gruppen",
"export": "Eksporter",
+1
View File
@@ -3199,6 +3199,7 @@
"deleted": "{count, plural, one {1 contact verwijderd} other {# contacten verwijderd}}",
"add_to_group": "Aan groep toevoegen",
"choose_group": "Kies een groep",
"new_group": "Nieuwe groep…",
"adding_contacts": "{count, plural, one {1 contact} other {# contacten}} toevoegen",
"added_to_group": "Contacten aan groep toegevoegd",
"export": "Exporteren",
+1
View File
@@ -3199,6 +3199,7 @@
"deleted": "{count, plural, one {Usunięto 1 kontakt} other {Usunięto # kontaktów}}",
"add_to_group": "Dodaj do grupy",
"choose_group": "Wybierz grupę",
"new_group": "Nowa grupa…",
"adding_contacts": "Dodawanie {count, plural, one {1 kontaktu} other {# kontaktów}}",
"added_to_group": "Kontakty dodano do grupy",
"export": "Eksportuj",
+1
View File
@@ -3199,6 +3199,7 @@
"deleted": "{count, plural, one {1 contato excluído} other {# contatos excluídos}}",
"add_to_group": "Adicionar ao grupo",
"choose_group": "Escolher um grupo",
"new_group": "Novo grupo…",
"adding_contacts": "Adicionando {count, plural, one {1 contato} other {# contatos}}",
"added_to_group": "Contatos adicionados ao grupo",
"export": "Exportar",
+1
View File
@@ -3200,6 +3200,7 @@
"deleted": "{count, plural, one {1 contact șters} few {# contacte șterse} other {# de contacte șterse}}",
"add_to_group": "Adaugă la grup",
"choose_group": "Alegeți un grup",
"new_group": "Grup nou…",
"adding_contacts": "Adăugare {count, plural, one {1 contact} few {# contacte} other {# de contacte}}",
"added_to_group": "Contacte adăugate la grup",
"export": "Export",
+1
View File
@@ -3199,6 +3199,7 @@
"deleted": "{count, plural, one {1 контакт удалён} other {# контактов удалено}}",
"add_to_group": "Добавить в группу",
"choose_group": "Выбрать группу",
"new_group": "Новая группа…",
"adding_contacts": "Добавляем {count, plural, one {1 контакт} other {# контактов}}",
"added_to_group": "Контакты добавлены в группу",
"export": "Экспорт",
+1
View File
@@ -3200,6 +3200,7 @@
"deleted": "{count, plural, one {Odstránený 1 kontakt} other {Odstránených # kontaktov}}",
"add_to_group": "Pridať do skupiny",
"choose_group": "Vyberte skupinu",
"new_group": "Nová skupina…",
"adding_contacts": "Pridávanie {count, plural, one {1 kontaktu} other {# kontaktov}}",
"added_to_group": "Kontakty boli pridané do skupiny",
"export": "Exportovať",
+1
View File
@@ -2514,6 +2514,7 @@
"deleted": "{count, plural, one {ลบผู้ติดต่อแล้ว 1 รายการ} other {ลบผู้ติดต่อแล้ว # รายการ}}",
"add_to_group": "เพิ่มในกลุ่ม",
"choose_group": "เลือกกลุ่ม",
"new_group": "กลุ่มใหม่…",
"adding_contacts": "กำลังเพิ่มผู้ติดต่อ {count, plural, one {1 รายการ} other {# รายการ}}",
"added_to_group": "เพิ่มผู้ติดต่อในกลุ่มแล้ว",
"export": "ส่งออก",
+1
View File
@@ -3199,6 +3199,7 @@
"deleted": "{count, plural, one {1 kişi silindi} other {# kişi silindi}}",
"add_to_group": "Gruba ekle",
"choose_group": "Grup seçin",
"new_group": "Yeni grup…",
"adding_contacts": "{count, plural, one {1 kişi} other {# kişi}} ekleniyor",
"added_to_group": "Kişiler gruba eklendi",
"export": "Dışa Aktar",
+1
View File
@@ -3199,6 +3199,7 @@
"deleted": "{count, plural, one {1 контакт видалено} few {# контакти видалено} many {# контактів видалено} other {# контактів видалено}}",
"add_to_group": "Додати до групи",
"choose_group": "Виберіть групу",
"new_group": "Нова група…",
"adding_contacts": "Додавання {count, plural, one {1 contact} other {# contacts}}",
"added_to_group": "Контакти додано до групи",
"export": "Експорт",
+1
View File
@@ -3200,6 +3200,7 @@
"deleted": "{count, plural, one {已刪除 1 位聯絡人} other {已刪除 # 位聯絡人}}",
"add_to_group": "新增至群組",
"choose_group": "選擇群組",
"new_group": "新增群組…",
"adding_contacts": "正在新增{count, plural, one {1 位聯絡人} other {# 位聯絡人}}",
"added_to_group": "聯絡人已新增至群組",
"export": "匯出",
+1
View File
@@ -3199,6 +3199,7 @@
"deleted": "{count, plural, one {已删除 1 位联系人} other {已删除 # 位联系人}}",
"add_to_group": "添加到群组",
"choose_group": "选择一个组",
"new_group": "新建群组…",
"adding_contacts": "正在添加{count, plural, one {1 位联系人} other {# 位联系人}}",
"added_to_group": "联系人已添加至群组",
"export": "导出",