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 { spacing, radius, typography, componentSizes, type ThemePalette } from '../../theme/tokens';
|
||||||
import { useColors } from '../../theme/colors';
|
import { useColors } from '../../theme/colors';
|
||||||
import type { AddressBook } from '../../api/types';
|
import type { AddressBook } from '../../api/types';
|
||||||
|
import { addressBookLabel } from '../../lib/address-book-label';
|
||||||
|
|
||||||
export function ContactsSettings() {
|
export function ContactsSettings() {
|
||||||
const c = useColors();
|
const c = useColors();
|
||||||
@@ -261,7 +262,7 @@ export function ContactsSettings() {
|
|||||||
<>
|
<>
|
||||||
<View style={{ flex: 1, minWidth: 0 }}>
|
<View style={{ flex: 1, minWidth: 0 }}>
|
||||||
<Text style={styles.bookName} numberOfLines={1}>
|
<Text style={styles.bookName} numberOfLines={1}>
|
||||||
{book.isShared && book.accountName ? `${book.name} (${book.accountName})` : book.name}
|
{addressBookLabel(book)}
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={styles.bookCount}>
|
<Text style={styles.bookCount}>
|
||||||
{t('settings.contacts.book_count', '{count, plural, one {# contact} other {# contacts}}', { count: book.count })}
|
{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 { describeSyncStatus, type StatusAction } from './status';
|
||||||
import { useAndroidSync } from './use-android-sync';
|
import { useAndroidSync } from './use-android-sync';
|
||||||
import { useSyncCollections } from './use-sync-collections';
|
import { useSyncCollections } from './use-sync-collections';
|
||||||
|
import { nameWithAccount } from '../../../lib/address-book-label';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
account: AccountEntry;
|
account: AccountEntry;
|
||||||
@@ -280,7 +281,7 @@ export function DeviceSyncAccountRow({ account, authority }: Props) {
|
|||||||
{ value: '', label: t('settings.device_sync.new_contacts_default', 'Default address book') },
|
{ value: '', label: t('settings.device_sync.new_contacts_default', 'Default address book') },
|
||||||
...writableSelected.map((col) => ({
|
...writableSelected.map((col) => ({
|
||||||
value: col.key,
|
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)
|
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);
|
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 () => {
|
it('deletes rows of deselected calendars and never syncs the birthday calendar', async () => {
|
||||||
const h = await new Harness().setup(['b', 'w']);
|
const h = await new Harness().setup(['b', 'w']);
|
||||||
const local = await h.calendars();
|
const local = await h.calendars();
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import type { CalendarLike } from '../wire';
|
|||||||
import { collectionKey, parseCollectionKey } from '../common/ids';
|
import { collectionKey, parseCollectionKey } from '../common/ids';
|
||||||
import { deepEqual, parseJsonColumn } from '../common/json';
|
import { deepEqual, parseJsonColumn } from '../common/json';
|
||||||
import { BIRTHDAY_CALENDAR_ID } from '../../lib/birthday-calendar';
|
import { BIRTHDAY_CALENDAR_ID } from '../../lib/birthday-calendar';
|
||||||
|
import { nameWithAccount } from '../../lib/address-book-label';
|
||||||
import { colors } from '../../theme/tokens';
|
import { colors } from '../../theme/tokens';
|
||||||
import { isSelfAddress } from './attendees';
|
import { isSelfAddress } from './attendees';
|
||||||
import { CAN_PARTIALLY_UPDATE } from './columns';
|
import { CAN_PARTIALLY_UPDATE } from './columns';
|
||||||
@@ -80,7 +81,7 @@ export function calendarValues(selected: SelectedCalendar, ctx: CalendarsContext
|
|||||||
return {
|
return {
|
||||||
[Calendars._SYNC_ID]: collectionKey(selected.jmapAccountId, calendar.id),
|
[Calendars._SYNC_ID]: collectionKey(selected.jmapAccountId, calendar.id),
|
||||||
[Calendars.NAME]: name,
|
[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_COLOR]: cssColorToArgb(calendar.color) ?? cssColorToArgb(paletteColor(calendar.id))!,
|
||||||
[Calendars.CALENDAR_ACCESS_LEVEL]: access.level,
|
[Calendars.CALENDAR_ACCESS_LEVEL]: access.level,
|
||||||
[Calendars.OWNER_ACCOUNT]: ctx.ownerAccount,
|
[Calendars.OWNER_ACCOUNT]: ctx.ownerAccount,
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import path from 'node:path';
|
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', () => {
|
describe('addressBookLabel', () => {
|
||||||
it('names a shared book once when Stalwart already put the account in its name', () => {
|
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');
|
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', () => {
|
it('is what the contact form shows', () => {
|
||||||
const form = fs.readFileSync(path.resolve(__dirname, '../../screens/ContactFormScreen.tsx'), 'utf8');
|
const form = fs.readFileSync(path.resolve(__dirname, '../../screens/ContactFormScreen.tsx'), 'utf8');
|
||||||
expect(form).toMatch(/\{addressBookLabel\(book\)\}/);
|
expect(form).toMatch(/\{addressBookLabel\(book\)\}/);
|
||||||
|
|||||||
@@ -1,13 +1,21 @@
|
|||||||
import type { AddressBook } from '../api/types';
|
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
|
* 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
|
* it comes from, unless its name already says so. Webmail contact-form.tsx.
|
||||||
* book "Stalwart Address Book (team@example.org)", which came out as
|
|
||||||
* "... (team@example.org) (team@example.org)". Webmail contact-form.tsx.
|
|
||||||
*/
|
*/
|
||||||
export function addressBookLabel(book: Pick<AddressBook, 'name' | 'isShared' | 'accountName'>): string {
|
export function addressBookLabel(book: Pick<AddressBook, 'name' | 'isShared' | 'accountName'>): string {
|
||||||
const name = book.name ?? '';
|
const name = book.name ?? '';
|
||||||
if (!book.isShared || !book.accountName || name.includes(book.accountName)) return name;
|
return book.isShared ? nameWithAccount(name, book.accountName) : name;
|
||||||
return `${name} (${book.accountName})`;
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user