mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-01 16:05:39 +00:00
fix: name a shared address book or calendar once when its name already has the account
This commit is contained in:
@@ -22,6 +22,7 @@ import { CONTACTS_AUTHORITY } from '../../device-sync/types';
|
||||
import { spacing, radius, typography, componentSizes, type ThemePalette } from '../../theme/tokens';
|
||||
import { useColors } from '../../theme/colors';
|
||||
import type { AddressBook } from '../../api/types';
|
||||
import { addressBookLabel } from '../../lib/address-book-label';
|
||||
|
||||
export function ContactsSettings() {
|
||||
const c = useColors();
|
||||
@@ -261,7 +262,7 @@ export function ContactsSettings() {
|
||||
<>
|
||||
<View style={{ flex: 1, minWidth: 0 }}>
|
||||
<Text style={styles.bookName} numberOfLines={1}>
|
||||
{book.isShared && book.accountName ? `${book.name} (${book.accountName})` : book.name}
|
||||
{addressBookLabel(book)}
|
||||
</Text>
|
||||
<Text style={styles.bookCount}>
|
||||
{t('settings.contacts.book_count', '{count, plural, one {# contact} other {# contacts}}', { count: book.count })}
|
||||
|
||||
@@ -40,6 +40,7 @@ import { DeviceSyncCollectionsSheet } from './DeviceSyncCollectionsSheet';
|
||||
import { describeSyncStatus, type StatusAction } from './status';
|
||||
import { useAndroidSync } from './use-android-sync';
|
||||
import { useSyncCollections } from './use-sync-collections';
|
||||
import { nameWithAccount } from '../../../lib/address-book-label';
|
||||
|
||||
interface Props {
|
||||
account: AccountEntry;
|
||||
@@ -280,7 +281,7 @@ export function DeviceSyncAccountRow({ account, authority }: Props) {
|
||||
{ value: '', label: t('settings.device_sync.new_contacts_default', 'Default address book') },
|
||||
...writableSelected.map((col) => ({
|
||||
value: col.key,
|
||||
label: col.isPersonal ? col.name : `${col.name} (${col.accountName})`,
|
||||
label: col.isPersonal ? col.name : nameWithAccount(col.name, col.accountName),
|
||||
})),
|
||||
];
|
||||
const newContactsValue = entry.newContactsAddressBook && newContactsOptions.some((o) => o.value === entry.newContactsAddressBook)
|
||||
|
||||
@@ -94,6 +94,16 @@ describe('calendar planner: calendar rows', () => {
|
||||
expect(values[Calendars.OWNER_ACCOUNT]).toBe(ME);
|
||||
});
|
||||
|
||||
it('names a shared calendar once when Stalwart already put the account in its name', () => {
|
||||
const plan = calendarPlanner.planCalendars(
|
||||
[{ jmapAccountId: 'd', calendar: { id: 'x', name: 'Stalwart Calendar (team@example.org)', myRights: { mayWriteAll: true } } as CalendarLike, readOnly: false, accountName: 'team@example.org' }],
|
||||
[],
|
||||
new Harness().ctx,
|
||||
);
|
||||
const values = (plan.groups[0].ops[0] as { values: Record<string, unknown> }).values;
|
||||
expect(values[Calendars.CALENDAR_DISPLAY_NAME]).toBe('Stalwart Calendar (team@example.org)');
|
||||
});
|
||||
|
||||
it('deletes rows of deselected calendars and never syncs the birthday calendar', async () => {
|
||||
const h = await new Harness().setup(['b', 'w']);
|
||||
const local = await h.calendars();
|
||||
|
||||
@@ -17,6 +17,7 @@ import type { CalendarLike } from '../wire';
|
||||
import { collectionKey, parseCollectionKey } from '../common/ids';
|
||||
import { deepEqual, parseJsonColumn } from '../common/json';
|
||||
import { BIRTHDAY_CALENDAR_ID } from '../../lib/birthday-calendar';
|
||||
import { nameWithAccount } from '../../lib/address-book-label';
|
||||
import { colors } from '../../theme/tokens';
|
||||
import { isSelfAddress } from './attendees';
|
||||
import { CAN_PARTIALLY_UPDATE } from './columns';
|
||||
@@ -80,7 +81,7 @@ export function calendarValues(selected: SelectedCalendar, ctx: CalendarsContext
|
||||
return {
|
||||
[Calendars._SYNC_ID]: collectionKey(selected.jmapAccountId, calendar.id),
|
||||
[Calendars.NAME]: name,
|
||||
[Calendars.CALENDAR_DISPLAY_NAME]: shared ? `${name} (${selected.accountName})` : name,
|
||||
[Calendars.CALENDAR_DISPLAY_NAME]: shared ? nameWithAccount(name, selected.accountName) : name,
|
||||
[Calendars.CALENDAR_COLOR]: cssColorToArgb(calendar.color) ?? cssColorToArgb(paletteColor(calendar.id))!,
|
||||
[Calendars.CALENDAR_ACCESS_LEVEL]: access.level,
|
||||
[Calendars.OWNER_ACCOUNT]: ctx.ownerAccount,
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { addressBookLabel } from '../address-book-label';
|
||||
import { addressBookLabel, nameWithAccount } from '../address-book-label';
|
||||
|
||||
describe('nameWithAccount', () => {
|
||||
it('adds the account once', () => {
|
||||
expect(nameWithAccount('Stalwart Calendar (team@example.org)', 'team@example.org')).toBe('Stalwart Calendar (team@example.org)');
|
||||
expect(nameWithAccount('Team', 'team@example.org')).toBe('Team (team@example.org)');
|
||||
expect(nameWithAccount('Team', undefined)).toBe('Team');
|
||||
});
|
||||
});
|
||||
|
||||
describe('addressBookLabel', () => {
|
||||
it('names a shared book once when Stalwart already put the account in its name', () => {
|
||||
@@ -15,6 +23,19 @@ describe('addressBookLabel', () => {
|
||||
expect(addressBookLabel({ name: 'Clients', isShared: false, accountName: 'usera@example.org' })).toBe('Clients');
|
||||
});
|
||||
|
||||
it('is what the contacts settings list shows', () => {
|
||||
// The shared book read "Stalwart Address Book (team@example.org) (team@example.org)".
|
||||
const settings = fs.readFileSync(path.resolve(__dirname, '../../components/settings/ContactsSettings.tsx'), 'utf8');
|
||||
expect(settings).toMatch(/\{addressBookLabel\(book\)\}/);
|
||||
expect(settings).not.toMatch(/\$\{book\.name\} \(\$\{book\.accountName\}\)/);
|
||||
});
|
||||
|
||||
it('is what the device sync address book picker shows', () => {
|
||||
const row = fs.readFileSync(path.resolve(__dirname, '../../components/settings/device-sync/DeviceSyncAccountRow.tsx'), 'utf8');
|
||||
expect(row).toMatch(/nameWithAccount\(col\.name, col\.accountName\)/);
|
||||
expect(row).not.toMatch(/\$\{col\.name\} \(\$\{col\.accountName\}\)/);
|
||||
});
|
||||
|
||||
it('is what the contact form shows', () => {
|
||||
const form = fs.readFileSync(path.resolve(__dirname, '../../screens/ContactFormScreen.tsx'), 'utf8');
|
||||
expect(form).toMatch(/\{addressBookLabel\(book\)\}/);
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
import type { AddressBook } from '../api/types';
|
||||
|
||||
/**
|
||||
* A shared collection's name with the account it comes from, unless the name
|
||||
* already says so. Stalwart names a group's default address book
|
||||
* "Stalwart Address Book (team@example.org)" and its default calendar the
|
||||
* same way, which came out as "... (team@example.org) (team@example.org)".
|
||||
*/
|
||||
export function nameWithAccount(name: string, accountName: string | null | undefined): string {
|
||||
if (!accountName || name.includes(accountName)) return name;
|
||||
return `${name} (${accountName})`;
|
||||
}
|
||||
|
||||
/**
|
||||
* How an address book is named in a picker: a shared one with the account
|
||||
* it comes from, unless its name already says so. Stalwart names a default
|
||||
* book "Stalwart Address Book (team@example.org)", which came out as
|
||||
* "... (team@example.org) (team@example.org)". Webmail contact-form.tsx.
|
||||
* it comes from, unless its name already says so. Webmail contact-form.tsx.
|
||||
*/
|
||||
export function addressBookLabel(book: Pick<AddressBook, 'name' | 'isShared' | 'accountName'>): string {
|
||||
const name = book.name ?? '';
|
||||
if (!book.isShared || !book.accountName || name.includes(book.accountName)) return name;
|
||||
return `${name} (${book.accountName})`;
|
||||
return book.isShared ? nameWithAccount(name, book.accountName) : name;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user