From 6cd05da510e0364d22cfd8edf3bcc5126e27b0d5 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Wed, 4 Nov 2026 10:55:22 +0100 Subject: [PATCH 1/6] fix: call a voice number Phone and a private entry Home on contacts The contact-detail test now expects the new label key for private, since the wording moved to the ux_pim_contact namespace. --- .../__tests__/contact-detail.test.tsx | 2 +- .../__tests__/contacts-ux-pim.test.tsx | 45 +++++++++++++++++++ components/contacts/contact-detail.tsx | 13 +++--- components/contacts/contact-form.tsx | 9 ++-- locales/ar/common.json | 4 ++ locales/ca/common.json | 4 ++ locales/cs/common.json | 4 ++ locales/da/common.json | 4 ++ locales/de/common.json | 4 ++ locales/en/common.json | 4 ++ locales/es/common.json | 4 ++ locales/et/common.json | 4 ++ locales/fa/common.json | 4 ++ locales/fr/common.json | 4 ++ locales/he/common.json | 4 ++ locales/hu/common.json | 4 ++ locales/it/common.json | 4 ++ locales/ja/common.json | 4 ++ locales/ko/common.json | 4 ++ locales/lv/common.json | 4 ++ locales/mn/common.json | 4 ++ locales/nb/common.json | 4 ++ locales/nl/common.json | 4 ++ locales/pl/common.json | 4 ++ locales/pt/common.json | 4 ++ locales/ro/common.json | 4 ++ locales/ru/common.json | 4 ++ locales/sk/common.json | 4 ++ locales/th/common.json | 4 ++ locales/tr/common.json | 4 ++ locales/uk/common.json | 4 ++ locales/vi/common.json | 4 ++ locales/zh-TW/common.json | 4 ++ locales/zh/common.json | 4 ++ 34 files changed, 179 insertions(+), 10 deletions(-) create mode 100644 components/contacts/__tests__/contacts-ux-pim.test.tsx diff --git a/components/contacts/__tests__/contact-detail.test.tsx b/components/contacts/__tests__/contact-detail.test.tsx index a06d4e1c3..d41b1ac3f 100644 --- a/components/contacts/__tests__/contact-detail.test.tsx +++ b/components/contacts/__tests__/contact-detail.test.tsx @@ -68,7 +68,7 @@ describe('ContactDetail', () => { render(); // The test translator returns the key, so a translated label shows up as its key. expect(screen.getByText('form.context_work')).toBeInTheDocument(); - expect(screen.getByText('form.context_private · form.phone_cell, x-sat')).toBeInTheDocument(); + expect(screen.getByText('ux_pim_contact.context_private · form.phone_cell, x-sat')).toBeInTheDocument(); // Custom vCard types are shown as stored. expect(screen.getByText('form.context_work, x-hq')).toBeInTheDocument(); }); diff --git a/components/contacts/__tests__/contacts-ux-pim.test.tsx b/components/contacts/__tests__/contacts-ux-pim.test.tsx new file mode 100644 index 000000000..9ff7af07b --- /dev/null +++ b/components/contacts/__tests__/contacts-ux-pim.test.tsx @@ -0,0 +1,45 @@ +import { render, screen } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; +import type { ContactCard } from '@/lib/jmap/types'; + +// Keys with their namespace, so a label read from ux_pim_contact can be told +// from the old one in contacts.form. +vi.mock('next-intl', () => ({ + useTranslations: (ns?: string) => (key: string) => (ns ? `${ns}.${key}` : key), + useLocale: () => 'en', + useFormatter: () => ({ dateTime: String, relativeTime: String, number: String }), +})); +vi.mock('@/stores/account-store', () => { + const state = { accounts: [], activeAccountId: null }; + const hook = (sel?: (s: typeof state) => unknown) => (typeof sel === 'function' ? sel(state) : state); + hook.getState = () => state; + return { useAccountStore: hook }; +}); +vi.mock('@/components/contacts/contact-activity', () => ({ ContactActivity: () => null })); + +import { ContactForm } from '../contact-form'; +import { ContactDetail } from '../contact-detail'; + +const anna: ContactCard = { + id: 'c1', + addressBookIds: {}, + name: { components: [{ kind: 'given', value: 'Anna' }, { kind: 'surname', value: 'Example' }], isOrdered: true }, + emails: { e0: { address: 'anna@example.net', contexts: { private: true } } }, + phones: { p0: { number: '+49 30 1', features: { voice: true }, contexts: { private: true } } }, +}; + +describe('pim-19: standard words read as people say them', () => { + it('offers "Phone" and "Home" in the editor, storing voice and private as before', () => { + render(); + const phoneType = screen.getByDisplayValue('ux_pim_contact.phone_voice') as HTMLSelectElement; + expect(phoneType.value).toBe('voice'); + expect(screen.getAllByDisplayValue('ux_pim_contact.context_private').map(s => (s as HTMLSelectElement).value)).toEqual(['private', 'private']); + expect(screen.queryByText('contacts.form.phone_voice')).toBeNull(); + expect(screen.queryByText('contacts.form.context_private')).toBeNull(); + }); + + it('labels a voice phone "Phone" and a private one "Home" on the contact page', () => { + render(); + expect(screen.getByText('ux_pim_contact.context_private · ux_pim_contact.phone_voice')).toBeInTheDocument(); + }); +}); diff --git a/components/contacts/contact-detail.tsx b/components/contacts/contact-detail.tsx index 66a3969e2..addc2d069 100644 --- a/components/contacts/contact-detail.tsx +++ b/components/contacts/contact-detail.tsx @@ -40,14 +40,15 @@ interface ContactDetailProps { } // Contexts and phone features the contact form offers, shown with the same -// labels here. Anything else (custom vCard types) is shown as stored. +// labels here. Anything else (custom vCard types) is shown as stored. The +// standard's "private" reads "Home" and "voice" reads "Phone" (#pim-19). const CONTEXT_LABEL_KEYS: Record = { work: "form.context_work", - private: "form.context_private", + private: "ux_pim_contact.context_private", }; const PHONE_FEATURE_LABEL_KEYS: Record = { - voice: "form.phone_voice", + voice: "ux_pim_contact.phone_voice", cell: "form.phone_cell", mobile: "form.phone_cell", fax: "form.phone_fax", @@ -78,12 +79,14 @@ function getCompletedYears(dateInput: AnniversaryDate): number | null { export function ContactDetail({ contact, onEdit, onDelete, onAddToGroup, onDuplicate, onMoveTo, onCompose, onPrint, isMobile, className, readOnly = false }: ContactDetailProps) { const t = useTranslations("contacts"); + const tRoot = useTranslations(); const tCommon = useTranslations("common"); const locale = useLocale(); // The ⋮ shows the contact's menu, the one right-click shows in the list (R6). const moreMenu = useContextMenu(); - const contextLabel = (key: string) => (CONTEXT_LABEL_KEYS[key] ? t(CONTEXT_LABEL_KEYS[key]) : key); - const featureLabel = (key: string) => (PHONE_FEATURE_LABEL_KEYS[key] ? t(PHONE_FEATURE_LABEL_KEYS[key]) : key); + const label = (labelKey: string) => (labelKey.startsWith("ux_pim_contact.") ? tRoot(labelKey) : t(labelKey)); + const contextLabel = (key: string) => (CONTEXT_LABEL_KEYS[key] ? label(CONTEXT_LABEL_KEYS[key]) : key); + const featureLabel = (key: string) => (PHONE_FEATURE_LABEL_KEYS[key] ? label(PHONE_FEATURE_LABEL_KEYS[key]) : key); const cryptoKeys = contact?.cryptoKeys ? Object.values(contact.cryptoKeys) : []; const hasCryptoKeysSlotOffers = usePluginSlotOffers('contact-cryptokeys').length > 0; diff --git a/components/contacts/contact-form.tsx b/components/contacts/contact-form.tsx index fac3205b0..4e43f313c 100644 --- a/components/contacts/contact-form.tsx +++ b/components/contacts/contact-form.tsx @@ -220,6 +220,7 @@ function Select({ value, onChange, children, className, id, "aria-label": ariaLa export function ContactForm({ contact, addressBooks, allKeywords, defaultAddressBookId, prefill, onSave, onCancel, loadCurrentMedia, onDirtyChange }: ContactFormProps) { const t = useTranslations("contacts.form"); + const tx = useTranslations("ux_pim_contact"); const isEditing = !!contact; // The header's close goes to its start when the window's own close is at its end. const [header, setHeader] = useState(null); @@ -916,7 +917,7 @@ export function ContactForm({ contact, addressBooks, allKeywords, defaultAddress > - + {emails.length > 1 && ( setEmails(emails.filter((_, j) => j !== i))} className="shrink-0"> @@ -966,7 +967,7 @@ export function ContactForm({ contact, addressBooks, allKeywords, defaultAddress aria-label={t("phone_type")} > - + @@ -984,7 +985,7 @@ export function ContactForm({ contact, addressBooks, allKeywords, defaultAddress > - + setPhones(phones.filter((_, j) => j !== i))} className="shrink-0"> @@ -1046,7 +1047,7 @@ export function ContactForm({ contact, addressBooks, allKeywords, defaultAddress > - + diff --git a/locales/ar/common.json b/locales/ar/common.json index f8800991e..adca5d49f 100644 --- a/locales/ar/common.json +++ b/locales/ar/common.json @@ -4483,6 +4483,10 @@ "menu": "فتح قائمة جهة الاتصال" } }, + "ux_pim_contact": { + "context_private": "المنزل", + "phone_voice": "هاتف" + }, "uxs_wording": { "more_options": "خيارات إضافية", "use_in_other_apps": "الاستخدام في تطبيقات أخرى", diff --git a/locales/ca/common.json b/locales/ca/common.json index 62c75d062..7968722c2 100644 --- a/locales/ca/common.json +++ b/locales/ca/common.json @@ -4483,6 +4483,10 @@ "menu": "Obre el menú del contacte" } }, + "ux_pim_contact": { + "context_private": "Casa", + "phone_voice": "Telèfon" + }, "uxs_wording": { "more_options": "Més opcions", "use_in_other_apps": "Fes-ho servir en altres aplicacions", diff --git a/locales/cs/common.json b/locales/cs/common.json index cd765731a..bfb5e43f7 100644 --- a/locales/cs/common.json +++ b/locales/cs/common.json @@ -4483,6 +4483,10 @@ "open_categories": "Otevřít kategorie", "row_actions": "Akce pro {name}" }, + "ux_pim_contact": { + "context_private": "Domácí", + "phone_voice": "Telefon" + }, "uxs_wording": { "more_options": "Další možnosti", "use_in_other_apps": "Použít v jiných aplikacích", diff --git a/locales/da/common.json b/locales/da/common.json index 68c3ace47..bbc9d59c5 100644 --- a/locales/da/common.json +++ b/locales/da/common.json @@ -4483,6 +4483,10 @@ "open_categories": "Åbn kategorier", "row_actions": "Handlinger for {name}" }, + "ux_pim_contact": { + "context_private": "Hjem", + "phone_voice": "Telefon" + }, "uxs_wording": { "more_options": "Flere indstillinger", "use_in_other_apps": "Brug i andre apps", diff --git a/locales/de/common.json b/locales/de/common.json index 6acbe0328..7f16b73bd 100644 --- a/locales/de/common.json +++ b/locales/de/common.json @@ -4483,6 +4483,10 @@ "open_categories": "Kategorien öffnen", "row_actions": "Aktionen für {name}" }, + "ux_pim_contact": { + "context_private": "Privat", + "phone_voice": "Telefon" + }, "uxs_wording": { "more_options": "Weitere Optionen", "use_in_other_apps": "In anderen Apps nutzen", diff --git a/locales/en/common.json b/locales/en/common.json index 9fe28226c..d7a1ec5c8 100644 --- a/locales/en/common.json +++ b/locales/en/common.json @@ -4483,6 +4483,10 @@ "menu": "Open the contact's menu" } }, + "ux_pim_contact": { + "context_private": "Home", + "phone_voice": "Phone" + }, "uxs_wording": { "more_options": "More options", "use_in_other_apps": "Use in other apps", diff --git a/locales/es/common.json b/locales/es/common.json index e1f5a5ebe..7df639718 100644 --- a/locales/es/common.json +++ b/locales/es/common.json @@ -4483,6 +4483,10 @@ "open_categories": "Abrir categorías", "row_actions": "Acciones para {name}" }, + "ux_pim_contact": { + "context_private": "Casa", + "phone_voice": "Teléfono" + }, "uxs_wording": { "more_options": "Más opciones", "use_in_other_apps": "Usar en otras aplicaciones", diff --git a/locales/et/common.json b/locales/et/common.json index f77713d19..74e57c678 100644 --- a/locales/et/common.json +++ b/locales/et/common.json @@ -4483,6 +4483,10 @@ "menu": "Ava kontakti menüü" } }, + "ux_pim_contact": { + "context_private": "Kodu", + "phone_voice": "Telefon" + }, "uxs_wording": { "more_options": "Rohkem valikuid", "use_in_other_apps": "Kasuta teistes rakendustes", diff --git a/locales/fa/common.json b/locales/fa/common.json index 36f5febbd..7a7ad3f15 100644 --- a/locales/fa/common.json +++ b/locales/fa/common.json @@ -4483,6 +4483,10 @@ "menu": "باز کردن منوی مخاطب" } }, + "ux_pim_contact": { + "context_private": "منزل", + "phone_voice": "تلفن" + }, "uxs_wording": { "more_options": "گزینه‌های بیشتر", "use_in_other_apps": "استفاده در برنامه‌های دیگر", diff --git a/locales/fr/common.json b/locales/fr/common.json index 96ad6b352..78bb7548f 100644 --- a/locales/fr/common.json +++ b/locales/fr/common.json @@ -4483,6 +4483,10 @@ "open_categories": "Ouvrir les catégories", "row_actions": "Actions pour {name}" }, + "ux_pim_contact": { + "context_private": "Domicile", + "phone_voice": "Téléphone" + }, "uxs_wording": { "more_options": "Plus d'options", "use_in_other_apps": "Utiliser dans d'autres applications", diff --git a/locales/he/common.json b/locales/he/common.json index 8190e738d..9a35bf159 100644 --- a/locales/he/common.json +++ b/locales/he/common.json @@ -4376,6 +4376,10 @@ "menu": "פתח את התפריט של איש הקשר" } }, + "ux_pim_contact": { + "context_private": "בית", + "phone_voice": "טלפון" + }, "uxs_wording": { "more_options": "אפשרויות נוספות", "use_in_other_apps": "שימוש באפליקציות אחרות", diff --git a/locales/hu/common.json b/locales/hu/common.json index 9d7abd079..8fcd45664 100644 --- a/locales/hu/common.json +++ b/locales/hu/common.json @@ -4483,6 +4483,10 @@ "menu": "Névjegy menüjének megnyitása" } }, + "ux_pim_contact": { + "context_private": "Otthoni", + "phone_voice": "Telefon" + }, "uxs_wording": { "more_options": "További beállítások", "use_in_other_apps": "Használat más alkalmazásokban", diff --git a/locales/it/common.json b/locales/it/common.json index c230dd475..ba65e86bc 100644 --- a/locales/it/common.json +++ b/locales/it/common.json @@ -4483,6 +4483,10 @@ "open_categories": "Apri categorie", "row_actions": "Azioni per {name}" }, + "ux_pim_contact": { + "context_private": "Casa", + "phone_voice": "Telefono" + }, "uxs_wording": { "more_options": "Altre opzioni", "use_in_other_apps": "Usa in altre app", diff --git a/locales/ja/common.json b/locales/ja/common.json index 859b086d6..7413e6e99 100644 --- a/locales/ja/common.json +++ b/locales/ja/common.json @@ -4483,6 +4483,10 @@ "open_categories": "カテゴリを開く", "row_actions": "{name} の操作" }, + "ux_pim_contact": { + "context_private": "自宅", + "phone_voice": "電話" + }, "uxs_wording": { "more_options": "その他のオプション", "use_in_other_apps": "ほかのアプリで使う", diff --git a/locales/ko/common.json b/locales/ko/common.json index b26122750..038a04a55 100644 --- a/locales/ko/common.json +++ b/locales/ko/common.json @@ -4483,6 +4483,10 @@ "open_categories": "카테고리 열기", "row_actions": "{name} 작업" }, + "ux_pim_contact": { + "context_private": "집", + "phone_voice": "전화" + }, "uxs_wording": { "more_options": "옵션 더 보기", "use_in_other_apps": "다른 앱에서 사용", diff --git a/locales/lv/common.json b/locales/lv/common.json index 6474a45eb..df153778d 100644 --- a/locales/lv/common.json +++ b/locales/lv/common.json @@ -4483,6 +4483,10 @@ "open_categories": "Atvērt kategorijas", "row_actions": "Darbības: {name}" }, + "ux_pim_contact": { + "context_private": "Mājas", + "phone_voice": "Tālrunis" + }, "uxs_wording": { "more_options": "Vairāk iespēju", "use_in_other_apps": "Lietot citās lietotnēs", diff --git a/locales/mn/common.json b/locales/mn/common.json index 49f47d310..bec3f4929 100644 --- a/locales/mn/common.json +++ b/locales/mn/common.json @@ -4483,6 +4483,10 @@ "menu": "Харилцагчийн цэсийг нээх" } }, + "ux_pim_contact": { + "context_private": "Гэр", + "phone_voice": "Утас" + }, "uxs_wording": { "more_options": "Бусад сонголт", "use_in_other_apps": "Бусад апп-д ашиглах", diff --git a/locales/nb/common.json b/locales/nb/common.json index 6c54c14c6..07ec4cd7b 100644 --- a/locales/nb/common.json +++ b/locales/nb/common.json @@ -4483,6 +4483,10 @@ "menu": "Åpne kontaktens meny" } }, + "ux_pim_contact": { + "context_private": "Hjem", + "phone_voice": "Telefon" + }, "uxs_wording": { "more_options": "Flere valg", "use_in_other_apps": "Bruk i andre apper", diff --git a/locales/nl/common.json b/locales/nl/common.json index caa6592d6..91dcf6f5c 100644 --- a/locales/nl/common.json +++ b/locales/nl/common.json @@ -4483,6 +4483,10 @@ "open_categories": "Categorieën openen", "row_actions": "Acties voor {name}" }, + "ux_pim_contact": { + "context_private": "Thuis", + "phone_voice": "Telefoon" + }, "uxs_wording": { "more_options": "Meer opties", "use_in_other_apps": "Gebruiken in andere apps", diff --git a/locales/pl/common.json b/locales/pl/common.json index a42440af6..2d685206b 100644 --- a/locales/pl/common.json +++ b/locales/pl/common.json @@ -4483,6 +4483,10 @@ "open_categories": "Otwórz kategorie", "row_actions": "Działania dla {name}" }, + "ux_pim_contact": { + "context_private": "Dom", + "phone_voice": "Telefon" + }, "uxs_wording": { "more_options": "Więcej opcji", "use_in_other_apps": "Użyj w innych aplikacjach", diff --git a/locales/pt/common.json b/locales/pt/common.json index bd73fa852..ed616d8a5 100644 --- a/locales/pt/common.json +++ b/locales/pt/common.json @@ -4483,6 +4483,10 @@ "open_categories": "Abrir categorias", "row_actions": "Ações para {name}" }, + "ux_pim_contact": { + "context_private": "Casa", + "phone_voice": "Telefone" + }, "uxs_wording": { "more_options": "Mais opções", "use_in_other_apps": "Usar noutras aplicações", diff --git a/locales/ro/common.json b/locales/ro/common.json index ad9938c1a..0ba1f75c8 100644 --- a/locales/ro/common.json +++ b/locales/ro/common.json @@ -4483,6 +4483,10 @@ "menu": "Deschideți meniul contactului" } }, + "ux_pim_contact": { + "context_private": "Acasă", + "phone_voice": "Telefon" + }, "uxs_wording": { "more_options": "Mai multe opțiuni", "use_in_other_apps": "Folosește în alte aplicații", diff --git a/locales/ru/common.json b/locales/ru/common.json index e32676136..3b53ae9ec 100644 --- a/locales/ru/common.json +++ b/locales/ru/common.json @@ -4483,6 +4483,10 @@ "open_categories": "Открыть категории", "row_actions": "Действия для {name}" }, + "ux_pim_contact": { + "context_private": "Домашний", + "phone_voice": "Телефон" + }, "uxs_wording": { "more_options": "Другие параметры", "use_in_other_apps": "Использовать в других приложениях", diff --git a/locales/sk/common.json b/locales/sk/common.json index 3da1ed6c4..9d90a7e39 100644 --- a/locales/sk/common.json +++ b/locales/sk/common.json @@ -4483,6 +4483,10 @@ "menu": "Otvoriť ponuku kontaktu" } }, + "ux_pim_contact": { + "context_private": "Domov", + "phone_voice": "Telefón" + }, "uxs_wording": { "more_options": "Ďalšie možnosti", "use_in_other_apps": "Použiť v iných aplikáciách", diff --git a/locales/th/common.json b/locales/th/common.json index d8fbce3f9..61e884b59 100644 --- a/locales/th/common.json +++ b/locales/th/common.json @@ -3821,6 +3821,10 @@ "menu": "เปิดเมนูของผู้ติดต่อ" } }, + "ux_pim_contact": { + "context_private": "บ้าน", + "phone_voice": "โทรศัพท์" + }, "uxs_wording": { "more_options": "ตัวเลือกเพิ่มเติม", "use_in_other_apps": "ใช้ในแอปอื่น", diff --git a/locales/tr/common.json b/locales/tr/common.json index 59e4b73db..ee0535a1f 100644 --- a/locales/tr/common.json +++ b/locales/tr/common.json @@ -4483,6 +4483,10 @@ "open_categories": "Kategorileri aç", "row_actions": "{name} için işlemler" }, + "ux_pim_contact": { + "context_private": "Ev", + "phone_voice": "Telefon" + }, "uxs_wording": { "more_options": "Diğer seçenekler", "use_in_other_apps": "Başka uygulamalarda kullan", diff --git a/locales/uk/common.json b/locales/uk/common.json index be354b49c..be055e0bf 100644 --- a/locales/uk/common.json +++ b/locales/uk/common.json @@ -4483,6 +4483,10 @@ "open_categories": "Відкрити категорії", "row_actions": "Дії для {name}" }, + "ux_pim_contact": { + "context_private": "Домашній", + "phone_voice": "Телефон" + }, "uxs_wording": { "more_options": "Інші параметри", "use_in_other_apps": "Використовувати в інших застосунках", diff --git a/locales/vi/common.json b/locales/vi/common.json index fe1be93e4..767c5b683 100644 --- a/locales/vi/common.json +++ b/locales/vi/common.json @@ -4495,6 +4495,10 @@ "menu": "Mở menu của liên hệ" } }, + "ux_pim_contact": { + "context_private": "Nhà riêng", + "phone_voice": "Điện thoại" + }, "uxs_wording": { "more_options": "Tùy chọn khác", "use_in_other_apps": "Dùng trong ứng dụng khác", diff --git a/locales/zh-TW/common.json b/locales/zh-TW/common.json index 205e145f6..617249424 100644 --- a/locales/zh-TW/common.json +++ b/locales/zh-TW/common.json @@ -4483,6 +4483,10 @@ "menu": "開啟聯絡人選單" } }, + "ux_pim_contact": { + "context_private": "住家", + "phone_voice": "電話" + }, "uxs_wording": { "more_options": "更多選項", "use_in_other_apps": "在其他應用程式中使用", diff --git a/locales/zh/common.json b/locales/zh/common.json index 0e601ed20..109399036 100644 --- a/locales/zh/common.json +++ b/locales/zh/common.json @@ -4483,6 +4483,10 @@ "open_categories": "打开分类", "row_actions": "{name} 的操作" }, + "ux_pim_contact": { + "context_private": "住宅", + "phone_voice": "电话" + }, "uxs_wording": { "more_options": "更多选项", "use_in_other_apps": "在其他应用中使用", From 629ba34419d8fcca9a17342546dae0f918d306bc Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Wed, 4 Nov 2026 11:02:17 +0100 Subject: [PATCH 2/6] =?UTF-8?q?fix:=20put=20rarely=20used=20contact=20fiel?= =?UTF-8?q?ds=20under=20More=20fields=20and=20mark=20dialog=20items=20with?= =?UTF-8?q?=20=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The contacts-app tests now click the new Import contacts… label (key import_menu) instead of import.title. --- .../contacts/__tests__/contacts-app.test.tsx | 6 +- .../__tests__/contacts-ux-pim.test.tsx | 54 +++- components/contacts/contact-form.tsx | 237 +++++++++--------- components/contacts/contact-list.tsx | 3 +- components/contacts/contacts-app.tsx | 3 +- components/contacts/contacts-sidebar.tsx | 5 +- locales/ar/common.json | 5 +- locales/ca/common.json | 5 +- locales/cs/common.json | 5 +- locales/da/common.json | 5 +- locales/de/common.json | 5 +- locales/en/common.json | 5 +- locales/es/common.json | 5 +- locales/et/common.json | 5 +- locales/fa/common.json | 5 +- locales/fr/common.json | 5 +- locales/he/common.json | 5 +- locales/hu/common.json | 5 +- locales/it/common.json | 5 +- locales/ja/common.json | 5 +- locales/ko/common.json | 5 +- locales/lv/common.json | 5 +- locales/mn/common.json | 5 +- locales/nb/common.json | 5 +- locales/nl/common.json | 5 +- locales/pl/common.json | 5 +- locales/pt/common.json | 5 +- locales/ro/common.json | 5 +- locales/ru/common.json | 5 +- locales/sk/common.json | 5 +- locales/th/common.json | 5 +- locales/tr/common.json | 5 +- locales/uk/common.json | 5 +- locales/vi/common.json | 5 +- locales/zh-TW/common.json | 5 +- locales/zh/common.json | 5 +- 36 files changed, 304 insertions(+), 154 deletions(-) diff --git a/components/contacts/__tests__/contacts-app.test.tsx b/components/contacts/__tests__/contacts-app.test.tsx index 802c8f994..7d8ba656b 100644 --- a/components/contacts/__tests__/contacts-app.test.tsx +++ b/components/contacts/__tests__/contacts-app.test.tsx @@ -215,7 +215,7 @@ describe('ContactsApp', () => { renderContacts(); fireEvent.click(screen.getAllByRole('button', { name: 'open_categories' })[0]); fireEvent.click(within(drawer()).getByRole('button', { name: 'new' })); - fireEvent.click(screen.getByText('import.title')); + fireEvent.click(screen.getByText('import_menu')); const dialog = screen.getByRole('dialog', { name: 'import.title' }); fireEvent.keyDown(dialog, { key: 'Escape' }); expect(screen.queryByRole('dialog', { name: 'import.title' })).toBeNull(); @@ -273,7 +273,7 @@ describe('ContactsApp', () => { try { renderContacts(); fireEvent.click(screen.getByRole('button', { name: 'more' })); - fireEvent.click(screen.getByText('import.title')); + fireEvent.click(screen.getByText('import_menu')); expect(screen.getByRole('dialog', { name: 'import.title' })).toBeInTheDocument(); } finally { window.matchMedia = matchMedia; @@ -287,7 +287,7 @@ describe('ContactsApp', () => { fireEvent.click(within(drawer()).getByText('Work')); expect(screen.getByText('empty_view_title')).toBeInTheDocument(); expect(screen.queryByText('empty_state_title')).toBeNull(); - fireEvent.click(screen.getByRole('button', { name: /import.title/ })); + fireEvent.click(screen.getByRole('button', { name: /import_menu/ })); expect(screen.getByRole('dialog', { name: 'import.title' })).toBeInTheDocument(); }); diff --git a/components/contacts/__tests__/contacts-ux-pim.test.tsx b/components/contacts/__tests__/contacts-ux-pim.test.tsx index 9ff7af07b..9b743586c 100644 --- a/components/contacts/__tests__/contacts-ux-pim.test.tsx +++ b/components/contacts/__tests__/contacts-ux-pim.test.tsx @@ -1,6 +1,6 @@ -import { render, screen } from '@testing-library/react'; +import { fireEvent, render, screen, within } from '@testing-library/react'; import { describe, expect, it, vi } from 'vitest'; -import type { ContactCard } from '@/lib/jmap/types'; +import type { AddressBook, ContactCard } from '@/lib/jmap/types'; // Keys with their namespace, so a label read from ux_pim_contact can be told // from the old one in contacts.form. @@ -19,6 +19,7 @@ vi.mock('@/components/contacts/contact-activity', () => ({ ContactActivity: () = import { ContactForm } from '../contact-form'; import { ContactDetail } from '../contact-detail'; +import { ContactsSidebar } from '../contacts-sidebar'; const anna: ContactCard = { id: 'c1', @@ -43,3 +44,52 @@ describe('pim-19: standard words read as people say them', () => { expect(screen.getByText('ux_pim_contact.context_private · ux_pim_contact.phone_voice')).toBeInTheDocument(); }); }); + +describe('pim-26: the contact editor keeps rare fields under "More fields"', () => { + it('collapses Gender, Calendar, Online services and Personal info when none has a value', () => { + render(); + expect(screen.getByText('ux_pim_contact.more_fields')).toBeInTheDocument(); + expect(screen.queryByText('contacts.form.gender_sex')).toBeNull(); + expect(screen.queryByText('contacts.form.calendar_uri')).toBeNull(); + expect(screen.queryByText('contacts.form.add_online_service')).toBeNull(); + expect(screen.queryByText('contacts.form.add_personal_info')).toBeNull(); + fireEvent.click(screen.getByText('ux_pim_contact.more_fields')); + expect(screen.getByText('contacts.form.gender_sex')).toBeInTheDocument(); + expect(screen.getByText('contacts.form.calendar_uri')).toBeInTheDocument(); + expect(screen.getByText('contacts.form.add_online_service')).toBeInTheDocument(); + expect(screen.getByText('contacts.form.add_personal_info')).toBeInTheDocument(); + }); + + it('opens "More fields" when one of them has a value', () => { + render(); + expect(screen.getByDisplayValue('https://cal.example/anna')).toBeInTheDocument(); + }); +}); + +function renderSidebar(addressBooks: AddressBook[]) { + return render( + , + ); +} + +describe('pim-26: the Contacts menus', () => { + it('ends "Import contacts" and "New address book" in "…", as they open a dialog', () => { + renderSidebar([{ id: 'b1', name: 'Contacts', isDefault: true } as AddressBook]); + fireEvent.click(screen.getByRole('button', { name: 'contacts.new' })); + const menu = screen.getByRole('menu'); + expect(within(menu).getByText('ux_pim_contact.import_menu')).toBeInTheDocument(); + expect(within(menu).getByText('ux_pim_contact.new_address_book_menu')).toBeInTheDocument(); + }); +}); diff --git a/components/contacts/contact-form.tsx b/components/contacts/contact-form.tsx index 4e43f313c..bea3db826 100644 --- a/components/contacts/contact-form.tsx +++ b/components/contacts/contact-form.tsx @@ -3,7 +3,7 @@ import { useState, useMemo, useCallback, useRef, useEffect, useId } from "react"; import { useCloseBesideControls } from "@/hooks/use-close-beside-controls"; import { useTranslations } from "next-intl"; -import { X, Plus, ChevronDown, ChevronRight, User, Building, MapPin, Globe, Cake, Heart, Tag, StickyNote, Mail, Phone, Calendar, UserCircle, Book, Camera, Trash2 } from "@/components/icons"; +import { X, Plus, ChevronDown, ChevronRight, User, Building, MapPin, Globe, Cake, Heart, Tag, StickyNote, Mail, Phone, Calendar, UserCircle, Book, Camera, Trash2, MoreHorizontal } from "@/components/icons"; import { Button } from "@/components/ui/button"; import { IconButton } from "@/components/ui/icon-button"; import { Input } from "@/components/ui/input"; @@ -478,6 +478,8 @@ export function ContactForm({ contact, addressBooks, allKeywords, defaultAddress const dirty = fields !== initialFields || photoEdit.kind !== "unchanged"; useEffect(() => { onDirtyChange?.(dirty); }, [dirty, onDirtyChange]); const footerRef = useBottomControls(); + const [hasMoreFields] = useState(() => onlineServices.length > 0 || personalInfoEntries.length > 0 + || !!(genderSex || genderIdentity) || !!(calendarUri || schedulingUri || freeBusyUri)); const handlePhotoSelect = async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; @@ -1059,37 +1061,6 @@ export function ContactForm({ contact, addressBooks, allKeywords, defaultAddress - {/* Online Services */} - 0}> -
- {onlineServices.map((svc, i) => ( -
- { const n = [...onlineServices]; n[i] = { ...n[i], uri: e.target.value }; setOnlineServices(n); }} - placeholder={t("url_placeholder")} - aria-label={t("online_service_url")} - className="flex-1" - /> - { const n = [...onlineServices]; n[i] = { ...n[i], service: e.target.value }; setOnlineServices(n); }} - placeholder={t("service_placeholder")} - aria-label={t("service_placeholder")} - className="w-24" - /> - setOnlineServices(onlineServices.filter((_, j) => j !== i))} className="shrink-0"> - - -
- ))} - -
-
- {/* Anniversaries */} 0}>
@@ -1124,50 +1095,6 @@ export function ContactForm({ contact, addressBooks, allKeywords, defaultAddress
- {/* Personal Info */} - 0}> -
- {personalInfoEntries.map((pi, i) => ( -
- { const n = [...personalInfoEntries]; n[i] = { ...n[i], value: e.target.value }; setPersonalInfoEntries(n); }} - placeholder={t("personal_info_placeholder")} - aria-label={t("personal_info")} - className="flex-1" - /> - - - setPersonalInfoEntries(personalInfoEntries.filter((_, j) => j !== i))} className="shrink-0"> - - -
- ))} - -
-
- {/* Categories */} - {/* Gender */} - -
-
- - -
-
- - setGenderIdentity(e.target.value)} placeholder={t("gender_identity_placeholder")} /> -
-
-
- - {/* Calendar */} - -
-
- - setCalendarUri(e.target.value)} placeholder="https://..." /> -
-
- - setSchedulingUri(e.target.value)} placeholder="https://..." /> -
-
- - setFreeBusyUri(e.target.value)} placeholder="https://..." /> -
-
-
- {/* Notes */}