diff --git a/components/contacts/__tests__/contacts-sidebar.test.tsx b/components/contacts/__tests__/contacts-sidebar.test.tsx
index a47534d0a..11a15b9a3 100644
--- a/components/contacts/__tests__/contacts-sidebar.test.tsx
+++ b/components/contacts/__tests__/contacts-sidebar.test.tsx
@@ -2,7 +2,7 @@ import { render, screen } from '@testing-library/react';
import { fireEvent } from '@testing-library/dom';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { ContactsSidebar } from '../contacts-sidebar';
-import type { ContactCard } from '@/lib/jmap/types';
+import type { AddressBook, ContactCard } from '@/lib/jmap/types';
// next-intl + next/navigation are mocked globally in vitest.setup (t returns the key).
vi.mock('@/stores/account-store', () => {
@@ -63,3 +63,51 @@ describe('ContactsSidebar — compose to group', () => {
expect(onComposeGroup).toHaveBeenCalledWith('g1', 'cc');
});
});
+
+const books: AddressBook[] = [
+ { id: 'ab-1', name: 'Personal', isDefault: true },
+ { id: 'ab-2', name: 'Work' },
+ { id: 'ab-3', name: 'Team', isShared: true, accountId: 'account-2', accountName: 'Team account' },
+];
+
+function renderBooksSidebar(onSetDefaultAddressBook = vi.fn()) {
+ render(
+ ,
+ );
+ return onSetDefaultAddressBook;
+}
+
+describe('ContactsSidebar - set default address book', () => {
+ beforeEach(() => vi.clearAllMocks());
+
+ it('offers "Set as default" for an owned, non-default book', () => {
+ const onSetDefaultAddressBook = renderBooksSidebar();
+ fireEvent.contextMenu(screen.getByText('Work'));
+
+ fireEvent.click(screen.getByText('address_books.set_default'));
+ expect(onSetDefaultAddressBook).toHaveBeenCalledWith(expect.objectContaining({ id: 'ab-2' }));
+ });
+
+ it('hides it for the book that is already the default', () => {
+ renderBooksSidebar();
+ fireEvent.contextMenu(screen.getByText('Personal'));
+
+ expect(screen.queryByText('address_books.set_default')).not.toBeInTheDocument();
+ });
+
+ it('hides it for a book shared from another account', () => {
+ renderBooksSidebar();
+ fireEvent.contextMenu(screen.getByText('Team'));
+
+ expect(screen.queryByText('address_books.set_default')).not.toBeInTheDocument();
+ });
+});
diff --git a/components/contacts/contact-form.tsx b/components/contacts/contact-form.tsx
index 566d53011..b8265c545 100644
--- a/components/contacts/contact-form.tsx
+++ b/components/contacts/contact-form.tsx
@@ -359,6 +359,12 @@ export function ContactForm({ contact, addressBooks, allKeywords, defaultAddress
if (defaultAddressBookId && addressBooks?.some(b => b.id === defaultAddressBookId)) {
return defaultAddressBookId;
}
+ // New contacts land in the account's default book unless told otherwise -
+ // preselect it so the form shows where the contact will actually go.
+ if (!contact) {
+ const ownDefault = addressBooks?.find(b => b.isDefault && !b.isShared);
+ if (ownDefault) return ownDefault.id;
+ }
return "";
}, [contact, defaultAddressBookId, addressBooks]);
const [selectedBookId, setSelectedBookId] = useState(currentBookId);
diff --git a/components/contacts/contacts-app.tsx b/components/contacts/contacts-app.tsx
index a9def7097..ff7292a0f 100644
--- a/components/contacts/contacts-app.tsx
+++ b/components/contacts/contacts-app.tsx
@@ -98,6 +98,7 @@ export function ContactsApp({ linkSegments }: ContactsAppProps = {}) {
renameAddressBook,
removeAddressBook,
shareAddressBook,
+ setDefaultAddressBook,
renameKeyword,
importContacts,
} = useContactStore();
@@ -950,6 +951,14 @@ export function ContactsApp({ linkSegments }: ContactsAppProps = {}) {
onDropContactsToCategory={handleDropContactsToCategory}
onRenameAddressBook={client ? (book) => setRenamingAddressBook(book) : undefined}
onShareAddressBook={client ? (book) => setSharingAddressBookId(book.id) : undefined}
+ onSetDefaultAddressBook={client ? async (book) => {
+ try {
+ await setDefaultAddressBook(client, book);
+ toast.success(t("address_books.default_updated"));
+ } catch {
+ toast.error(t("address_books.set_default_failed"));
+ }
+ } : undefined}
onCreateContactInBook={(book) => {
setDefaultBookIdForCreate(book.id);
handleCreateNew();
diff --git a/components/contacts/contacts-sidebar.tsx b/components/contacts/contacts-sidebar.tsx
index c645f2d3d..dc6ed5ad5 100644
--- a/components/contacts/contacts-sidebar.tsx
+++ b/components/contacts/contacts-sidebar.tsx
@@ -2,7 +2,7 @@
import { useMemo, useState, useCallback, useEffect, useRef, type DragEvent } from "react";
import { useTranslations } from "next-intl";
-import { BookUser, User, Users, Plus, Share2, Book, BookPlus, ChevronRight, ChevronDown, UserPlus, UsersRound, Upload, Tag, Pencil, Trash2, Settings, Mail } from "lucide-react";
+import { BookUser, User, Users, Plus, Share2, Book, BookPlus, ChevronRight, ChevronDown, UserPlus, UsersRound, Upload, Tag, Pencil, Trash2, Settings, Mail, Star } from "lucide-react";
import { useRouter } from "next/navigation";
import { Button } from "@/components/ui/button";
import { ContextMenu, ContextMenuItem, ContextMenuSeparator, ContextMenuSubMenu } from "@/components/ui/context-menu";
@@ -31,6 +31,7 @@ interface ContactsSidebarProps {
onDropContactsToCategory?: (contactIds: string[], keyword: string) => void;
onRenameAddressBook?: (addressBook: AddressBook) => void;
onShareAddressBook?: (addressBook: AddressBook) => void;
+ onSetDefaultAddressBook?: (addressBook: AddressBook) => void;
onCreateContactInBook?: (addressBook: AddressBook) => void;
onDeleteAddressBook?: (addressBook: AddressBook) => void;
onRenameKeyword?: (keyword: string) => void;
@@ -101,6 +102,7 @@ export function ContactsSidebar({
onDropContactsToCategory,
onRenameAddressBook,
onShareAddressBook,
+ onSetDefaultAddressBook,
onCreateContactInBook,
onDeleteAddressBook,
onRenameKeyword,
@@ -111,6 +113,7 @@ export function ContactsSidebar({
const router = useRouter();
const { contextMenu: groupContextMenu, openContextMenu: openGroupContextMenu, closeContextMenu: closeGroupContextMenu, menuRef: groupMenuRef } = useContextMenu();
const { contextMenu: bookContextMenu, openContextMenu: openBookContextMenu, closeContextMenu: closeBookContextMenu, menuRef: bookMenuRef } = useContextMenu();
+ const hasBookMenu = !!(onRenameAddressBook || onShareAddressBook || onSetDefaultAddressBook || onCreateContactInBook || onDeleteAddressBook);
const { contextMenu: keywordContextMenu, openContextMenu: openKeywordContextMenu, closeContextMenu: closeKeywordContextMenu, menuRef: keywordMenuRef } = useContextMenu();
const [collapsed, setCollapsed] = useState>(loadCollapsed);
@@ -383,7 +386,7 @@ export function ContactsSidebar({
contactCount={contactCountByBook[book.id] || 0}
onSelect={() => onSelectCategory({ addressBookId: book.id })}
onDropContacts={onDropContacts}
- onContextMenu={(onRenameAddressBook || onShareAddressBook || onCreateContactInBook || onDeleteAddressBook) ? (e) => openBookContextMenu(e, book) : undefined}
+ onContextMenu={hasBookMenu ? (e) => openBookContextMenu(e, book) : undefined}
/>
))}
@@ -402,7 +405,7 @@ export function ContactsSidebar({
contactCount={contactCountByBook[book.id] || 0}
onSelect={() => onSelectCategory({ addressBookId: book.id })}
onDropContacts={onDropContacts}
- onContextMenu={(onRenameAddressBook || onShareAddressBook || onCreateContactInBook || onDeleteAddressBook) ? (e) => openBookContextMenu(e, book) : undefined}
+ onContextMenu={hasBookMenu ? (e) => openBookContextMenu(e, book) : undefined}
/>
))}
@@ -449,7 +452,7 @@ export function ContactsSidebar({
contactCount={contactCountByBook[book.id] || 0}
onSelect={() => onSelectCategory({ addressBookId: book.id })}
onDropContacts={onDropContacts}
- onContextMenu={(onRenameAddressBook || onShareAddressBook || onCreateContactInBook || onDeleteAddressBook) ? (e) => openBookContextMenu(e, book) : undefined}
+ onContextMenu={hasBookMenu ? (e) => openBookContextMenu(e, book) : undefined}
/>
))}
@@ -593,7 +596,7 @@ export function ContactsSidebar({
contactCount={contactCountByBook[book.id] || 0}
onSelect={() => onSelectCategory({ addressBookId: book.id })}
onDropContacts={onDropContacts}
- onContextMenu={(onRenameAddressBook || onShareAddressBook || onCreateContactInBook || onDeleteAddressBook) ? (e) => openBookContextMenu(e, book) : undefined}
+ onContextMenu={hasBookMenu ? (e) => openBookContextMenu(e, book) : undefined}
/>
))}
@@ -601,13 +604,16 @@ export function ContactsSidebar({
{/* Address book context menu */}
- {bookContextMenu.data && (onRenameAddressBook || onShareAddressBook || onCreateContactInBook || onDeleteAddressBook) && (() => {
+ {bookContextMenu.data && hasBookMenu && (() => {
const book = bookContextMenu.data;
const canCreate = onCreateContactInBook && book.myRights?.mayWrite !== false;
const canRename = onRenameAddressBook && book.myRights?.mayWrite !== false;
const canShare = onShareAddressBook && book.myRights?.mayShare && !book.isShared;
+ // The default only applies to books the account owns, and re-setting the
+ // current default is a no-op, so hide it in both cases.
+ const canSetDefault = onSetDefaultAddressBook && !book.isShared && !book.isDefault;
const canDelete = onDeleteAddressBook && !book.isDefault && !book.isShared && book.myRights?.mayDelete !== false;
- const showSeparator = (canCreate || canRename || canShare) && canDelete;
+ const showSeparator = (canCreate || canRename || canShare || canSetDefault) && canDelete;
return (
)}
+ {canSetDefault && (
+ {
+ closeBookContextMenu();
+ onSetDefaultAddressBook(book);
+ }}
+ />
+ )}
{showSeparator && }
{canDelete && (
s.managedAccountId);
- const { addressBooks, contacts, supportsSync, fetchAddressBooks, createAddressBook, renameAddressBook, shareAddressBook, renameKeyword } = useContactStore();
+ const { addressBooks, contacts, supportsSync, fetchAddressBooks, createAddressBook, renameAddressBook, shareAddressBook, setDefaultAddressBook, renameKeyword } = useContactStore();
const [editingId, setEditingId] = useState(null);
const [editingKeyword, setEditingKeyword] = useState(null);
const [sharingId, setSharingId] = useState(null);
@@ -116,6 +116,19 @@ export function AddressBookManagementSettings() {
}
};
+ const handleSetDefault = async (book: AddressBook) => {
+ if (!client) return;
+ setIsLoading(true);
+ try {
+ await setDefaultAddressBook(client, book);
+ toast.success(t("default_updated"));
+ } catch {
+ toast.error(t("set_default_failed"));
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
// Group: personal first, then by shared account
const personal = addressBooks.filter((b) => !b.isShared);
const sharedGroups = new Map();
@@ -141,6 +154,9 @@ export function AddressBookManagementSettings() {
}
const canRename = !book.isShared || book.myRights?.mayWrite !== false;
+ // The default only applies to books the account owns, and re-setting the
+ // current default is a no-op, so hide it in both cases.
+ const canSetDefault = !!client && !book.isShared && !book.isDefault;
return (
)}
+ {canSetDefault && (
+
+ )}
{!book.isShared && book.myRights?.mayShare && (