feat: add "New address book" creation UI #415

This commit is contained in:
Linus Rath
2026-06-24 19:53:41 +02:00
parent f285a2bd64
commit de68d68fb7
24 changed files with 171 additions and 3 deletions
+40
View File
@@ -84,6 +84,7 @@ export default function ContactsPage() {
bulkDeleteContacts, bulkDeleteContacts,
bulkAddToGroup, bulkAddToGroup,
moveContactToAddressBook, moveContactToAddressBook,
createAddressBook,
renameAddressBook, renameAddressBook,
removeAddressBook, removeAddressBook,
shareAddressBook, shareAddressBook,
@@ -95,6 +96,7 @@ export default function ContactsPage() {
const [activeCategory, setActiveCategory] = useState<ContactCategory>("all"); const [activeCategory, setActiveCategory] = useState<ContactCategory>("all");
const [showImportDialog, setShowImportDialog] = useState(false); const [showImportDialog, setShowImportDialog] = useState(false);
const [renamingAddressBook, setRenamingAddressBook] = useState<AddressBook | null>(null); const [renamingAddressBook, setRenamingAddressBook] = useState<AddressBook | null>(null);
const [creatingAddressBook, setCreatingAddressBook] = useState(false);
const [sharingAddressBookId, setSharingAddressBookId] = useState<string | null>(null); const [sharingAddressBookId, setSharingAddressBookId] = useState<string | null>(null);
const [defaultBookIdForCreate, setDefaultBookIdForCreate] = useState<string | undefined>(undefined); const [defaultBookIdForCreate, setDefaultBookIdForCreate] = useState<string | undefined>(undefined);
const [createPrefill, setCreatePrefill] = useState<{ email?: string; name?: string } | undefined>(undefined); const [createPrefill, setCreatePrefill] = useState<{ email?: string; name?: string } | undefined>(undefined);
@@ -300,6 +302,34 @@ export default function ContactsPage() {
} }
}, [client, supportsSync, contacts, updateContact, updateLocalContact, t]); }, [client, supportsSync, contacts, updateContact, updateLocalContact, t]);
// Refresh address books (and contacts) after a structural change, staying
// multi-account aware so a freshly created book lands in the sidebar.
const refreshAddressBooks = useCallback(async () => {
if (!client) return;
if (multiAccountEnabled && accountClients.length > 0) {
const activeId = useAuthStore.getState().activeAccountId;
if (activeId) {
const { fetchAllAccountsAddressBooks } = useContactStore.getState();
await fetchAllAccountsAddressBooks(accountClients, activeId);
return;
}
}
await useContactStore.getState().fetchAddressBooks(client);
}, [client, multiAccountEnabled, accountClients]);
const handleCreateAddressBook = useCallback(async (name: string) => {
if (!client) return;
try {
await createAddressBook(client, name);
await refreshAddressBooks();
toast.success(t("address_books.created"));
setCreatingAddressBook(false);
} catch (error) {
console.error('Failed to create address book:', error);
toast.error(t("address_books.create_failed"));
}
}, [client, createAddressBook, refreshAddressBooks, t]);
const handleImportContacts = useCallback(async (importedContacts: ContactCard[]) => { const handleImportContacts = useCallback(async (importedContacts: ContactCard[]) => {
return importContacts( return importContacts(
supportsSync && client ? client : null, supportsSync && client ? client : null,
@@ -857,6 +887,7 @@ export default function ContactsPage() {
onSelectCategory={handleSelectCategory} onSelectCategory={handleSelectCategory}
onCreateGroup={handleCreateGroup} onCreateGroup={handleCreateGroup}
onCreateContact={handleCreateNew} onCreateContact={handleCreateNew}
onCreateAddressBook={client ? () => setCreatingAddressBook(true) : undefined}
onImport={() => setShowImportDialog(true)} onImport={() => setShowImportDialog(true)}
onEditGroup={handleEditGroupFromSidebar} onEditGroup={handleEditGroupFromSidebar}
onDeleteGroup={handleDeleteGroupFromSidebar} onDeleteGroup={handleDeleteGroupFromSidebar}
@@ -1006,6 +1037,15 @@ export default function ContactsPage() {
}} }}
/> />
)} )}
{creatingAddressBook && (
<RenameDialog
currentName=""
title={t("address_books.create")}
label={t("address_books.name_label")}
onCancel={() => setCreatingAddressBook(false)}
onConfirm={handleCreateAddressBook}
/>
)}
{renamingAddressBook && ( {renamingAddressBook && (
<RenameDialog <RenameDialog
currentName={renamingAddressBook.name} currentName={renamingAddressBook.name}
+12 -1
View File
@@ -2,7 +2,7 @@
import { useMemo, useState, useCallback, useEffect, useRef, type DragEvent } from "react"; import { useMemo, useState, useCallback, useEffect, useRef, type DragEvent } from "react";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { BookUser, User, Users, Plus, Share2, Book, 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 } from "lucide-react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { ContextMenu, ContextMenuItem, ContextMenuSeparator, ContextMenuSubMenu } from "@/components/ui/context-menu"; import { ContextMenu, ContextMenuItem, ContextMenuSeparator, ContextMenuSubMenu } from "@/components/ui/context-menu";
@@ -22,6 +22,7 @@ interface ContactsSidebarProps {
onSelectCategory: (category: ContactCategory) => void; onSelectCategory: (category: ContactCategory) => void;
onCreateGroup: () => void; onCreateGroup: () => void;
onCreateContact: () => void; onCreateContact: () => void;
onCreateAddressBook?: () => void;
onImport?: () => void; onImport?: () => void;
onEditGroup?: (groupId: string) => void; onEditGroup?: (groupId: string) => void;
onDeleteGroup?: (groupId: string) => void; onDeleteGroup?: (groupId: string) => void;
@@ -91,6 +92,7 @@ export function ContactsSidebar({
onSelectCategory, onSelectCategory,
onCreateGroup, onCreateGroup,
onCreateContact, onCreateContact,
onCreateAddressBook,
onImport, onImport,
onEditGroup, onEditGroup,
onDeleteGroup, onDeleteGroup,
@@ -298,6 +300,15 @@ export function ContactsSidebar({
<UsersRound className="w-4 h-4" /> <UsersRound className="w-4 h-4" />
{t("groups.create")} {t("groups.create")}
</button> </button>
{onCreateAddressBook && (
<button
className="w-full flex items-center gap-2 px-3 py-1.5 text-sm hover:bg-accent transition-colors text-left"
onClick={() => { setShowMenu(false); onCreateAddressBook(); }}
>
<BookPlus className="w-4 h-4" />
{t("address_books.create")}
</button>
)}
{onImport && ( {onImport && (
<button <button
className="w-full flex items-center gap-2 px-3 py-1.5 text-sm hover:bg-accent transition-colors text-left" className="w-full flex items-center gap-2 px-3 py-1.5 text-sm hover:bg-accent transition-colors text-left"
@@ -2,7 +2,7 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { Book, Pencil, Share2, Tag, Users } from "lucide-react"; import { Book, BookPlus, Pencil, Share2, Tag, Users } from "lucide-react";
import { useContactStore } from "@/stores/contact-store"; import { useContactStore } from "@/stores/contact-store";
import { useAuthStore } from "@/stores/auth-store"; import { useAuthStore } from "@/stores/auth-store";
import { useManagedAccountStore } from "@/stores/managed-account-store"; import { useManagedAccountStore } from "@/stores/managed-account-store";
@@ -11,6 +11,7 @@ import { SettingsSection } from "./settings-section";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import type { AddressBook, AddressBookRights } from "@/lib/jmap/types"; import type { AddressBook, AddressBookRights } from "@/lib/jmap/types";
import { ShareCollectionDialog } from "./share-collection-dialog"; import { ShareCollectionDialog } from "./share-collection-dialog";
import { RenameDialog } from "@/components/files/rename-dialog";
function AddressBookEditRow({ function AddressBookEditRow({
initial, initial,
@@ -73,10 +74,11 @@ export function AddressBookManagementSettings() {
const tSettings = useTranslations("settings.contacts"); const tSettings = useTranslations("settings.contacts");
const { client } = useAuthStore(); const { client } = useAuthStore();
const managedAccountId = useManagedAccountStore((s) => s.managedAccountId); const managedAccountId = useManagedAccountStore((s) => s.managedAccountId);
const { addressBooks, contacts, supportsSync, fetchAddressBooks, renameAddressBook, shareAddressBook, renameKeyword } = useContactStore(); const { addressBooks, contacts, supportsSync, fetchAddressBooks, createAddressBook, renameAddressBook, shareAddressBook, renameKeyword } = useContactStore();
const [editingId, setEditingId] = useState<string | null>(null); const [editingId, setEditingId] = useState<string | null>(null);
const [editingKeyword, setEditingKeyword] = useState<string | null>(null); const [editingKeyword, setEditingKeyword] = useState<string | null>(null);
const [sharingId, setSharingId] = useState<string | null>(null); const [sharingId, setSharingId] = useState<string | null>(null);
const [creating, setCreating] = useState(false);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
useEffect(() => { useEffect(() => {
@@ -85,6 +87,21 @@ export function AddressBookManagementSettings() {
} }
}, [client, addressBooks.length, fetchAddressBooks]); }, [client, addressBooks.length, fetchAddressBooks]);
const handleCreate = async (name: string) => {
if (!client) return;
setIsLoading(true);
try {
await createAddressBook(client, name);
await fetchAddressBooks(client);
setCreating(false);
toast.success(t("created"));
} catch {
toast.error(t("create_failed"));
} finally {
setIsLoading(false);
}
};
const handleUpdate = async (book: AddressBook, newName: string) => { const handleUpdate = async (book: AddressBook, newName: string) => {
if (!client) return; if (!client) return;
setIsLoading(true); setIsLoading(true);
@@ -226,6 +243,19 @@ export function AddressBookManagementSettings() {
{addressBooks.length === 0 && ( {addressBooks.length === 0 && (
<p className="text-sm text-muted-foreground py-2">{tSettings("no_address_books")}</p> <p className="text-sm text-muted-foreground py-2">{tSettings("no_address_books")}</p>
)} )}
{/* Creating targets the user's own account, so hide it while scoped to a
managed (shared) account. */}
{!managedAccountId && client && (
<button
type="button"
onClick={() => setCreating(true)}
className="flex items-center gap-2 py-2.5 px-3 w-full rounded-md border border-dashed border-border text-sm text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
>
<BookPlus className="w-4 h-4 flex-shrink-0" />
{t("create")}
</button>
)}
</div> </div>
</SettingsSection> </SettingsSection>
@@ -278,6 +308,16 @@ export function AddressBookManagementSettings() {
</div> </div>
)} )}
{creating && (
<RenameDialog
currentName=""
title={t("create")}
label={t("name_label")}
onCancel={() => setCreating(false)}
onConfirm={handleCreate}
/>
)}
{sharingId && client && (() => { {sharingId && client && (() => {
const book = addressBooks.find((b) => b.id === sharingId); const book = addressBooks.find((b) => b.id === sharingId);
if (!book) return null; if (!book) return null;
+3
View File
@@ -2172,6 +2172,9 @@
"title": "Sdílené" "title": "Sdílené"
}, },
"address_books": { "address_books": {
"create": "Nový adresář",
"created": "Adresář vytvořen",
"create_failed": "Nepodařilo se vytvořit adresář",
"title": "Moje adresáře", "title": "Moje adresáře",
"shared_prefix": "Sdílené: {name}", "shared_prefix": "Sdílené: {name}",
"moved": "Kontakt přesunut do {name}", "moved": "Kontakt přesunut do {name}",
+3
View File
@@ -2172,6 +2172,9 @@
"title": "Delt" "title": "Delt"
}, },
"address_books": { "address_books": {
"create": "Ny adressebog",
"created": "Adressebog oprettet",
"create_failed": "Kunne ikke oprette adressebog",
"title": "Mine adressebøger", "title": "Mine adressebøger",
"shared_prefix": "Delt: {name}", "shared_prefix": "Delt: {name}",
"moved": "Kontakt flyttet til {name}", "moved": "Kontakt flyttet til {name}",
+3
View File
@@ -2172,6 +2172,9 @@
"title": "Geteilt" "title": "Geteilt"
}, },
"address_books": { "address_books": {
"create": "Neues Adressbuch",
"created": "Adressbuch erstellt",
"create_failed": "Adressbuch konnte nicht erstellt werden",
"title": "Meine Adressbücher", "title": "Meine Adressbücher",
"shared_prefix": "Geteilt: {name}", "shared_prefix": "Geteilt: {name}",
"moved": "Kontakt verschoben nach {name}", "moved": "Kontakt verschoben nach {name}",
+3
View File
@@ -2173,6 +2173,9 @@
"title": "Shared" "title": "Shared"
}, },
"address_books": { "address_books": {
"create": "New address book",
"created": "Address book created",
"create_failed": "Failed to create address book",
"title": "My Address Books", "title": "My Address Books",
"shared_prefix": "Shared: {name}", "shared_prefix": "Shared: {name}",
"moved": "Contact moved to {name}", "moved": "Contact moved to {name}",
+3
View File
@@ -2172,6 +2172,9 @@
"title": "Compartidos" "title": "Compartidos"
}, },
"address_books": { "address_books": {
"create": "Nueva libreta de direcciones",
"created": "Libreta de direcciones creada",
"create_failed": "No se pudo crear la libreta de direcciones",
"title": "Mis Libretas de Direcciones", "title": "Mis Libretas de Direcciones",
"shared_prefix": "Compartido: {name}", "shared_prefix": "Compartido: {name}",
"moved": "Contacto movido a {name}", "moved": "Contacto movido a {name}",
+3
View File
@@ -2173,6 +2173,9 @@
"title": "اشتراکی" "title": "اشتراکی"
}, },
"address_books": { "address_books": {
"create": "دفترچه آدرس جدید",
"created": "دفترچه آدرس ایجاد شد",
"create_failed": "ایجاد دفترچه آدرس ناموفق بود",
"title": "دفترچه‌های آدرس من", "title": "دفترچه‌های آدرس من",
"shared_prefix": "اشتراکی: {name}", "shared_prefix": "اشتراکی: {name}",
"moved": "مخاطب به {name} منتقل شد", "moved": "مخاطب به {name} منتقل شد",
+3
View File
@@ -2172,6 +2172,9 @@
"title": "Partagés" "title": "Partagés"
}, },
"address_books": { "address_books": {
"create": "Nouveau carnet d'adresses",
"created": "Carnet d'adresses créé",
"create_failed": "Échec de la création du carnet d'adresses",
"title": "Mes Carnets d'adresses", "title": "Mes Carnets d'adresses",
"shared_prefix": "Partagé : {name}", "shared_prefix": "Partagé : {name}",
"moved": "Contact déplacé vers {name}", "moved": "Contact déplacé vers {name}",
+3
View File
@@ -2173,6 +2173,9 @@
"title": "Megosztott" "title": "Megosztott"
}, },
"address_books": { "address_books": {
"create": "Új címjegyzék",
"created": "Címjegyzék létrehozva",
"create_failed": "Nem sikerült létrehozni a címjegyzéket",
"title": "Címjegyzékeim", "title": "Címjegyzékeim",
"shared_prefix": "Megosztott: {name}", "shared_prefix": "Megosztott: {name}",
"moved": "Névjegy áthelyezve ide: {name}", "moved": "Névjegy áthelyezve ide: {name}",
+3
View File
@@ -2172,6 +2172,9 @@
"title": "Condivisi" "title": "Condivisi"
}, },
"address_books": { "address_books": {
"create": "Nuova rubrica",
"created": "Rubrica creata",
"create_failed": "Impossibile creare la rubrica",
"title": "Le mie Rubriche", "title": "Le mie Rubriche",
"shared_prefix": "Condiviso: {name}", "shared_prefix": "Condiviso: {name}",
"moved": "Contatto spostato in {name}", "moved": "Contatto spostato in {name}",
+3
View File
@@ -2172,6 +2172,9 @@
"title": "共有" "title": "共有"
}, },
"address_books": { "address_books": {
"create": "新しいアドレス帳",
"created": "アドレス帳を作成しました",
"create_failed": "アドレス帳の作成に失敗しました",
"title": "マイアドレス帳", "title": "マイアドレス帳",
"shared_prefix": "共有: {name}", "shared_prefix": "共有: {name}",
"moved": "連絡先を {name} に移動しました", "moved": "連絡先を {name} に移動しました",
+3
View File
@@ -2172,6 +2172,9 @@
"title": "공유됨" "title": "공유됨"
}, },
"address_books": { "address_books": {
"create": "새 주소록",
"created": "주소록이 생성되었습니다",
"create_failed": "주소록을 만들지 못했습니다",
"title": "내 주소록", "title": "내 주소록",
"shared_prefix": "공유됨: {name}", "shared_prefix": "공유됨: {name}",
"moved": "연락처가 {name}(으)로 이동되었어요", "moved": "연락처가 {name}(으)로 이동되었어요",
+3
View File
@@ -2168,6 +2168,9 @@
"title": "Koplietotie" "title": "Koplietotie"
}, },
"address_books": { "address_books": {
"create": "Jauna adrešu grāmata",
"created": "Adrešu grāmata izveidota",
"create_failed": "Neizdevās izveidot adrešu grāmatu",
"title": "Manas adrešu grāmatas", "title": "Manas adrešu grāmatas",
"shared_prefix": "Koplietotā: {name}", "shared_prefix": "Koplietotā: {name}",
"moved": "Kontakts pārvietots uz {name}", "moved": "Kontakts pārvietots uz {name}",
+3
View File
@@ -2172,6 +2172,9 @@
"title": "Gedeeld" "title": "Gedeeld"
}, },
"address_books": { "address_books": {
"create": "Nieuw adresboek",
"created": "Adresboek aangemaakt",
"create_failed": "Kan adresboek niet aanmaken",
"title": "Mijn Adresboeken", "title": "Mijn Adresboeken",
"shared_prefix": "Gedeeld: {name}", "shared_prefix": "Gedeeld: {name}",
"moved": "Contact verplaatst naar {name}", "moved": "Contact verplaatst naar {name}",
+3
View File
@@ -2172,6 +2172,9 @@
"title": "Udostępnione" "title": "Udostępnione"
}, },
"address_books": { "address_books": {
"create": "Nowa książka adresowa",
"created": "Utworzono książkę adresową",
"create_failed": "Nie udało się utworzyć książki adresowej",
"title": "Moje książki adresowe", "title": "Moje książki adresowe",
"shared_prefix": "Udostępnione: {name}", "shared_prefix": "Udostępnione: {name}",
"moved": "Kontakt przeniesiono do {name}", "moved": "Kontakt przeniesiono do {name}",
+3
View File
@@ -2172,6 +2172,9 @@
"title": "Compartilhados" "title": "Compartilhados"
}, },
"address_books": { "address_books": {
"create": "Novo livro de endereços",
"created": "Livro de endereços criado",
"create_failed": "Falha ao criar o livro de endereços",
"title": "Meus Catálogos de Endereços", "title": "Meus Catálogos de Endereços",
"shared_prefix": "Compartilhado: {name}", "shared_prefix": "Compartilhado: {name}",
"moved": "Contato movido para {name}", "moved": "Contato movido para {name}",
+3
View File
@@ -2173,6 +2173,9 @@
"title": "Partajat" "title": "Partajat"
}, },
"address_books": { "address_books": {
"create": "Agendă nouă",
"created": "Agendă creată",
"create_failed": "Nu s-a putut crea agenda",
"title": "Agendele mele de adrese", "title": "Agendele mele de adrese",
"shared_prefix": "Partajat: {name}", "shared_prefix": "Partajat: {name}",
"moved": "Contactul a fost mutat la {name}", "moved": "Contactul a fost mutat la {name}",
+3
View File
@@ -2172,6 +2172,9 @@
"title": "Общий" "title": "Общий"
}, },
"address_books": { "address_books": {
"create": "Новая адресная книга",
"created": "Адресная книга создана",
"create_failed": "Не удалось создать адресную книгу",
"title": "Мои адресные книги", "title": "Мои адресные книги",
"shared_prefix": "Общая: {name}", "shared_prefix": "Общая: {name}",
"moved": "Контакт перемещён в {name}", "moved": "Контакт перемещён в {name}",
+3
View File
@@ -2172,6 +2172,9 @@
"title": "Paylaşılan" "title": "Paylaşılan"
}, },
"address_books": { "address_books": {
"create": "Yeni adres defteri",
"created": "Adres defteri oluşturuldu",
"create_failed": "Adres defteri oluşturulamadı",
"title": "Adres Defterlerim", "title": "Adres Defterlerim",
"shared_prefix": "Paylaşılan: {name}", "shared_prefix": "Paylaşılan: {name}",
"moved": "Kişi {name} konumuna taşındı", "moved": "Kişi {name} konumuna taşındı",
+3
View File
@@ -2172,6 +2172,9 @@
"title": "Спільний доступ" "title": "Спільний доступ"
}, },
"address_books": { "address_books": {
"create": "Нова адресна книга",
"created": "Адресну книгу створено",
"create_failed": "Не вдалося створити адресну книгу",
"title": "Мої адресні книги", "title": "Мої адресні книги",
"shared_prefix": "Спільно: {name}", "shared_prefix": "Спільно: {name}",
"moved": "Контакт переміщено до {name}", "moved": "Контакт переміщено до {name}",
+3
View File
@@ -2172,6 +2172,9 @@
"title": "共享" "title": "共享"
}, },
"address_books": { "address_books": {
"create": "新建通讯录",
"created": "通讯录已创建",
"create_failed": "创建通讯录失败",
"title": "我的地址簿", "title": "我的地址簿",
"shared_prefix": "共享:{name}", "shared_prefix": "共享:{name}",
"moved": "联系人已移至 {name}", "moved": "联系人已移至 {name}",
+17
View File
@@ -201,6 +201,7 @@ interface ContactStore {
bulkDeleteContacts: (client: IJMAPClient | null, ids: string[]) => Promise<void>; bulkDeleteContacts: (client: IJMAPClient | null, ids: string[]) => Promise<void>;
bulkAddToGroup: (client: IJMAPClient | null, groupId: string, contactIds: string[]) => Promise<void>; bulkAddToGroup: (client: IJMAPClient | null, groupId: string, contactIds: string[]) => Promise<void>;
moveContactToAddressBook: (client: IJMAPClient, contactIds: string[], addressBook: AddressBook) => Promise<void>; moveContactToAddressBook: (client: IJMAPClient, contactIds: string[], addressBook: AddressBook) => Promise<void>;
createAddressBook: (client: IJMAPClient, name: string) => Promise<AddressBook>;
renameAddressBook: (client: IJMAPClient, addressBook: AddressBook, newName: string) => Promise<void>; renameAddressBook: (client: IJMAPClient, addressBook: AddressBook, newName: string) => Promise<void>;
removeAddressBook: (client: IJMAPClient, addressBook: AddressBook) => Promise<void>; removeAddressBook: (client: IJMAPClient, addressBook: AddressBook) => Promise<void>;
shareAddressBook: (client: IJMAPClient, addressBook: AddressBook, principalId: string, rights: AddressBookRights | null) => Promise<void>; shareAddressBook: (client: IJMAPClient, addressBook: AddressBook, principalId: string, rights: AddressBookRights | null) => Promise<void>;
@@ -849,6 +850,22 @@ export const useContactStore = create<ContactStore>()(
} }
}, },
createAddressBook: async (client, name) => {
set({ error: null });
const trimmed = name.trim();
if (!trimmed) throw new Error('Address book name is required');
try {
// New books always belong to the active account; the caller refreshes
// the list afterwards (single- or multi-account aware) so the freshly
// created book lands in state with its full server-set properties.
return await client.createAddressBook(trimmed);
} catch (error) {
const msg = error instanceof Error ? error.message : 'Failed to create address book';
set({ error: msg });
throw error;
}
},
renameAddressBook: async (client, addressBook, newName) => { renameAddressBook: async (client, addressBook, newName) => {
set({ error: null }); set({ error: null });
const trimmed = newName.trim(); const trimmed = newName.trim();