mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 00:12:39 +00:00
feat: sort contacts by last name #963
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { describe, it, expect, vi, afterEach } from 'vitest';
|
||||
import { ContactList } from '../contact-list';
|
||||
import { useSettingsStore } from '@/stores/settings-store';
|
||||
import type { ContactCard } from '@/lib/jmap/types';
|
||||
|
||||
function makeContact(overrides: Partial<ContactCard> & { id: string }): ContactCard {
|
||||
@@ -78,4 +79,42 @@ describe('ContactList', () => {
|
||||
expect(screen.getByText('bulk.export')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('sort order (#963)', () => {
|
||||
const carol = makeContact({
|
||||
id: '4',
|
||||
name: { components: [{ kind: 'given', value: 'Carol' }, { kind: 'surname', value: 'Smith' }], isOrdered: true },
|
||||
});
|
||||
const family = [alice, bob, carol];
|
||||
const NAME = /^(Alice Smith|Bob Jones|Carol Smith)$/;
|
||||
const renderedNames = () => screen.getAllByText(NAME).map((el) => el.textContent);
|
||||
|
||||
afterEach(() => {
|
||||
useSettingsStore.setState({ sortContactsByLastName: false, groupContactsByLetter: true });
|
||||
});
|
||||
|
||||
it('sorts by display name by default', () => {
|
||||
render(<ContactList {...defaultProps} contacts={family} />);
|
||||
expect(renderedNames()).toEqual(['Alice Smith', 'Bob Jones', 'Carol Smith']);
|
||||
expect(screen.getByText('A')).toBeInTheDocument();
|
||||
expect(screen.getByText('B')).toBeInTheDocument();
|
||||
expect(screen.getByText('C')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('groups family members together when sorting by last name', () => {
|
||||
useSettingsStore.setState({ sortContactsByLastName: true });
|
||||
render(<ContactList {...defaultProps} contacts={family} />);
|
||||
expect(renderedNames()).toEqual(['Bob Jones', 'Alice Smith', 'Carol Smith']);
|
||||
// Letter headers follow the surname, not the given name.
|
||||
expect(screen.getByText('J')).toBeInTheDocument();
|
||||
expect(screen.getByText('S')).toBeInTheDocument();
|
||||
expect(screen.queryByText('A')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('still matches the search query against the display name', () => {
|
||||
useSettingsStore.setState({ sortContactsByLastName: true });
|
||||
render(<ContactList {...defaultProps} contacts={family} searchQuery="alice" />);
|
||||
expect(renderedNames()).toEqual(['Alice Smith']);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import { useTranslations, useLocale } from "next-intl";
|
||||
import { Search, BookUser, Trash2, Users, Download, X, UserPlus, CheckSquare, Square, Filter, Mail, Phone, Image as ImageIcon, RotateCcw, Menu } from "lucide-react";
|
||||
import { Search, BookUser, Trash2, Users, Download, X, UserPlus, CheckSquare, Square, Filter, Mail, Phone, Image as ImageIcon, RotateCcw, Menu, ArrowDownAZ } from "lucide-react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ContactListItem } from "./contact-list-item";
|
||||
@@ -10,7 +10,7 @@ import { ContactContextMenu } from "./contact-context-menu";
|
||||
import { useContextMenu } from "@/hooks/use-context-menu";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { AnniversaryDate, ContactCard } from "@/lib/jmap/types";
|
||||
import { getContactDisplayName, getContactPhotoUri } from "@/stores/contact-store";
|
||||
import { getContactDisplayName, getContactPhotoUri, getContactSortName } from "@/stores/contact-store";
|
||||
import { useSettingsStore } from "@/stores/settings-store";
|
||||
|
||||
type TriState = boolean | null;
|
||||
@@ -141,6 +141,8 @@ export function ContactList({
|
||||
const locale = useLocale();
|
||||
const density = useSettingsStore((state) => state.density);
|
||||
const groupByLetter = useSettingsStore((state) => state.groupContactsByLetter);
|
||||
const sortByLastName = useSettingsStore((state) => state.sortContactsByLastName);
|
||||
const updateSetting = useSettingsStore((state) => state.updateSetting);
|
||||
const { contextMenu, openContextMenu, closeContextMenu, menuRef } = useContextMenu<ContactCard>();
|
||||
const [filtersOpen, setFiltersOpen] = useState(false);
|
||||
const [filters, setFilters] = useState<ListFilters>(EMPTY_FILTERS);
|
||||
@@ -229,21 +231,22 @@ export function ContactList({
|
||||
|
||||
const sorted = useMemo(() => {
|
||||
return [...filtered].sort((a, b) => {
|
||||
const nameA = getContactDisplayName(a).toLowerCase();
|
||||
const nameB = getContactDisplayName(b).toLowerCase();
|
||||
const nameA = getContactSortName(a, sortByLastName).toLowerCase();
|
||||
const nameB = getContactSortName(b, sortByLastName).toLowerCase();
|
||||
return nameA.localeCompare(nameB);
|
||||
});
|
||||
}, [filtered]);
|
||||
}, [filtered, sortByLastName]);
|
||||
|
||||
const sortedIds = useMemo(() => sorted.map(c => c.id), [sorted]);
|
||||
|
||||
// Group sorted contacts by first letter of display name. Non-letter
|
||||
// starters (digits, symbols, empty) collect under "#" which sorts last.
|
||||
// Group sorted contacts by first letter of the sort name (display name, or
|
||||
// surname when sorting by last name). Non-letter starters (digits, symbols,
|
||||
// empty) collect under "#" which sorts last.
|
||||
const groupedSections = useMemo(() => {
|
||||
const collator = new Intl.Collator(locale, { sensitivity: "base" });
|
||||
const groups = new Map<string, ContactCard[]>();
|
||||
for (const contact of sorted) {
|
||||
const name = getContactDisplayName(contact).trim();
|
||||
const name = getContactSortName(contact, sortByLastName).trim();
|
||||
const first = name.charAt(0);
|
||||
const letter = first && first.toLocaleUpperCase(locale).match(/\p{L}/u)
|
||||
? first.toLocaleUpperCase(locale)
|
||||
@@ -259,7 +262,7 @@ export function ContactList({
|
||||
return collator.compare(a, b);
|
||||
})
|
||||
.map(([letter, items]) => ({ letter, items }));
|
||||
}, [sorted, locale]);
|
||||
}, [sorted, locale, sortByLastName]);
|
||||
|
||||
const hasSelection = selectedContactIds.size > 0;
|
||||
const allSelected = sorted.length > 0 && sorted.every(c => selectedContactIds.has(c.id));
|
||||
@@ -451,6 +454,26 @@ export function ContactList({
|
||||
onClick={() => setFilters((f) => ({ ...f, hasPhoto: cycleTri(f.hasPhoto) }))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Sort order (persisted setting, also on /settings/contacts) */}
|
||||
<div className="flex flex-wrap items-center gap-2 pt-1 border-t border-border/50">
|
||||
<span className="text-xs text-muted-foreground inline-flex items-center gap-1">
|
||||
<ArrowDownAZ className="w-3.5 h-3.5" />
|
||||
{t("filters.sort_by")}
|
||||
</span>
|
||||
<ToggleChip
|
||||
icon={null}
|
||||
label={t("filters.sort_first_name")}
|
||||
value={sortByLastName ? null : true}
|
||||
onClick={() => updateSetting("sortContactsByLastName", false)}
|
||||
/>
|
||||
<ToggleChip
|
||||
icon={null}
|
||||
label={t("filters.sort_last_name")}
|
||||
value={sortByLastName ? true : null}
|
||||
onClick={() => updateSetting("sortContactsByLastName", true)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -22,6 +22,7 @@ export function ContactsSettings() {
|
||||
importContacts,
|
||||
} = useContactStore();
|
||||
const groupContactsByLetter = useSettingsStore((s) => s.groupContactsByLetter);
|
||||
const sortContactsByLastName = useSettingsStore((s) => s.sortContactsByLastName);
|
||||
const updateSetting = useSettingsStore((s) => s.updateSetting);
|
||||
const [showImport, setShowImport] = useState(false);
|
||||
|
||||
@@ -68,6 +69,16 @@ export function ContactsSettings() {
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem
|
||||
label={tSettings("sort_by_last_name_label")}
|
||||
description={tSettings("sort_by_last_name_description")}
|
||||
>
|
||||
<ToggleSwitch
|
||||
checked={sortContactsByLastName}
|
||||
onChange={(checked) => updateSetting("sortContactsByLastName", checked)}
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem
|
||||
label={tSettings("import_label")}
|
||||
description={tSettings("import_description")}
|
||||
|
||||
@@ -1898,6 +1898,8 @@
|
||||
"title": "جهات الاتصال",
|
||||
"description": "استيراد وتصدير جهات اتصالك",
|
||||
"group_by_letter_label": "التجميع حسب الحرف الأول",
|
||||
"sort_by_last_name_label": "الترتيب حسب اسم العائلة",
|
||||
"sort_by_last_name_description": "ترتيب قائمة جهات الاتصال حسب اسم العائلة بحيث يظهر أفراد العائلة معًا",
|
||||
"group_by_letter_description": "إظهار عناوين أقسام أبجدية في قائمة جهات الاتصال",
|
||||
"import_label": "استيراد جهات الاتصال",
|
||||
"import_description": "استيراد جهات الاتصال من ملف vCard (.vcf)",
|
||||
@@ -2746,7 +2748,10 @@
|
||||
"any_month": "أي شهر",
|
||||
"has_email": "لديه بريد إلكتروني",
|
||||
"has_phone": "لديه هاتف",
|
||||
"has_photo": "لديه صورة"
|
||||
"has_photo": "لديه صورة",
|
||||
"sort_by": "الترتيب حسب",
|
||||
"sort_first_name": "الاسم الأول",
|
||||
"sort_last_name": "اسم العائلة"
|
||||
}
|
||||
},
|
||||
"calendar": {
|
||||
|
||||
@@ -1898,6 +1898,8 @@
|
||||
"title": "Contactes",
|
||||
"description": "Importeu i exporteu els contactes",
|
||||
"group_by_letter_label": "Agrupa per lletra inicial",
|
||||
"sort_by_last_name_label": "Ordena per cognom",
|
||||
"sort_by_last_name_description": "Ordena la llista de contactes per cognom perquè els membres d'una família apareguin junts",
|
||||
"group_by_letter_description": "Mostra capçaleres de secció alfabètiques a la llista de contactes",
|
||||
"import_label": "Importa contactes",
|
||||
"import_description": "Importeu contactes des d'un fitxer vCard (.vcf)",
|
||||
@@ -2746,7 +2748,10 @@
|
||||
"any_month": "Qualsevol mes",
|
||||
"has_email": "Té correu electrònic",
|
||||
"has_phone": "Té telèfon",
|
||||
"has_photo": "Té foto"
|
||||
"has_photo": "Té foto",
|
||||
"sort_by": "Ordena per",
|
||||
"sort_first_name": "Nom",
|
||||
"sort_last_name": "Cognom"
|
||||
}
|
||||
},
|
||||
"calendar": {
|
||||
|
||||
@@ -1891,6 +1891,8 @@
|
||||
"title": "Kontakty",
|
||||
"description": "Import a export kontaktů",
|
||||
"group_by_letter_label": "Seskupit podle prvního písmena",
|
||||
"sort_by_last_name_label": "Řadit podle příjmení",
|
||||
"sort_by_last_name_description": "Seřadit seznam kontaktů podle příjmení, aby členové rodiny byli pohromadě",
|
||||
"group_by_letter_description": "Zobrazit záhlaví sekcí podle abecedy v seznamu kontaktů",
|
||||
"import_label": "Importovat kontakty",
|
||||
"import_description": "Importovat kontakty ze souboru vCard (.vcf)",
|
||||
@@ -2745,7 +2747,10 @@
|
||||
"any_month": "Kterýkoli měsíc",
|
||||
"has_email": "Má e-mail",
|
||||
"has_phone": "Má telefon",
|
||||
"has_photo": "Má fotku"
|
||||
"has_photo": "Má fotku",
|
||||
"sort_by": "Řadit podle",
|
||||
"sort_first_name": "Jméno",
|
||||
"sort_last_name": "Příjmení"
|
||||
},
|
||||
"open_categories": "Otevřít kategorie"
|
||||
},
|
||||
|
||||
@@ -1894,6 +1894,8 @@
|
||||
"title": "Kontakter",
|
||||
"description": "Importér og eksportér dine kontakter",
|
||||
"group_by_letter_label": "Gruppér efter første bogstav",
|
||||
"sort_by_last_name_label": "Sortér efter efternavn",
|
||||
"sort_by_last_name_description": "Sortér kontaktlisten efter efternavn, så familiemedlemmer står samlet",
|
||||
"group_by_letter_description": "Vis alfabetiske sektionsoverskrifter i kontaktlisten",
|
||||
"import_label": "Importér kontakter",
|
||||
"import_description": "Importér kontakter fra en vCard-fil (.vcf)",
|
||||
@@ -2745,7 +2747,10 @@
|
||||
"any_month": "Enhver måned",
|
||||
"has_email": "Har e-mail",
|
||||
"has_phone": "Har telefon",
|
||||
"has_photo": "Har billede"
|
||||
"has_photo": "Har billede",
|
||||
"sort_by": "Sortér efter",
|
||||
"sort_first_name": "Fornavn",
|
||||
"sort_last_name": "Efternavn"
|
||||
},
|
||||
"open_categories": "Åbn kategorier"
|
||||
},
|
||||
|
||||
@@ -1901,7 +1901,9 @@
|
||||
"categories_description": "Kontaktkategorien umbenennen",
|
||||
"no_categories": "Keine Kategorien gefunden",
|
||||
"group_by_letter_description": "Alphabetische Abschnittsüberschriften in der Kontaktliste anzeigen",
|
||||
"group_by_letter_label": "Nach Anfangsbuchstaben gruppieren"
|
||||
"group_by_letter_label": "Nach Anfangsbuchstaben gruppieren",
|
||||
"sort_by_last_name_label": "Nach Nachname sortieren",
|
||||
"sort_by_last_name_description": "Kontaktliste nach Nachnamen ordnen, damit Familienmitglieder zusammenstehen"
|
||||
},
|
||||
"downloads": {
|
||||
"title": "Downloads",
|
||||
@@ -2745,7 +2747,10 @@
|
||||
"any_month": "Beliebiger Monat",
|
||||
"has_email": "Mit E-Mail",
|
||||
"has_phone": "Mit Telefon",
|
||||
"has_photo": "Mit Foto"
|
||||
"has_photo": "Mit Foto",
|
||||
"sort_by": "Sortieren nach",
|
||||
"sort_first_name": "Vorname",
|
||||
"sort_last_name": "Nachname"
|
||||
},
|
||||
"open_categories": "Kategorien öffnen"
|
||||
},
|
||||
|
||||
@@ -1899,6 +1899,8 @@
|
||||
"description": "Import and export your contacts",
|
||||
"group_by_letter_label": "Group by first letter",
|
||||
"group_by_letter_description": "Show alphabetical section headers in the contact list",
|
||||
"sort_by_last_name_label": "Sort by last name",
|
||||
"sort_by_last_name_description": "Order the contact list by surname so family members appear together",
|
||||
"import_label": "Import Contacts",
|
||||
"import_description": "Import contacts from a vCard (.vcf) file",
|
||||
"export_label": "Export Contacts",
|
||||
@@ -2746,7 +2748,10 @@
|
||||
"any_month": "Any month",
|
||||
"has_email": "Has email",
|
||||
"has_phone": "Has phone",
|
||||
"has_photo": "Has photo"
|
||||
"has_photo": "Has photo",
|
||||
"sort_by": "Sort by",
|
||||
"sort_first_name": "First name",
|
||||
"sort_last_name": "Last name"
|
||||
}
|
||||
},
|
||||
"calendar": {
|
||||
|
||||
@@ -1901,7 +1901,9 @@
|
||||
"categories_description": "Renombrar categorías de contactos",
|
||||
"no_categories": "No se encontraron categorías",
|
||||
"group_by_letter_description": "Mostrar encabezados alfabéticos en la lista de contactos",
|
||||
"group_by_letter_label": "Agrupar por primera letra"
|
||||
"group_by_letter_label": "Agrupar por primera letra",
|
||||
"sort_by_last_name_label": "Ordenar por apellido",
|
||||
"sort_by_last_name_description": "Ordenar la lista de contactos por apellido para que los miembros de una familia aparezcan juntos"
|
||||
},
|
||||
"downloads": {
|
||||
"title": "Descargas",
|
||||
@@ -2745,7 +2747,10 @@
|
||||
"any_month": "Cualquier mes",
|
||||
"has_email": "Con correo",
|
||||
"has_phone": "Con teléfono",
|
||||
"has_photo": "Con foto"
|
||||
"has_photo": "Con foto",
|
||||
"sort_by": "Ordenar por",
|
||||
"sort_first_name": "Nombre",
|
||||
"sort_last_name": "Apellido"
|
||||
},
|
||||
"open_categories": "Abrir categorías"
|
||||
},
|
||||
|
||||
@@ -1898,6 +1898,8 @@
|
||||
"title": "مخاطبین",
|
||||
"description": "وارد کردن و خروجی مخاطبین",
|
||||
"group_by_letter_label": "گروهبندی بر اساس حرف اول",
|
||||
"sort_by_last_name_label": "مرتبسازی بر اساس نام خانوادگی",
|
||||
"sort_by_last_name_description": "لیست مخاطبین را بر اساس نام خانوادگی مرتب کنید تا اعضای خانواده کنار هم نمایش داده شوند",
|
||||
"group_by_letter_description": "نمایش هدرهای الفبایی در لیست مخاطبین",
|
||||
"import_label": "وارد کردن مخاطبین",
|
||||
"import_description": "وارد کردن مخاطبین از فایل vCard (.vcf)",
|
||||
@@ -2746,7 +2748,10 @@
|
||||
"any_month": "هر ماه",
|
||||
"has_email": "دارای ایمیل",
|
||||
"has_phone": "دارای تلفن",
|
||||
"has_photo": "دارای عکس"
|
||||
"has_photo": "دارای عکس",
|
||||
"sort_by": "مرتبسازی بر اساس",
|
||||
"sort_first_name": "نام",
|
||||
"sort_last_name": "نام خانوادگی"
|
||||
}
|
||||
},
|
||||
"calendar": {
|
||||
|
||||
@@ -1901,7 +1901,9 @@
|
||||
"categories_description": "Renommer les catégories de contacts",
|
||||
"no_categories": "Aucune catégorie trouvée",
|
||||
"group_by_letter_description": "Afficher des en-têtes alphabétiques dans la liste de contacts",
|
||||
"group_by_letter_label": "Grouper par première lettre"
|
||||
"group_by_letter_label": "Grouper par première lettre",
|
||||
"sort_by_last_name_label": "Trier par nom de famille",
|
||||
"sort_by_last_name_description": "Classer la liste des contacts par nom de famille pour regrouper les membres d'une même famille"
|
||||
},
|
||||
"downloads": {
|
||||
"title": "Téléchargements",
|
||||
@@ -2745,7 +2747,10 @@
|
||||
"any_month": "Tous les mois",
|
||||
"has_email": "Avec e-mail",
|
||||
"has_phone": "Avec téléphone",
|
||||
"has_photo": "Avec photo"
|
||||
"has_photo": "Avec photo",
|
||||
"sort_by": "Trier par",
|
||||
"sort_first_name": "Prénom",
|
||||
"sort_last_name": "Nom de famille"
|
||||
},
|
||||
"open_categories": "Ouvrir les catégories"
|
||||
},
|
||||
|
||||
@@ -1867,6 +1867,8 @@
|
||||
"categories_description": "שנה שם של קטגוריות אנשי קשר",
|
||||
"no_categories": "לא נמצאו קטגוריות",
|
||||
"group_by_letter_label": "קבוצה לפי אות ראשונה",
|
||||
"sort_by_last_name_label": "מיון לפי שם משפחה",
|
||||
"sort_by_last_name_description": "סידור רשימת אנשי הקשר לפי שם משפחה כך שבני אותה משפחה יופיעו יחד",
|
||||
"group_by_letter_description": "הצג כותרות חלקים אלפביתיים ברשימת אנשי הקשר"
|
||||
},
|
||||
"filters": {
|
||||
@@ -2670,7 +2672,10 @@
|
||||
"any_month": "כל חודש",
|
||||
"has_email": "יש דוא״ל",
|
||||
"has_phone": "יש טלפון",
|
||||
"has_photo": "יש תמונה"
|
||||
"has_photo": "יש תמונה",
|
||||
"sort_by": "מיון לפי",
|
||||
"sort_first_name": "שם פרטי",
|
||||
"sort_last_name": "שם משפחה"
|
||||
}
|
||||
},
|
||||
"calendar": {
|
||||
|
||||
@@ -1894,6 +1894,8 @@
|
||||
"title": "Névjegyek",
|
||||
"description": "Névjegyek importálása és exportálása",
|
||||
"group_by_letter_label": "Csoportosítás kezdőbetű szerint",
|
||||
"sort_by_last_name_label": "Rendezés vezetéknév szerint",
|
||||
"sort_by_last_name_description": "A névjegylista vezetéknév szerinti rendezése, hogy a családtagok egymás mellett legyenek",
|
||||
"group_by_letter_description": "ABC sorrendű fejléc mutatása a névjegy listában",
|
||||
"import_label": "Névjegyek importálása",
|
||||
"import_description": "Névjegyek importálása vCard (.vcf) fájlból",
|
||||
@@ -2746,7 +2748,10 @@
|
||||
"any_month": "Bármely hónap",
|
||||
"has_email": "Van e-mail",
|
||||
"has_phone": "Van telefon",
|
||||
"has_photo": "Van fotó"
|
||||
"has_photo": "Van fotó",
|
||||
"sort_by": "Rendezés",
|
||||
"sort_first_name": "Keresztnév",
|
||||
"sort_last_name": "Vezetéknév"
|
||||
}
|
||||
},
|
||||
"calendar": {
|
||||
|
||||
@@ -1901,7 +1901,9 @@
|
||||
"categories_description": "Rinomina le categorie dei contatti",
|
||||
"no_categories": "Nessuna categoria trovata",
|
||||
"group_by_letter_description": "Mostra intestazioni alfabetiche nell'elenco dei contatti",
|
||||
"group_by_letter_label": "Raggruppa per prima lettera"
|
||||
"group_by_letter_label": "Raggruppa per prima lettera",
|
||||
"sort_by_last_name_label": "Ordina per cognome",
|
||||
"sort_by_last_name_description": "Ordina l'elenco dei contatti per cognome così i familiari compaiono insieme"
|
||||
},
|
||||
"downloads": {
|
||||
"title": "Download",
|
||||
@@ -2745,7 +2747,10 @@
|
||||
"any_month": "Qualsiasi mese",
|
||||
"has_email": "Con email",
|
||||
"has_phone": "Con telefono",
|
||||
"has_photo": "Con foto"
|
||||
"has_photo": "Con foto",
|
||||
"sort_by": "Ordina per",
|
||||
"sort_first_name": "Nome",
|
||||
"sort_last_name": "Cognome"
|
||||
},
|
||||
"open_categories": "Apri categorie"
|
||||
},
|
||||
|
||||
@@ -1901,7 +1901,9 @@
|
||||
"categories_description": "連絡先カテゴリの名前を変更",
|
||||
"no_categories": "カテゴリが見つかりません",
|
||||
"group_by_letter_description": "連絡先リストにアルファベット順のセクション見出しを表示",
|
||||
"group_by_letter_label": "頭文字でグループ化"
|
||||
"group_by_letter_label": "頭文字でグループ化",
|
||||
"sort_by_last_name_label": "姓で並べ替え",
|
||||
"sort_by_last_name_description": "家族が並んで表示されるように、連絡先一覧を姓の順に並べ替えます"
|
||||
},
|
||||
"downloads": {
|
||||
"title": "ダウンロード",
|
||||
@@ -2745,7 +2747,10 @@
|
||||
"any_month": "すべての月",
|
||||
"has_email": "メールあり",
|
||||
"has_phone": "電話あり",
|
||||
"has_photo": "写真あり"
|
||||
"has_photo": "写真あり",
|
||||
"sort_by": "並べ替え",
|
||||
"sort_first_name": "名",
|
||||
"sort_last_name": "姓"
|
||||
},
|
||||
"open_categories": "カテゴリを開く"
|
||||
},
|
||||
|
||||
@@ -1901,7 +1901,9 @@
|
||||
"categories_description": "연락처 카테고리 이름 변경",
|
||||
"no_categories": "카테고리를 찾을 수 없음",
|
||||
"group_by_letter_description": "연락처 목록에 알파벳순 섹션 헤더 표시",
|
||||
"group_by_letter_label": "첫 글자로 그룹화"
|
||||
"group_by_letter_label": "첫 글자로 그룹화",
|
||||
"sort_by_last_name_label": "성으로 정렬",
|
||||
"sort_by_last_name_description": "가족 구성원이 함께 표시되도록 연락처 목록을 성 기준으로 정렬합니다"
|
||||
},
|
||||
"downloads": {
|
||||
"title": "다운로드",
|
||||
@@ -2745,7 +2747,10 @@
|
||||
"any_month": "모든 달",
|
||||
"has_email": "이메일 있음",
|
||||
"has_phone": "전화번호 있음",
|
||||
"has_photo": "사진 있음"
|
||||
"has_photo": "사진 있음",
|
||||
"sort_by": "정렬 기준",
|
||||
"sort_first_name": "이름",
|
||||
"sort_last_name": "성"
|
||||
},
|
||||
"open_categories": "카테고리 열기"
|
||||
},
|
||||
|
||||
@@ -1901,7 +1901,9 @@
|
||||
"categories_description": "Pārdēvēt kontaktu kategorijas",
|
||||
"no_categories": "Kategorijas nav atrastas",
|
||||
"group_by_letter_description": "Rādīt alfabētiskos sadaļu virsrakstus kontaktu sarakstā",
|
||||
"group_by_letter_label": "Grupēt pēc pirmā burta"
|
||||
"group_by_letter_label": "Grupēt pēc pirmā burta",
|
||||
"sort_by_last_name_label": "Kārtot pēc uzvārda",
|
||||
"sort_by_last_name_description": "Kārtot kontaktu sarakstu pēc uzvārda, lai ģimenes locekļi būtu kopā"
|
||||
},
|
||||
"downloads": {
|
||||
"title": "Lejupielādes",
|
||||
@@ -2745,7 +2747,10 @@
|
||||
"any_month": "Jebkurš mēnesis",
|
||||
"has_email": "Ar e-pastu",
|
||||
"has_phone": "Ar tālruni",
|
||||
"has_photo": "Ar foto"
|
||||
"has_photo": "Ar foto",
|
||||
"sort_by": "Kārtot pēc",
|
||||
"sort_first_name": "Vārds",
|
||||
"sort_last_name": "Uzvārds"
|
||||
},
|
||||
"open_categories": "Atvērt kategorijas"
|
||||
},
|
||||
|
||||
@@ -1898,6 +1898,8 @@
|
||||
"title": "Харилцах утас",
|
||||
"description": "Харилцагчаа импортлох, экспортлох",
|
||||
"group_by_letter_label": "Эхний үсгээр бүлэглэх",
|
||||
"sort_by_last_name_label": "Овгоор эрэмбэлэх",
|
||||
"sort_by_last_name_description": "Гэр бүлийн гишүүд хамт харагдахаар харилцах хүмүүсийн жагсаалтыг овгоор эрэмбэлэх",
|
||||
"group_by_letter_description": "Харилцах хүмүүсийн жагсаалтад цагаан толгойн үсгийн дарааллаар хэсгийн толгойг харуулах",
|
||||
"import_label": "Харилцагчдыг импортлох",
|
||||
"import_description": "vCard (.vcf) файлаас харилцагчдыг импортлох",
|
||||
@@ -2746,7 +2748,10 @@
|
||||
"any_month": "Ямар ч сар",
|
||||
"has_email": "Имэйл хаягтай",
|
||||
"has_phone": "Утастай",
|
||||
"has_photo": "Зурагтай"
|
||||
"has_photo": "Зурагтай",
|
||||
"sort_by": "Эрэмбэлэх",
|
||||
"sort_first_name": "Нэр",
|
||||
"sort_last_name": "Овог"
|
||||
}
|
||||
},
|
||||
"calendar": {
|
||||
|
||||
@@ -1898,6 +1898,8 @@
|
||||
"title": "Kontakter",
|
||||
"description": "Importer og eksporter kontaktene dine",
|
||||
"group_by_letter_label": "Grupper etter første bokstav",
|
||||
"sort_by_last_name_label": "Sorter etter etternavn",
|
||||
"sort_by_last_name_description": "Sorter kontaktlisten etter etternavn slik at familiemedlemmer står samlet",
|
||||
"group_by_letter_description": "Vis alfabetiske overskrifter i kontaktlisten",
|
||||
"import_label": "Importer kontakter",
|
||||
"import_description": "Importer kontakter fra en vCard-fil (.vcf).",
|
||||
@@ -2746,7 +2748,10 @@
|
||||
"any_month": "Alle måneder",
|
||||
"has_email": "Har e-postadresse",
|
||||
"has_phone": "Har telefonnummer",
|
||||
"has_photo": "Har bilde"
|
||||
"has_photo": "Har bilde",
|
||||
"sort_by": "Sorter etter",
|
||||
"sort_first_name": "Fornavn",
|
||||
"sort_last_name": "Etternavn"
|
||||
}
|
||||
},
|
||||
"calendar": {
|
||||
|
||||
@@ -1901,7 +1901,9 @@
|
||||
"categories_description": "Contactcategorieën hernoemen",
|
||||
"no_categories": "Geen categorieën gevonden",
|
||||
"group_by_letter_description": "Toon alfabetische sectiekoppen in de contactenlijst",
|
||||
"group_by_letter_label": "Groeperen op eerste letter"
|
||||
"group_by_letter_label": "Groeperen op eerste letter",
|
||||
"sort_by_last_name_label": "Sorteren op achternaam",
|
||||
"sort_by_last_name_description": "Sorteer de contactenlijst op achternaam zodat gezinsleden bij elkaar staan"
|
||||
},
|
||||
"downloads": {
|
||||
"title": "Downloads",
|
||||
@@ -2745,7 +2747,10 @@
|
||||
"any_month": "Elke maand",
|
||||
"has_email": "Met e-mail",
|
||||
"has_phone": "Met telefoon",
|
||||
"has_photo": "Met foto"
|
||||
"has_photo": "Met foto",
|
||||
"sort_by": "Sorteren op",
|
||||
"sort_first_name": "Voornaam",
|
||||
"sort_last_name": "Achternaam"
|
||||
},
|
||||
"open_categories": "Categorieën openen"
|
||||
},
|
||||
|
||||
@@ -1901,7 +1901,9 @@
|
||||
"categories_description": "Zmień nazwy kategorii kontaktów",
|
||||
"no_categories": "Nie znaleziono kategorii",
|
||||
"group_by_letter_description": "Pokaż alfabetyczne nagłówki sekcji na liście kontaktów",
|
||||
"group_by_letter_label": "Grupuj według pierwszej litery"
|
||||
"group_by_letter_label": "Grupuj według pierwszej litery",
|
||||
"sort_by_last_name_label": "Sortuj według nazwiska",
|
||||
"sort_by_last_name_description": "Uporządkuj listę kontaktów według nazwiska, aby członkowie rodziny byli obok siebie"
|
||||
},
|
||||
"downloads": {
|
||||
"title": "Pobrane",
|
||||
@@ -2745,7 +2747,10 @@
|
||||
"any_month": "Dowolny miesiąc",
|
||||
"has_email": "Z e-mailem",
|
||||
"has_phone": "Z telefonem",
|
||||
"has_photo": "Ze zdjęciem"
|
||||
"has_photo": "Ze zdjęciem",
|
||||
"sort_by": "Sortuj według",
|
||||
"sort_first_name": "Imię",
|
||||
"sort_last_name": "Nazwisko"
|
||||
},
|
||||
"open_categories": "Otwórz kategorie"
|
||||
},
|
||||
|
||||
@@ -1901,7 +1901,9 @@
|
||||
"categories_description": "Renomear categorias de contatos",
|
||||
"no_categories": "Nenhuma categoria encontrada",
|
||||
"group_by_letter_description": "Mostrar cabeçalhos de seção alfabéticos na lista de contatos",
|
||||
"group_by_letter_label": "Agrupar pela primeira letra"
|
||||
"group_by_letter_label": "Agrupar pela primeira letra",
|
||||
"sort_by_last_name_label": "Ordenar por sobrenome",
|
||||
"sort_by_last_name_description": "Ordenar a lista de contatos por sobrenome para que os membros da família apareçam juntos"
|
||||
},
|
||||
"downloads": {
|
||||
"title": "Downloads",
|
||||
@@ -2745,7 +2747,10 @@
|
||||
"any_month": "Qualquer mês",
|
||||
"has_email": "Com e-mail",
|
||||
"has_phone": "Com telefone",
|
||||
"has_photo": "Com foto"
|
||||
"has_photo": "Com foto",
|
||||
"sort_by": "Ordenar por",
|
||||
"sort_first_name": "Nome",
|
||||
"sort_last_name": "Sobrenome"
|
||||
},
|
||||
"open_categories": "Abrir categorias"
|
||||
},
|
||||
|
||||
@@ -1898,6 +1898,8 @@
|
||||
"title": "Contacte",
|
||||
"description": "Importați și exportați contactele",
|
||||
"group_by_letter_label": "Grupați după prima literă",
|
||||
"sort_by_last_name_label": "Sortare după nume de familie",
|
||||
"sort_by_last_name_description": "Ordonați lista de contacte după numele de familie, astfel încât membrii familiei să apară împreună",
|
||||
"group_by_letter_description": "Afișați titlurile secțiunilor în ordine alfabetică în lista de contacte",
|
||||
"import_label": "Importați contactele",
|
||||
"import_description": "Importați contactele dintr-un fișier „vCard” (.vcf)",
|
||||
@@ -2746,7 +2748,10 @@
|
||||
"any_month": "Orice lună",
|
||||
"has_email": "Are e-mail",
|
||||
"has_phone": "Are telefon",
|
||||
"has_photo": "Are fotografie"
|
||||
"has_photo": "Are fotografie",
|
||||
"sort_by": "Sortare după",
|
||||
"sort_first_name": "Prenume",
|
||||
"sort_last_name": "Nume de familie"
|
||||
}
|
||||
},
|
||||
"calendar": {
|
||||
|
||||
@@ -1901,7 +1901,9 @@
|
||||
"categories_description": "Переименование категорий контактов",
|
||||
"no_categories": "Категории не найдены",
|
||||
"group_by_letter_description": "Показывать алфавитные заголовки разделов в списке контактов",
|
||||
"group_by_letter_label": "Группировать по первой букве"
|
||||
"group_by_letter_label": "Группировать по первой букве",
|
||||
"sort_by_last_name_label": "Сортировать по фамилии",
|
||||
"sort_by_last_name_description": "Упорядочить список контактов по фамилии, чтобы члены семьи шли подряд"
|
||||
},
|
||||
"downloads": {
|
||||
"title": "Загрузки",
|
||||
@@ -2745,7 +2747,10 @@
|
||||
"any_month": "Любой месяц",
|
||||
"has_email": "С эл. почтой",
|
||||
"has_phone": "С телефоном",
|
||||
"has_photo": "С фото"
|
||||
"has_photo": "С фото",
|
||||
"sort_by": "Сортировать по",
|
||||
"sort_first_name": "Имя",
|
||||
"sort_last_name": "Фамилия"
|
||||
},
|
||||
"open_categories": "Открыть категории"
|
||||
},
|
||||
|
||||
@@ -1898,6 +1898,8 @@
|
||||
"title": "Kontakty",
|
||||
"description": "Import a export kontaktov",
|
||||
"group_by_letter_label": "Zoskupiť podľa prvého písmena",
|
||||
"sort_by_last_name_label": "Zoradiť podľa priezviska",
|
||||
"sort_by_last_name_description": "Zoradiť zoznam kontaktov podľa priezviska, aby členovia rodiny boli pri sebe",
|
||||
"group_by_letter_description": "Zobraziť abecedné záhlavia sekcií v zozname kontaktov",
|
||||
"import_label": "Importovať kontakty",
|
||||
"import_description": "Importovať kontakty zo súboru vCard (.vcf)",
|
||||
@@ -2746,7 +2748,10 @@
|
||||
"any_month": "Ktorýkoľvek mesiac",
|
||||
"has_email": "Má e-mail",
|
||||
"has_phone": "Má telefón",
|
||||
"has_photo": "Má fotku"
|
||||
"has_photo": "Má fotku",
|
||||
"sort_by": "Zoradiť podľa",
|
||||
"sort_first_name": "Meno",
|
||||
"sort_last_name": "Priezvisko"
|
||||
}
|
||||
},
|
||||
"calendar": {
|
||||
|
||||
@@ -1891,6 +1891,8 @@
|
||||
"title": "Kişiler",
|
||||
"description": "Kişilerinizi içe ve dışa aktarın",
|
||||
"group_by_letter_label": "İlk harfe göre grupla",
|
||||
"sort_by_last_name_label": "Soyada göre sırala",
|
||||
"sort_by_last_name_description": "Aile üyeleri bir arada görünsün diye kişi listesini soyada göre sıralar",
|
||||
"group_by_letter_description": "Kişi listesinde alfabetik bölüm başlıkları göster",
|
||||
"import_label": "Kişileri İçe Aktar",
|
||||
"import_description": "Kişileri bir vCard (.vcf) dosyasından içe aktarın",
|
||||
@@ -2745,7 +2747,10 @@
|
||||
"any_month": "Herhangi bir ay",
|
||||
"has_email": "E-postası var",
|
||||
"has_phone": "Telefonu var",
|
||||
"has_photo": "Fotoğrafı var"
|
||||
"has_photo": "Fotoğrafı var",
|
||||
"sort_by": "Sıralama",
|
||||
"sort_first_name": "Ad",
|
||||
"sort_last_name": "Soyad"
|
||||
},
|
||||
"open_categories": "Kategorileri aç"
|
||||
},
|
||||
|
||||
@@ -1901,7 +1901,9 @@
|
||||
"categories_description": "Перейменувати категорії контактів",
|
||||
"no_categories": "Категорії не знайдено",
|
||||
"group_by_letter_description": "Показувати алфавітні заголовки розділів у списку контактів",
|
||||
"group_by_letter_label": "Групувати за першою літерою"
|
||||
"group_by_letter_label": "Групувати за першою літерою",
|
||||
"sort_by_last_name_label": "Сортувати за прізвищем",
|
||||
"sort_by_last_name_description": "Упорядкувати список контактів за прізвищем, щоб члени родини були поруч"
|
||||
},
|
||||
"downloads": {
|
||||
"title": "Завантаження",
|
||||
@@ -2745,7 +2747,10 @@
|
||||
"any_month": "Будь-який місяць",
|
||||
"has_email": "З ел. поштою",
|
||||
"has_phone": "З телефоном",
|
||||
"has_photo": "З фото"
|
||||
"has_photo": "З фото",
|
||||
"sort_by": "Сортувати за",
|
||||
"sort_first_name": "Ім'я",
|
||||
"sort_last_name": "Прізвище"
|
||||
},
|
||||
"open_categories": "Відкрити категорії"
|
||||
},
|
||||
|
||||
@@ -1898,6 +1898,8 @@
|
||||
"title": "聯絡人",
|
||||
"description": "匯入與匯出聯絡人",
|
||||
"group_by_letter_label": "依首字分組",
|
||||
"sort_by_last_name_label": "依姓氏排序",
|
||||
"sort_by_last_name_description": "依姓氏排列聯絡人清單,讓家庭成員顯示在一起",
|
||||
"group_by_letter_description": "在聯絡人清單中顯示依字母排列的分節標題",
|
||||
"import_label": "匯入聯絡人",
|
||||
"import_description": "從 vCard(.vcf)檔案匯入聯絡人",
|
||||
@@ -2746,7 +2748,10 @@
|
||||
"any_month": "任何月份",
|
||||
"has_email": "有電子郵件地址",
|
||||
"has_phone": "有電話",
|
||||
"has_photo": "有照片"
|
||||
"has_photo": "有照片",
|
||||
"sort_by": "排序方式",
|
||||
"sort_first_name": "名",
|
||||
"sort_last_name": "姓"
|
||||
}
|
||||
},
|
||||
"calendar": {
|
||||
|
||||
@@ -1901,7 +1901,9 @@
|
||||
"categories_description": "重命名联系人类别",
|
||||
"no_categories": "未找到类别",
|
||||
"group_by_letter_description": "在联系人列表中显示按字母顺序排列的分节标题",
|
||||
"group_by_letter_label": "按首字母分组"
|
||||
"group_by_letter_label": "按首字母分组",
|
||||
"sort_by_last_name_label": "按姓氏排序",
|
||||
"sort_by_last_name_description": "按姓氏排列联系人列表,使家庭成员显示在一起"
|
||||
},
|
||||
"downloads": {
|
||||
"title": "下载",
|
||||
@@ -2745,7 +2747,10 @@
|
||||
"any_month": "任意月份",
|
||||
"has_email": "有邮箱",
|
||||
"has_phone": "有电话",
|
||||
"has_photo": "有照片"
|
||||
"has_photo": "有照片",
|
||||
"sort_by": "排序方式",
|
||||
"sort_first_name": "名",
|
||||
"sort_last_name": "姓"
|
||||
},
|
||||
"open_categories": "打开分类"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { getContactSortName } from '../contact-store';
|
||||
import type { ContactCard } from '@/lib/jmap/types';
|
||||
|
||||
const make = (overrides: Partial<ContactCard>): ContactCard => ({
|
||||
id: 'c1',
|
||||
addressBookIds: {},
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const structured = make({
|
||||
name: {
|
||||
components: [
|
||||
{ kind: 'given', value: 'Alice' },
|
||||
{ kind: 'middle', value: 'Jane' },
|
||||
{ kind: 'surname', value: 'Smith' },
|
||||
],
|
||||
isOrdered: true,
|
||||
},
|
||||
});
|
||||
|
||||
describe('getContactSortName (#963)', () => {
|
||||
it('returns the display name when not sorting by last name', () => {
|
||||
expect(getContactSortName(structured, false)).toBe('Alice Smith');
|
||||
});
|
||||
|
||||
it('leads with the surname when sorting by last name', () => {
|
||||
expect(getContactSortName(structured, true)).toBe('Smith, Alice Jane');
|
||||
});
|
||||
|
||||
it('returns just the surname when no given name exists', () => {
|
||||
const c = make({ name: { components: [{ kind: 'surname', value: 'Smith' }], isOrdered: true } });
|
||||
expect(getContactSortName(c, true)).toBe('Smith');
|
||||
});
|
||||
|
||||
it('uses the last word of name.full when there are no components', () => {
|
||||
const c = make({ name: { full: 'Jean Pierre Dupont' } });
|
||||
expect(getContactSortName(c, true)).toBe('Dupont, Jean Pierre');
|
||||
});
|
||||
|
||||
it('keeps a single-word name.full as-is', () => {
|
||||
const c = make({ name: { full: 'Madonna' } });
|
||||
expect(getContactSortName(c, true)).toBe('Madonna');
|
||||
});
|
||||
|
||||
it('does not split organization or email fallbacks into a surname', () => {
|
||||
const org = make({ organizations: { o1: { name: 'Acme Corp' } } });
|
||||
expect(getContactSortName(org, true)).toBe('Acme Corp');
|
||||
const mail = make({ emails: { e0: { address: 'someone@example.com' } } });
|
||||
expect(getContactSortName(mail, true)).toBe('someone@example.com');
|
||||
});
|
||||
|
||||
it('falls back to the display name for a given-only name (e.g. a group)', () => {
|
||||
const c = make({ kind: 'group', name: { components: [{ kind: 'given', value: 'Team' }], isOrdered: true } });
|
||||
expect(getContactSortName(c, true)).toBe('Team');
|
||||
});
|
||||
});
|
||||
@@ -149,6 +149,34 @@ export function getContactDisplayName(contact: ContactCard): string {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Name used to order (and letter-group) the contact list. With `byLastName`
|
||||
// the surname leads ("Smith, Alice") so family members sit together (#963).
|
||||
// Contacts without a structured surname fall back to the last word of
|
||||
// `name.full`; everything else (nickname, org, email) keeps the display name.
|
||||
export function getContactSortName(contact: ContactCard, byLastName: boolean): string {
|
||||
const display = getContactDisplayName(contact);
|
||||
if (!byLastName) return display;
|
||||
const components = contact.name?.components;
|
||||
if (components && components.length > 0) {
|
||||
const pick = (...kinds: string[]) =>
|
||||
components.filter(c => kinds.includes(c.kind) && c.value).map(c => c.value).join(' ');
|
||||
const surname = pick('surname', 'surname2');
|
||||
if (surname) {
|
||||
const rest = pick('given', 'given2', 'middle', 'additional');
|
||||
return rest ? `${surname}, ${rest}` : surname;
|
||||
}
|
||||
}
|
||||
const full = contact.name?.full;
|
||||
if (full && display === full) {
|
||||
const words = full.trim().split(/\s+/);
|
||||
if (words.length > 1) {
|
||||
const last = words[words.length - 1];
|
||||
return `${last}, ${words.slice(0, -1).join(' ')}`;
|
||||
}
|
||||
}
|
||||
return display;
|
||||
}
|
||||
|
||||
export function getContactPrimaryEmail(contact: ContactCard): string {
|
||||
if (!contact.emails) return '';
|
||||
return Object.values(contact.emails)[0]?.address || '';
|
||||
|
||||
@@ -388,6 +388,9 @@ interface SettingsState {
|
||||
|
||||
// Contacts Display
|
||||
groupContactsByLetter: boolean;
|
||||
// Sort (and group) the contact list by surname instead of given name so
|
||||
// family members sit together (#963).
|
||||
sortContactsByLastName: boolean;
|
||||
|
||||
// Email Notifications
|
||||
emailNotificationsEnabled: boolean;
|
||||
@@ -620,6 +623,7 @@ const DEFAULT_SETTINGS = {
|
||||
|
||||
// Contacts Display
|
||||
groupContactsByLetter: true,
|
||||
sortContactsByLastName: false,
|
||||
|
||||
// Email Notifications
|
||||
emailNotificationsEnabled: true,
|
||||
@@ -835,6 +839,7 @@ export const useSettingsStore = create<SettingsState>()(
|
||||
birthdayCalendarColor: state.birthdayCalendarColor,
|
||||
sharedCalendarColors: state.sharedCalendarColors,
|
||||
groupContactsByLetter: state.groupContactsByLetter,
|
||||
sortContactsByLastName: state.sortContactsByLastName,
|
||||
expandedFilterView: state.expandedFilterView,
|
||||
showTimeInMonthView: state.showTimeInMonthView,
|
||||
showWeekNumbers: state.showWeekNumbers,
|
||||
|
||||
Reference in New Issue
Block a user