From ffad3ea78ba52757f7d5a714f7f03a12a904faf7 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Wed, 8 Apr 2026 13:05:42 +0200 Subject: [PATCH] feat: Ability to rename address book #152 --- app/[locale]/contacts/page.tsx | 47 ++++ app/[locale]/settings/page.tsx | 3 +- components/contacts/contacts-sidebar.tsx | 139 ++++++++-- .../address-book-management-settings.tsx | 247 ++++++++++++++++++ components/settings/advanced-settings.tsx | 2 +- lib/demo/demo-client.ts | 5 + lib/jmap/client-interface.ts | 1 + lib/jmap/client.ts | 29 ++ locales/de/common.json | 20 +- locales/en/common.json | 20 +- locales/es/common.json | 20 +- locales/fr/common.json | 20 +- locales/it/common.json | 20 +- locales/ja/common.json | 20 +- locales/ko/common.json | 20 +- locales/nl/common.json | 20 +- locales/pl/common.json | 20 +- locales/pt/common.json | 20 +- locales/ru/common.json | 20 +- locales/zh/common.json | 20 +- stores/contact-store.ts | 53 ++++ 21 files changed, 712 insertions(+), 54 deletions(-) create mode 100644 components/settings/address-book-management-settings.tsx diff --git a/app/[locale]/contacts/page.tsx b/app/[locale]/contacts/page.tsx index 3fdafc5e..2b1b7758 100644 --- a/app/[locale]/contacts/page.tsx +++ b/app/[locale]/contacts/page.tsx @@ -13,6 +13,7 @@ import { ContactGroupForm } from "@/components/contacts/contact-group-form"; import { ContactGroupDetail } from "@/components/contacts/contact-group-detail"; import { ContactsSidebar, type ContactCategory } from "@/components/contacts/contacts-sidebar"; import { ContactImportDialog } from "@/components/contacts/contact-import-dialog"; +import { RenameDialog } from "@/components/files/rename-dialog"; import { exportContacts } from "@/components/contacts/contact-export"; import { useContactStore, getContactDisplayName } from "@/stores/contact-store"; import { useAuthStore, redirectToLogin } from "@/stores/auth-store"; @@ -72,12 +73,16 @@ export default function ContactsPage() { bulkDeleteContacts, bulkAddToGroup, moveContactToAddressBook, + renameAddressBook, + renameKeyword, importContacts, } = useContactStore(); const [view, setView] = useState("list"); const [activeCategory, setActiveCategory] = useState("all"); const [showImportDialog, setShowImportDialog] = useState(false); + const [renamingAddressBook, setRenamingAddressBook] = useState(null); + const [renamingKeyword, setRenamingKeyword] = useState(null); const [selectedGroupId, setSelectedGroupId] = useState(null); const hasFetched = useRef(false); const { dialogProps: confirmDialogProps, confirm: confirmDialog } = useConfirmDialog(); @@ -631,6 +636,8 @@ export default function ContactsPage() { onDeleteGroup={handleDeleteGroupFromSidebar} onDropContacts={handleDropContacts} onDropContactsToCategory={handleDropContactsToCategory} + onRenameAddressBook={client ? (book) => setRenamingAddressBook(book) : undefined} + onRenameKeyword={(kw) => setRenamingKeyword(kw)} /> + {renamingKeyword !== null && ( + setRenamingKeyword(null)} + onConfirm={async (newName) => { + try { + await renameKeyword(supportsSync && client ? client : null, renamingKeyword, newName); + toast.success(t("category_renamed")); + if (typeof activeCategory === "object" && "keyword" in activeCategory && activeCategory.keyword === renamingKeyword) { + setActiveCategory({ keyword: newName.trim() }); + } + setRenamingKeyword(null); + } catch (err) { + console.error("Failed to rename category:", err); + toast.error(t("category_rename_failed")); + } + }} + /> + )} + {renamingAddressBook && ( + setRenamingAddressBook(null)} + onConfirm={async (newName) => { + if (!client) return; + try { + await renameAddressBook(client, renamingAddressBook, newName); + toast.success(t("address_books.renamed")); + setRenamingAddressBook(null); + } catch (err) { + console.error("Failed to rename address book:", err); + toast.error(t("address_books.rename_failed")); + } + }} + /> + )} {showImportDialog && (
diff --git a/app/[locale]/settings/page.tsx b/app/[locale]/settings/page.tsx index 0d15900a..33e93943 100644 --- a/app/[locale]/settings/page.tsx +++ b/app/[locale]/settings/page.tsx @@ -36,6 +36,7 @@ import { IdentitySettings } from '@/components/settings/identity-settings'; import { VacationSettings } from '@/components/settings/vacation-settings'; import { CalendarSettings } from '@/components/settings/calendar-settings'; import { CalendarManagementSettings } from '@/components/settings/calendar-management-settings'; +import { AddressBookManagementSettings } from '@/components/settings/address-book-management-settings'; import { FilterSettings } from '@/components/settings/filter-settings'; import { TemplateSettings } from '@/components/settings/template-settings'; import { AdvancedSettings } from '@/components/settings/advanced-settings'; @@ -211,7 +212,7 @@ export default function SettingsPage() { {activeTab === 'encryption' && } {activeTab === 'vacation' && } {activeTab === 'calendar' && <>
} - {activeTab === 'contacts' && } + {activeTab === 'contacts' && <>
} {activeTab === 'filters' && } {activeTab === 'templates' && } {activeTab === 'folders' && } diff --git a/components/contacts/contacts-sidebar.tsx b/components/contacts/contacts-sidebar.tsx index f4f5cd45..71051635 100644 --- a/components/contacts/contacts-sidebar.tsx +++ b/components/contacts/contacts-sidebar.tsx @@ -2,7 +2,8 @@ import { useMemo, useState, useCallback, useEffect, useRef, type DragEvent } from "react"; import { useTranslations } from "next-intl"; -import { BookUser, Users, Plus, Share2, Book, ChevronRight, ChevronDown, UserPlus, UsersRound, Upload, Tag, Pencil, Trash2 } from "lucide-react"; +import { BookUser, Users, Plus, Share2, Book, ChevronRight, ChevronDown, UserPlus, UsersRound, Upload, Tag, Pencil, Trash2, Settings } from "lucide-react"; +import { useRouter } from "next/navigation"; import { Button } from "@/components/ui/button"; import { ContextMenu, ContextMenuItem, ContextMenuSeparator } from "@/components/ui/context-menu"; import { useContextMenu } from "@/hooks/use-context-menu"; @@ -25,6 +26,8 @@ interface ContactsSidebarProps { onDeleteGroup?: (groupId: string) => void; onDropContacts?: (contactIds: string[], addressBook: AddressBook) => void; onDropContactsToCategory?: (contactIds: string[], keyword: string) => void; + onRenameAddressBook?: (addressBook: AddressBook) => void; + onRenameKeyword?: (keyword: string) => void; className?: string; } @@ -58,10 +61,15 @@ export function ContactsSidebar({ onDeleteGroup, onDropContacts, onDropContactsToCategory, + onRenameAddressBook, + onRenameKeyword, className, }: ContactsSidebarProps) { const t = useTranslations("contacts"); + const router = useRouter(); const { contextMenu: groupContextMenu, openContextMenu: openGroupContextMenu, closeContextMenu: closeGroupContextMenu, menuRef: groupMenuRef } = useContextMenu(); + const { contextMenu: bookContextMenu, openContextMenu: openBookContextMenu, closeContextMenu: closeBookContextMenu, menuRef: bookMenuRef } = useContextMenu(); + const { contextMenu: keywordContextMenu, openContextMenu: openKeywordContextMenu, closeContextMenu: closeKeywordContextMenu, menuRef: keywordMenuRef } = useContextMenu(); const [collapsed, setCollapsed] = useState>(loadCollapsed); const [showMenu, setShowMenu] = useState(false); @@ -246,19 +254,32 @@ export function ContactsSidebar({ {/* My Address Books */} {personalBooks.length > 0 && (
- +
+ + +
{!collapsed.addressBooks && personalBooks.map((book) => ( onSelectCategory({ addressBookId: book.id })} onDropContacts={onDropContacts} + onContextMenu={onRenameAddressBook ? (e) => openBookContextMenu(e, book) : undefined} /> ))}
@@ -362,6 +384,7 @@ export function ContactsSidebar({ isActive={isActive} onSelect={() => onSelectCategory({ keyword })} onDropContacts={onDropContactsToCategory} + onContextMenu={onRenameKeyword ? (e) => openKeywordContextMenu(e, keyword) : undefined} /> ); })} @@ -372,20 +395,33 @@ export function ContactsSidebar({ {/* Shared accounts with address books */} {sharedBookGroups.map((group) => (
- +
+ + +
{!collapsed[`shared-${group.accountId}`] && group.books.map((book) => ( onSelectCategory({ addressBookId: book.id })} onDropContacts={onDropContacts} + onContextMenu={onRenameAddressBook ? (e) => openBookContextMenu(e, book) : undefined} /> ))}
))}
+ {/* Address book context menu */} + {bookContextMenu.data && onRenameAddressBook && ( + + { + const book = bookContextMenu.data!; + closeBookContextMenu(); + onRenameAddressBook(book); + }} + /> + + )} + + {/* Keyword (category) context menu */} + {keywordContextMenu.data && onRenameKeyword && ( + + { + const kw = keywordContextMenu.data!; + closeKeywordContextMenu(); + onRenameKeyword(kw); + }} + /> + + )} + {/* Group context menu */} {groupContextMenu.data && ( void; onDropContacts?: (contactIds: string[], keyword: string) => void; + onContextMenu?: (e: React.MouseEvent) => void; }) { const [isDragOver, setIsDragOver] = useState(false); @@ -476,6 +555,7 @@ function CategoryItem({ return ( + +
+ + ); +} + +export function AddressBookManagementSettings() { + const t = useTranslations("contacts.address_books"); + const tContacts = useTranslations("contacts"); + const tSettings = useTranslations("settings.contacts"); + const { client } = useAuthStore(); + const { addressBooks, contacts, supportsSync, fetchAddressBooks, renameAddressBook, renameKeyword } = useContactStore(); + const [editingId, setEditingId] = useState(null); + const [editingKeyword, setEditingKeyword] = useState(null); + const [isLoading, setIsLoading] = useState(false); + + useEffect(() => { + if (client && addressBooks.length === 0) { + fetchAddressBooks(client); + } + }, [client, addressBooks.length, fetchAddressBooks]); + + const handleUpdate = async (book: AddressBook, newName: string) => { + if (!client) return; + setIsLoading(true); + try { + await renameAddressBook(client, book, newName); + setEditingId(null); + toast.success(t("renamed")); + } catch { + toast.error(t("rename_failed")); + } finally { + setIsLoading(false); + } + }; + + // Group: personal first, then by shared account + const personal = addressBooks.filter((b) => !b.isShared); + const sharedGroups = new Map(); + for (const book of addressBooks) { + if (!book.isShared || !book.accountId) continue; + const key = book.accountId; + const existing = sharedGroups.get(key); + if (existing) existing.books.push(book); + else sharedGroups.set(key, { accountName: book.accountName || book.accountId, books: [book] }); + } + + const renderBook = (book: AddressBook) => { + if (editingId === book.id) { + return ( + handleUpdate(book, name)} + onCancel={() => setEditingId(null)} + isLoading={isLoading} + /> + ); + } + + const canRename = !book.isShared || book.myRights?.mayWrite !== false; + + return ( +
+ +
+ {book.name} +
+ {book.isDefault && ( + + {t("default")} + + )} +
+ {canRename && ( + + )} +
+
+ ); + }; + + // Collect keywords with counts + const keywordCounts: Record = {}; + for (const c of contacts) { + if (c.kind === "group" || !c.keywords) continue; + for (const [kw, active] of Object.entries(c.keywords)) { + if (active) keywordCounts[kw] = (keywordCounts[kw] || 0) + 1; + } + } + const sortedKeywords = Object.entries(keywordCounts).sort(([a], [b]) => a.localeCompare(b)); + + const handleRenameKeyword = async (oldKw: string, newKw: string) => { + setIsLoading(true); + try { + await renameKeyword(supportsSync && client ? client : null, oldKw, newKw); + setEditingKeyword(null); + toast.success(tContacts("category_renamed")); + } catch { + toast.error(tContacts("category_rename_failed")); + } finally { + setIsLoading(false); + } + }; + + return ( + <> + +
+ {personal.map(renderBook)} + + {Array.from(sharedGroups.entries()).map(([accountId, group]) => ( +
+

+ + {t("shared_prefix", { name: group.accountName })} +

+ {group.books.map(renderBook)} +
+ ))} + + {addressBooks.length === 0 && ( +

{tSettings("no_address_books")}

+ )} +
+
+ +
+ +
+ {sortedKeywords.map(([keyword, count]) => { + if (editingKeyword === keyword) { + return ( + handleRenameKeyword(keyword, name)} + onCancel={() => setEditingKeyword(null)} + isLoading={isLoading} + /> + ); + } + return ( +
+ +
+ {keyword} +
+ {count} +
+ +
+
+ ); + })} + {sortedKeywords.length === 0 && ( +

{tSettings("no_categories")}

+ )} +
+
+
+ + ); +} diff --git a/components/settings/advanced-settings.tsx b/components/settings/advanced-settings.tsx index 67d1dae0..4d0045ae 100644 --- a/components/settings/advanced-settings.tsx +++ b/components/settings/advanced-settings.tsx @@ -112,7 +112,7 @@ export function AdvancedSettings() { { return [...this.data.addressBooks]; } async getAllAddressBooks(): Promise { return [...this.data.addressBooks]; } + async updateAddressBook(addressBookId: string, updates: Partial): Promise { + const book = this.data.addressBooks.find(b => b.id === addressBookId); + if (book) Object.assign(book, updates); + } + async getContacts(addressBookId?: string): Promise { if (addressBookId) return this.data.contacts.filter(c => c.addressBookIds[addressBookId]); return [...this.data.contacts]; diff --git a/lib/jmap/client-interface.ts b/lib/jmap/client-interface.ts index 127bd0a2..2ee86860 100644 --- a/lib/jmap/client-interface.ts +++ b/lib/jmap/client-interface.ts @@ -178,6 +178,7 @@ export interface IJMAPClient { getContactsAccountId(): string; getAddressBooks(): Promise; getAllAddressBooks(): Promise; + updateAddressBook(addressBookId: string, updates: Partial, targetAccountId?: string): Promise; getContacts(addressBookId?: string): Promise; getAllContacts(): Promise; getContact(contactId: string, accountId?: string): Promise; diff --git a/lib/jmap/client.ts b/lib/jmap/client.ts index dc88b49f..cddd4bea 100644 --- a/lib/jmap/client.ts +++ b/lib/jmap/client.ts @@ -2818,6 +2818,35 @@ export class JMAPClient implements IJMAPClient { } } + async updateAddressBook(addressBookId: string, updates: Partial, targetAccountId?: string): Promise { + const accountId = targetAccountId || this.getContactsAccountId(); + // Only forward server-settable properties + const { name, description, sortOrder, isDefault, color } = updates as Record; + const patch: Record = {}; + if (name !== undefined) patch.name = name; + if (description !== undefined) patch.description = description; + if (sortOrder !== undefined) patch.sortOrder = sortOrder; + if (isDefault !== undefined) patch.isDefault = isDefault; + if (color !== undefined) patch.color = color; + + const response = await this.request([ + ["AddressBook/set", { + accountId, + update: { [addressBookId]: patch }, + }, "0"] + ], this.contactUsing()); + + if (response.methodResponses?.[0]?.[0] === "AddressBook/set") { + const result = response.methodResponses[0][1]; + if (result.notUpdated?.[addressBookId]) { + const error = result.notUpdated[addressBookId]; + throw new Error(error.description || "Failed to update address book"); + } + return; + } + throw new Error("Failed to update address book"); + } + private async fetchPaginatedContacts( accountId: string, filter?: Record, diff --git a/locales/de/common.json b/locales/de/common.json index d2f1e0c8..bc6a92e6 100644 --- a/locales/de/common.json +++ b/locales/de/common.json @@ -1230,7 +1230,13 @@ "import_label": "Import Contacts", "import_description": "Import contacts from a vCard (.vcf) file", "export_label": "Export Contacts", - "export_description": "Export all contacts as a vCard (.vcf) file" + "export_description": "Export all contacts as a vCard (.vcf) file", + "manage_title": "Adressbücher", + "manage_description": "Adressbücher umbenennen", + "no_address_books": "Keine Adressbücher gefunden", + "categories_title": "Kategorien", + "categories_description": "Kontaktkategorien umbenennen", + "no_categories": "Keine Kategorien gefunden" }, "filters": { "title": "E-Mail-Filter", @@ -1635,6 +1641,10 @@ "search_placeholder": "Kontakte suchen...", "create_new": "Neuer Kontakt", "no_category": "Ohne Kategorie", + "rename_category": "Kategorie umbenennen", + "category_name_label": "Kategoriename", + "category_renamed": "Kategorie umbenannt", + "category_rename_failed": "Kategorie konnte nicht umbenannt werden", "category_added": "Kontakt zu {name} hinzugefügt", "category_added_plural": "{count} Kontakte zu {name} hinzugefügt", "empty_state": "Keine Kontakte", @@ -1661,7 +1671,13 @@ "moved": "Kontakt verschoben nach {name}", "moved_plural": "{count} Kontakte verschoben nach {name}", "move_failed": "Kontakt konnte nicht verschoben werden", - "address_book": "Adressbuch" + "address_book": "Adressbuch", + "rename": "Adressbuch umbenennen", + "name_label": "Name des Adressbuchs", + "renamed": "Adressbuch umbenannt", + "rename_failed": "Adressbuch konnte nicht umbenannt werden", + "default": "Standard", + "manage": "Adressbücher verwalten" }, "detail": { "emails": "E-Mail-Adressen", diff --git a/locales/en/common.json b/locales/en/common.json index 3dc5e4cc..9ecc5d70 100644 --- a/locales/en/common.json +++ b/locales/en/common.json @@ -1230,7 +1230,13 @@ "import_label": "Import Contacts", "import_description": "Import contacts from a vCard (.vcf) file", "export_label": "Export Contacts", - "export_description": "Export all contacts as a vCard (.vcf) file" + "export_description": "Export all contacts as a vCard (.vcf) file", + "manage_title": "Address Books", + "manage_description": "Rename your address books", + "no_address_books": "No address books found", + "categories_title": "Categories", + "categories_description": "Rename contact categories", + "no_categories": "No categories found" }, "filters": { "title": "Email Filters", @@ -1635,6 +1641,10 @@ "search_placeholder": "Search contacts...", "create_new": "New Contact", "no_category": "No Category", + "rename_category": "Rename category", + "category_name_label": "Category name", + "category_renamed": "Category renamed", + "category_rename_failed": "Failed to rename category", "category_added": "Contact added to {name}", "category_added_plural": "{count} contacts added to {name}", "empty_state": "No contacts yet", @@ -1661,7 +1671,13 @@ "moved": "Contact moved to {name}", "moved_plural": "{count} contacts moved to {name}", "move_failed": "Failed to move contact", - "address_book": "Address Book" + "address_book": "Address Book", + "rename": "Rename address book", + "name_label": "Address book name", + "renamed": "Address book renamed", + "rename_failed": "Failed to rename address book", + "default": "Default", + "manage": "Manage address books" }, "detail": { "emails": "Email Addresses", diff --git a/locales/es/common.json b/locales/es/common.json index 797d1a87..c4aed84c 100644 --- a/locales/es/common.json +++ b/locales/es/common.json @@ -1230,7 +1230,13 @@ "import_label": "Import Contacts", "import_description": "Import contacts from a vCard (.vcf) file", "export_label": "Export Contacts", - "export_description": "Export all contacts as a vCard (.vcf) file" + "export_description": "Export all contacts as a vCard (.vcf) file", + "manage_title": "Libretas de direcciones", + "manage_description": "Renombrar tus libretas de direcciones", + "no_address_books": "No se encontraron libretas de direcciones", + "categories_title": "Categorías", + "categories_description": "Renombrar categorías de contactos", + "no_categories": "No se encontraron categorías" }, "filters": { "title": "Filtros de correo", @@ -1635,6 +1641,10 @@ "search_placeholder": "Buscar contactos...", "create_new": "Nuevo contacto", "no_category": "Sin categoría", + "rename_category": "Renombrar categoría", + "category_name_label": "Nombre de la categoría", + "category_renamed": "Categoría renombrada", + "category_rename_failed": "Error al renombrar la categoría", "category_added": "Contacto añadido a {name}", "category_added_plural": "{count} contactos añadidos a {name}", "empty_state": "No hay contactos", @@ -1661,7 +1671,13 @@ "moved": "Contacto movido a {name}", "moved_plural": "{count} contactos movidos a {name}", "move_failed": "Error al mover el contacto", - "address_book": "Libreta de direcciones" + "address_book": "Libreta de direcciones", + "rename": "Renombrar libreta de direcciones", + "name_label": "Nombre de la libreta de direcciones", + "renamed": "Libreta de direcciones renombrada", + "rename_failed": "Error al renombrar la libreta de direcciones", + "default": "Predeterminada", + "manage": "Administrar libretas de direcciones" }, "detail": { "emails": "Direcciones de correo", diff --git a/locales/fr/common.json b/locales/fr/common.json index ac3af583..a2f7da9a 100644 --- a/locales/fr/common.json +++ b/locales/fr/common.json @@ -1230,7 +1230,13 @@ "import_label": "Import Contacts", "import_description": "Import contacts from a vCard (.vcf) file", "export_label": "Export Contacts", - "export_description": "Export all contacts as a vCard (.vcf) file" + "export_description": "Export all contacts as a vCard (.vcf) file", + "manage_title": "Carnets d'adresses", + "manage_description": "Renommer vos carnets d'adresses", + "no_address_books": "Aucun carnet d'adresses trouvé", + "categories_title": "Catégories", + "categories_description": "Renommer les catégories de contacts", + "no_categories": "Aucune catégorie trouvée" }, "filters": { "title": "Filtres de courrier", @@ -1635,6 +1641,10 @@ "search_placeholder": "Rechercher des contacts...", "create_new": "Nouveau contact", "no_category": "Sans catégorie", + "rename_category": "Renommer la catégorie", + "category_name_label": "Nom de la catégorie", + "category_renamed": "Catégorie renommée", + "category_rename_failed": "Échec du renommage de la catégorie", "category_added": "Contact ajouté à {name}", "category_added_plural": "{count} contacts ajoutés à {name}", "empty_state": "Aucun contact", @@ -1661,7 +1671,13 @@ "moved": "Contact déplacé vers {name}", "moved_plural": "{count} contacts déplacés vers {name}", "move_failed": "Échec du déplacement du contact", - "address_book": "Carnet d'adresses" + "address_book": "Carnet d'adresses", + "rename": "Renommer le carnet d'adresses", + "name_label": "Nom du carnet d'adresses", + "renamed": "Carnet d'adresses renommé", + "rename_failed": "Échec du renommage du carnet d'adresses", + "default": "Par défaut", + "manage": "Gérer les carnets d'adresses" }, "detail": { "emails": "Adresses e-mail", diff --git a/locales/it/common.json b/locales/it/common.json index 63224765..714b98ea 100644 --- a/locales/it/common.json +++ b/locales/it/common.json @@ -1230,7 +1230,13 @@ "import_label": "Import Contacts", "import_description": "Import contacts from a vCard (.vcf) file", "export_label": "Export Contacts", - "export_description": "Export all contacts as a vCard (.vcf) file" + "export_description": "Export all contacts as a vCard (.vcf) file", + "manage_title": "Rubriche", + "manage_description": "Rinomina le tue rubriche", + "no_address_books": "Nessuna rubrica trovata", + "categories_title": "Categorie", + "categories_description": "Rinomina le categorie dei contatti", + "no_categories": "Nessuna categoria trovata" }, "filters": { "title": "Filtri email", @@ -1635,6 +1641,10 @@ "search_placeholder": "Cerca contatti...", "create_new": "Nuovo contatto", "no_category": "Senza categoria", + "rename_category": "Rinomina categoria", + "category_name_label": "Nome della categoria", + "category_renamed": "Categoria rinominata", + "category_rename_failed": "Impossibile rinominare la categoria", "category_added": "Contatto aggiunto a {name}", "category_added_plural": "{count} contatti aggiunti a {name}", "empty_state": "Nessun contatto", @@ -1661,7 +1671,13 @@ "moved": "Contatto spostato in {name}", "moved_plural": "{count} contatti spostati in {name}", "move_failed": "Impossibile spostare il contatto", - "address_book": "Rubrica" + "address_book": "Rubrica", + "rename": "Rinomina rubrica", + "name_label": "Nome della rubrica", + "renamed": "Rubrica rinominata", + "rename_failed": "Impossibile rinominare la rubrica", + "default": "Predefinita", + "manage": "Gestisci rubriche" }, "detail": { "emails": "Indirizzi email", diff --git a/locales/ja/common.json b/locales/ja/common.json index 2ac2a534..32ec986c 100644 --- a/locales/ja/common.json +++ b/locales/ja/common.json @@ -1230,7 +1230,13 @@ "import_label": "Import Contacts", "import_description": "Import contacts from a vCard (.vcf) file", "export_label": "Export Contacts", - "export_description": "Export all contacts as a vCard (.vcf) file" + "export_description": "Export all contacts as a vCard (.vcf) file", + "manage_title": "アドレス帳", + "manage_description": "アドレス帳の名前を変更", + "no_address_books": "アドレス帳が見つかりません", + "categories_title": "カテゴリ", + "categories_description": "連絡先カテゴリの名前を変更", + "no_categories": "カテゴリが見つかりません" }, "filters": { "title": "メールフィルター", @@ -1635,6 +1641,10 @@ "search_placeholder": "連絡先を検索...", "create_new": "新しい連絡先", "no_category": "カテゴリなし", + "rename_category": "カテゴリの名前を変更", + "category_name_label": "カテゴリ名", + "category_renamed": "カテゴリの名前を変更しました", + "category_rename_failed": "カテゴリの名前変更に失敗しました", "category_added": "{name} に連絡先を追加しました", "category_added_plural": "{count} 件の連絡先を {name} に追加しました", "empty_state": "連絡先がありません", @@ -1661,7 +1671,13 @@ "moved": "連絡先を {name} に移動しました", "moved_plural": "{count} 件の連絡先を {name} に移動しました", "move_failed": "連絡先の移動に失敗しました", - "address_book": "アドレス帳" + "address_book": "アドレス帳", + "rename": "アドレス帳の名前を変更", + "name_label": "アドレス帳名", + "renamed": "アドレス帳の名前を変更しました", + "rename_failed": "アドレス帳の名前変更に失敗しました", + "default": "デフォルト", + "manage": "アドレス帳を管理" }, "detail": { "emails": "メールアドレス", diff --git a/locales/ko/common.json b/locales/ko/common.json index 997a77fa..cad0abd5 100644 --- a/locales/ko/common.json +++ b/locales/ko/common.json @@ -1230,7 +1230,13 @@ "import_label": "연락처 가져오기", "import_description": "vCard(.vcf) 파일에서 연락처를 가져와요", "export_label": "연락처 내보내기", - "export_description": "모든 연락처를 vCard(.vcf) 파일로 저장해요" + "export_description": "모든 연락처를 vCard(.vcf) 파일로 저장해요", + "manage_title": "주소록", + "manage_description": "주소록 이름 변경", + "no_address_books": "주소록을 찾을 수 없음", + "categories_title": "카테고리", + "categories_description": "연락처 카테고리 이름 변경", + "no_categories": "카테고리를 찾을 수 없음" }, "filters": { "title": "이메일 필터", @@ -1635,6 +1641,10 @@ "search_placeholder": "연락처 검색...", "create_new": "새 연락처", "no_category": "카테고리 없음", + "rename_category": "카테고리 이름 변경", + "category_name_label": "카테고리 이름", + "category_renamed": "카테고리 이름이 변경되었습니다", + "category_rename_failed": "카테고리 이름 변경 실패", "category_added": "{name}에 연락처가 추가되었어요", "category_added_plural": "{name}에 {count}개의 연락처가 추가되었어요", "empty_state": "아직 연락처가 없어요", @@ -1661,7 +1671,13 @@ "moved": "연락처가 {name}(으)로 이동되었어요", "moved_plural": "{count}개의 연락처가 {name}(으)로 이동되었어요", "move_failed": "연락처를 이동하지 못했어요", - "address_book": "주소록" + "address_book": "주소록", + "rename": "주소록 이름 변경", + "name_label": "주소록 이름", + "renamed": "주소록 이름이 변경되었습니다", + "rename_failed": "주소록 이름 변경 실패", + "default": "기본", + "manage": "주소록 관리" }, "detail": { "emails": "이메일", diff --git a/locales/nl/common.json b/locales/nl/common.json index b512b439..e180b8f1 100644 --- a/locales/nl/common.json +++ b/locales/nl/common.json @@ -1230,7 +1230,13 @@ "import_label": "Import Contacts", "import_description": "Import contacts from a vCard (.vcf) file", "export_label": "Export Contacts", - "export_description": "Export all contacts as a vCard (.vcf) file" + "export_description": "Export all contacts as a vCard (.vcf) file", + "manage_title": "Adresboeken", + "manage_description": "Hernoem uw adresboeken", + "no_address_books": "Geen adresboeken gevonden", + "categories_title": "Categorieën", + "categories_description": "Contactcategorieën hernoemen", + "no_categories": "Geen categorieën gevonden" }, "filters": { "title": "E-mailfilters", @@ -1635,6 +1641,10 @@ "search_placeholder": "Contacten zoeken...", "create_new": "Nieuw contact", "no_category": "Geen categorie", + "rename_category": "Categorie hernoemen", + "category_name_label": "Categorienaam", + "category_renamed": "Categorie hernoemd", + "category_rename_failed": "Categorie hernoemen mislukt", "category_added": "Contact toegevoegd aan {name}", "category_added_plural": "{count} contacten toegevoegd aan {name}", "empty_state": "Geen contacten", @@ -1661,7 +1671,13 @@ "moved": "Contact verplaatst naar {name}", "moved_plural": "{count} contacten verplaatst naar {name}", "move_failed": "Verplaatsen van contact mislukt", - "address_book": "Adresboek" + "address_book": "Adresboek", + "rename": "Adresboek hernoemen", + "name_label": "Naam van adresboek", + "renamed": "Adresboek hernoemd", + "rename_failed": "Adresboek hernoemen mislukt", + "default": "Standaard", + "manage": "Adresboeken beheren" }, "detail": { "emails": "E-mailadressen", diff --git a/locales/pl/common.json b/locales/pl/common.json index c7b74a68..3b64203f 100644 --- a/locales/pl/common.json +++ b/locales/pl/common.json @@ -1232,7 +1232,13 @@ "import_label": "Importuj kontakty", "import_description": "Importuj kontakty z pliku vCard (.vcf)", "export_label": "Eksportuj kontakty", - "export_description": "Eksportuj wszystkie kontakty jako plik vCard (.vcf)" + "export_description": "Eksportuj wszystkie kontakty jako plik vCard (.vcf)", + "manage_title": "Książki adresowe", + "manage_description": "Zmień nazwy swoich książek adresowych", + "no_address_books": "Nie znaleziono książek adresowych", + "categories_title": "Kategorie", + "categories_description": "Zmień nazwy kategorii kontaktów", + "no_categories": "Nie znaleziono kategorii" }, "filters": { "title": "Filtry wiadomości e-mail", @@ -1637,6 +1643,10 @@ "search_placeholder": "Szukaj kontaktów...", "create_new": "Nowy kontakt", "no_category": "Brak kategorii", + "rename_category": "Zmień nazwę kategorii", + "category_name_label": "Nazwa kategorii", + "category_renamed": "Zmieniono nazwę kategorii", + "category_rename_failed": "Nie udało się zmienić nazwy kategorii", "category_added": "Kontakt dodany do {name}", "category_added_plural": "{count} kontaktów dodano do {name}", "empty_state": "Brak kontaktów", @@ -1663,7 +1673,13 @@ "moved": "Kontakt przeniesiono do {name}", "moved_plural": "{count} kontaktów przeniesiono do {name}", "move_failed": "Nie udało się przenieść kontaktu", - "address_book": "Książka adresowa" + "address_book": "Książka adresowa", + "rename": "Zmień nazwę książki adresowej", + "name_label": "Nazwa książki adresowej", + "renamed": "Zmieniono nazwę książki adresowej", + "rename_failed": "Nie udało się zmienić nazwy książki adresowej", + "default": "Domyślna", + "manage": "Zarządzaj książkami adresowymi" }, "detail": { "emails": "Adresy e-mail", diff --git a/locales/pt/common.json b/locales/pt/common.json index de9cc07b..aa006639 100644 --- a/locales/pt/common.json +++ b/locales/pt/common.json @@ -1230,7 +1230,13 @@ "import_label": "Import Contacts", "import_description": "Import contacts from a vCard (.vcf) file", "export_label": "Export Contacts", - "export_description": "Export all contacts as a vCard (.vcf) file" + "export_description": "Export all contacts as a vCard (.vcf) file", + "manage_title": "Catálogos de endereços", + "manage_description": "Renomeie seus catálogos de endereços", + "no_address_books": "Nenhum catálogo de endereços encontrado", + "categories_title": "Categorias", + "categories_description": "Renomear categorias de contatos", + "no_categories": "Nenhuma categoria encontrada" }, "filters": { "title": "Filtros de e-mail", @@ -1635,6 +1641,10 @@ "search_placeholder": "Pesquisar contatos...", "create_new": "Novo contato", "no_category": "Sem categoria", + "rename_category": "Renomear categoria", + "category_name_label": "Nome da categoria", + "category_renamed": "Categoria renomeada", + "category_rename_failed": "Falha ao renomear a categoria", "category_added": "Contato adicionado a {name}", "category_added_plural": "{count} contatos adicionados a {name}", "empty_state": "Nenhum contato", @@ -1661,7 +1671,13 @@ "moved": "Contato movido para {name}", "moved_plural": "{count} contatos movidos para {name}", "move_failed": "Falha ao mover o contato", - "address_book": "Catálogo de endereços" + "address_book": "Catálogo de endereços", + "rename": "Renomear catálogo de endereços", + "name_label": "Nome do catálogo de endereços", + "renamed": "Catálogo de endereços renomeado", + "rename_failed": "Falha ao renomear o catálogo de endereços", + "default": "Padrão", + "manage": "Gerenciar catálogos de endereços" }, "detail": { "emails": "Endereços de e-mail", diff --git a/locales/ru/common.json b/locales/ru/common.json index f9f776e3..34e082e8 100644 --- a/locales/ru/common.json +++ b/locales/ru/common.json @@ -1230,7 +1230,13 @@ "import_label": "Импорт контактов", "import_description": "Импортировать контакты из файла vCard (.vcf)", "export_label": "Экспорт контактов", - "export_description": "Экспортировать все контакты в виде файла vCard (.vcf)" + "export_description": "Экспортировать все контакты в виде файла vCard (.vcf)", + "manage_title": "Адресные книги", + "manage_description": "Переименуйте ваши адресные книги", + "no_address_books": "Адресные книги не найдены", + "categories_title": "Категории", + "categories_description": "Переименование категорий контактов", + "no_categories": "Категории не найдены" }, "filters": { "title": "Фильтры почты", @@ -1635,6 +1641,10 @@ "search_placeholder": "Поиск контактов...", "create_new": "Новый контакт", "no_category": "Без категории", + "rename_category": "Переименовать категорию", + "category_name_label": "Название категории", + "category_renamed": "Категория переименована", + "category_rename_failed": "Не удалось переименовать категорию", "category_added": "Контакт добавлен в {name}", "category_added_plural": "{count} контактов добавлено в {name}", "empty_state": "Контактов пока нет", @@ -1661,7 +1671,13 @@ "moved": "Контакт перемещён в {name}", "moved_plural": "{count} контактов перемещено в {name}", "move_failed": "Не удалось переместить контакт", - "address_book": "Адресная книга" + "address_book": "Адресная книга", + "rename": "Переименовать адресную книгу", + "name_label": "Название адресной книги", + "renamed": "Адресная книга переименована", + "rename_failed": "Не удалось переименовать адресную книгу", + "default": "По умолчанию", + "manage": "Управление адресными книгами" }, "detail": { "emails": "Адреса электронной почты", diff --git a/locales/zh/common.json b/locales/zh/common.json index 217cbf4f..2fd33cef 100644 --- a/locales/zh/common.json +++ b/locales/zh/common.json @@ -1230,7 +1230,13 @@ "import_label": "导入联系人", "import_description": "从 vCard (.vcf) 文件导入联系人", "export_label": "导出联系人", - "export_description": "将所有联系人导出为 vCard (.vcf) 文件" + "export_description": "将所有联系人导出为 vCard (.vcf) 文件", + "manage_title": "地址簿", + "manage_description": "重命名您的地址簿", + "no_address_books": "未找到地址簿", + "categories_title": "类别", + "categories_description": "重命名联系人类别", + "no_categories": "未找到类别" }, "filters": { "title": "邮件过滤器", @@ -1635,6 +1641,10 @@ "search_placeholder": "搜索联系人...", "create_new": "新联系人", "no_category": "没有类别", + "rename_category": "重命名类别", + "category_name_label": "类别名称", + "category_renamed": "类别已重命名", + "category_rename_failed": "重命名类别失败", "category_added": "联系人已添加至 {name}", "category_added_plural": "{count} 联系人已添加到 {name}", "empty_state": "还没有联系人", @@ -1661,7 +1671,13 @@ "moved": "联系人已移至 {name}", "moved_plural": "{count} 联系人已移至 {name}", "move_failed": "无法移动联系人", - "address_book": "地址簿" + "address_book": "地址簿", + "rename": "重命名地址簿", + "name_label": "地址簿名称", + "renamed": "地址簿已重命名", + "rename_failed": "重命名地址簿失败", + "default": "默认", + "manage": "管理地址簿" }, "detail": { "emails": "邮箱地址", diff --git a/stores/contact-store.ts b/stores/contact-store.ts index 9ea3ac78..d516e621 100644 --- a/stores/contact-store.ts +++ b/stores/contact-store.ts @@ -82,6 +82,8 @@ interface ContactStore { bulkDeleteContacts: (client: IJMAPClient | null, ids: string[]) => Promise; bulkAddToGroup: (client: IJMAPClient | null, groupId: string, contactIds: string[]) => Promise; moveContactToAddressBook: (client: IJMAPClient, contactIds: string[], addressBook: AddressBook) => Promise; + renameAddressBook: (client: IJMAPClient, addressBook: AddressBook, newName: string) => Promise; + renameKeyword: (client: IJMAPClient | null, oldKeyword: string, newKeyword: string) => Promise; importContacts: (client: IJMAPClient | null, contacts: ContactCard[]) => Promise; } @@ -605,6 +607,57 @@ export const useContactStore = create()( } }, + renameAddressBook: async (client, addressBook, newName) => { + set({ error: null }); + const trimmed = newName.trim(); + if (!trimmed) return; + try { + const originalId = addressBook.originalId || addressBook.id; + const accountId = addressBook.isShared ? addressBook.accountId : undefined; + await client.updateAddressBook(originalId, { name: trimmed }, accountId); + set((state) => ({ + addressBooks: state.addressBooks.map(b => + b.id === addressBook.id ? { ...b, name: trimmed } : b + ), + })); + } catch (error) { + const msg = error instanceof Error ? error.message : 'Failed to rename address book'; + set({ error: msg }); + throw error; + } + }, + + renameKeyword: async (client, oldKeyword, newKeyword) => { + set({ error: null }); + const oldKw = oldKeyword.trim(); + const newKw = newKeyword.trim(); + if (!oldKw || !newKw || oldKw === newKw) return; + + const { contacts, supportsSync } = get(); + const affected = contacts.filter(c => c.keywords?.[oldKw]); + + for (const contact of affected) { + const { [oldKw]: _old, ...rest } = contact.keywords || {}; + const updatedKeywords: Record = { ...rest, [newKw]: true }; + try { + if (supportsSync && client) { + const originalId = contact.originalId || contact.id; + const accountId = contact.isShared ? contact.accountId : undefined; + await client.updateContact(originalId, { keywords: updatedKeywords }, accountId); + } + set((state) => ({ + contacts: state.contacts.map(c => + c.id === contact.id ? { ...c, keywords: updatedKeywords } : c + ), + })); + } catch (error) { + const msg = error instanceof Error ? error.message : 'Failed to rename category'; + set({ error: msg }); + throw error; + } + } + }, + importContacts: async (client, contacts) => { const { supportsSync } = get(); let imported = 0;