Add type for address

This commit is contained in:
Aleksejs Kovalovs 2026-03-12 17:21:41 +02:00
parent 434bbbc9c8
commit c58c5eb1fb
No known key found for this signature in database
GPG key ID: EBDE72B2FD0FD95E
5 changed files with 182 additions and 59 deletions

View file

@ -66,6 +66,54 @@ describe('AddressAdapter', () => {
};
expect(adapter.extract(contact)).toEqual([]);
});
it('supports useContactTypes correctly', () => {
const contact: GoogleContact = {
resourceName: 'p4',
addresses: [
createAddress({ type: 'Home', formattedValue: 'Home 1' }),
createAddress({ type: 'Home', formattedValue: 'Home 2' }),
createAddress({ type: 'Work', formattedValue: 'Work 1' }),
createAddress({ formattedValue: 'Other 1' }),
],
};
const results = adapter.extract(contact, { useContactTypes: true });
expect(results).toEqual([
{ value: 'Home 1', type: 'home', index: 0 },
{ value: 'Home 2', type: 'home', index: 1 },
{ value: 'Work 1', type: 'work', index: 0 },
{ value: 'Other 1', type: 'other', index: 0 },
]);
});
});
describe('Array Strategy', () => {
const contextWithArray = { namingStrategy: NamingStrategy.Array };
it('combines addresses into a single array extraction', () => {
const results = adapter.extract(mockContact, contextWithArray);
expect(results).toEqual([
{
value: [
'123 Main St\nSpringfield, USA 12345',
'456 Office Rd\nWorktown, UK AB1 2CD',
],
},
]);
});
it('returns single string value if only one address', () => {
const singleContact: GoogleContact = {
resourceName: 'p2',
addresses: [mockContact.addresses![0]!],
};
const results = adapter.extract(singleContact, contextWithArray);
expect(results).toEqual([
{
value: '123 Main St\nSpringfield, USA 12345',
},
]);
});
});
describe('VCF Strategy', () => {
@ -130,5 +178,30 @@ describe('AddressAdapter', () => {
[]
);
});
it('supports useContactTypes for VCF strategy', () => {
const typeContact: GoogleContact = {
resourceName: 'p6',
addresses: [
createAddress({
city: 'City 1',
type: 'Home',
}),
createAddress({
city: 'City 2',
type: 'Home',
}),
],
};
const results = adapter.extract(typeContact, {
namingStrategy: NamingStrategy.VCF,
useContactTypes: true,
});
expect(results).toEqual([
{ value: 'City 1', suffix: 'CITY', index: 0, type: 'home' },
{ value: 'City 2', suffix: 'CITY', index: 1, type: 'home' },
]);
});
});
});

View file

@ -14,58 +14,108 @@ export class AddressAdapter implements FieldAdapter {
const isVcfStrategy = context?.namingStrategy === NamingStrategy.VCF;
const typeCounts: Record<string, number> = {};
const processType = (typeStr?: string) => {
let normalizedType = (typeStr ?? 'other').toLowerCase().replace(/[^a-z0-9]/g, '');
if (!normalizedType) {
normalizedType = 'other';
}
const currentCount = typeCounts[normalizedType] ?? 0;
typeCounts[normalizedType] = currentCount + 1;
return { type: normalizedType, index: currentCount };
};
if (isVcfStrategy) {
// For VCF strategy, return subfields
const results: ExtractionResult[] = [];
addresses.forEach((addr, addrIndex) => {
// Add each subfield as a separate result
// All subfields of the same address get the same index
if (addr.streetAddress) {
results.push({
value: addr.streetAddress,
suffix: 'STREET',
index: addrIndex,
});
}
if (addr.city) {
results.push({ value: addr.city, suffix: 'CITY', index: addrIndex });
}
if (addr.country) {
results.push({
value: addr.country,
suffix: 'COUNTRY',
index: addrIndex,
});
}
if (addr.postalCode) {
results.push({
value: addr.postalCode,
suffix: 'POSTALCODE',
index: addrIndex,
});
}
if (addr.extendedAddress) {
results.push({
value: addr.extendedAddress,
suffix: 'EXTENDED',
index: addrIndex,
});
}
if (addr.formattedType) {
results.push({
value: addr.formattedType,
suffix: 'TYPE',
index: addrIndex,
});
const typeInfo = context.useContactTypes
? processType(addr.type)
: undefined;
const derivedIndex = typeInfo ? typeInfo.index : addrIndex;
const derivedType = typeInfo ? typeInfo.type : undefined;
const addResult = (subfield: string, suffix: string) => {
const result: ExtractionResult = {
value: subfield,
suffix,
index: derivedIndex,
};
if (derivedType !== undefined) {
result.type = derivedType;
}
results.push(result);
};
const fields = [
{ value: addr.streetAddress, suffix: 'STREET' },
{ value: addr.city, suffix: 'CITY' },
{ value: addr.country, suffix: 'COUNTRY' },
{ value: addr.postalCode, suffix: 'POSTALCODE' },
{ value: addr.extendedAddress, suffix: 'EXTENDED' },
{ value: addr.formattedType, suffix: 'TYPE' },
];
for (const field of fields) {
if (field.value) {
addResult(field.value, field.suffix);
}
}
});
return results;
} else {
// For Default strategy, return formattedValue as before
return addresses
.filter((item) => item.formattedValue)
.map((item) => ({ value: item.formattedValue }));
}
const validAddresses = addresses.filter((item) => item.formattedValue);
if (validAddresses.length === 0) {
return [];
}
if (context?.namingStrategy === NamingStrategy.Array) {
return this.extractForArrayStrategy(validAddresses);
}
if (context?.useContactTypes === true) {
return this.extractWithSemanticTypes(validAddresses);
}
return validAddresses.map((item) => ({ value: item.formattedValue }));
}
private extractForArrayStrategy(
validAddresses: { formattedValue: string; type?: string }[]
): ExtractionResult[] {
const first = validAddresses[0]?.formattedValue;
const allValues = validAddresses.map((item) => item.formattedValue);
return [
{
value: allValues.length === 1 && first ? first : allValues,
},
];
}
private extractWithSemanticTypes(
validAddresses: { formattedValue: string; type?: string }[]
): ExtractionResult[] {
const typeCounts: Record<string, number> = {};
const results: ExtractionResult[] = [];
for (const item of validAddresses) {
let normalizedType = (item.type ?? 'other').toLowerCase().replace(/[^a-z0-9]/g, '');
if (!normalizedType) {
normalizedType = 'other';
}
const currentCount = typeCounts[normalizedType] ?? 0;
typeCounts[normalizedType] = currentCount + 1;
results.push({
value: item.formattedValue,
type: normalizedType,
index: currentCount,
});
}
return results;
}
}

View file

@ -96,10 +96,10 @@ export const ru = {
'Skip nameless contacts': 'Пропускать контакты без имени',
'If enabled, contacts without a name (where the file would be named by ID) will be skipped during sync':
'Если включено, контакты без имени (у которых файл называется по ID) будут пропускаться при синхронизации',
'Use contact types for emails and phones':
'Использовать типы для e-mail и телефонов',
'If enabled, emails and phones will be formatted with their specific types (e.g. email_work, phone_mobile) instead of generic indexes. This is ignored in the Array naming strategy.':
'Если включено, электронные адреса и телефоны будут отформатированы с учетом их типов (например, email_work, phone_mobile) вместо индексов. Игнорируется в стратегии "Массив".',
'Use contact types for emails, phones and addresses':
'Использовать типы для e-mail, телефонов и адресов',
'If enabled, emails, phones and addresses will be formatted with their specific types (e.g. email_work, phone_mobile, address_home) instead of generic indexes. This is ignored in the Array naming strategy.':
'Если включено, электронные адреса, телефоны и адреса будут отформатированы с учетом их типов (например, email_work, phone_mobile, address_home) вместо индексов. Игнорируется в стратегии "Массив".',
};
/**
@ -200,10 +200,10 @@ export const lv = {
'Skip nameless contacts': 'Izlaist kontaktus bez vārda',
'If enabled, contacts without a name (where the file would be named by ID) will be skipped during sync':
'Ja iespējots, kontakti bez vārda (kuru fails tiktu nosaukts pēc ID) tiks izlaisti sinhronizācijas laikā',
'Use contact types for emails and phones':
'Izmantot e-pasta un tālruņa tipus',
'If enabled, emails and phones will be formatted with their specific types (e.g. email_work, phone_mobile) instead of generic indexes. This is ignored in the Array naming strategy.':
'Ja iespējots, e-pasti un tālruņi tiks formatēti ar to specifiskajiem tipiem (piem., email_work, phone_mobile), nevis vispārīgiem rādītājiem. Tas tiek ignorēts Array nosaukšanas stratēģijā.',
'Use contact types for emails, phones and addresses':
'Izmantot e-pasta, tālruņa un adrešu tipus',
'If enabled, emails, phones and addresses will be formatted with their specific types (e.g. email_work, phone_mobile, address_home) instead of generic indexes. This is ignored in the Array naming strategy.':
'Ja iespējots, e-pasti, tālruņi un adreses tiks formatēti ar to specifiskajiem tipiem (piem., email_work, phone_mobile, address_home), nevis vispārīgiem rādītājiem. Tas tiek ignorēts Array nosaukšanas stratēģijā.',
};
/**
@ -302,8 +302,8 @@ export const en = {
'Skip nameless contacts': 'Skip nameless contacts',
'If enabled, contacts without a name (where the file would be named by ID) will be skipped during sync':
'If enabled, contacts without a name (where the file would be named by ID) will be skipped during sync',
'Use contact types for emails and phones':
'Use contact types for emails and phones',
'If enabled, emails and phones will be formatted with their specific types (e.g. email_work, phone_mobile) instead of generic indexes. This is ignored in the Array naming strategy.':
'If enabled, emails and phones will be formatted with their specific types (e.g. email_work, phone_mobile) instead of generic indexes. This is ignored in the Array naming strategy.',
'Use contact types for emails, phones and addresses':
'Use contact types for emails, phones and addresses',
'If enabled, emails, phones and addresses will be formatted with their specific types (e.g. email_work, phone_mobile, address_home) instead of generic indexes. This is ignored in the Array naming strategy.':
'If enabled, emails, phones and addresses will be formatted with their specific types (e.g. email_work, phone_mobile, address_home) instead of generic indexes. This is ignored in the Array naming strategy.',
};

View file

@ -119,10 +119,10 @@ export class ContactSyncSettingTab extends PluginSettingTab {
);
new Setting(containerEl)
.setName(t('Use contact types for emails and phones'))
.setName(t('Use contact types for emails, phones and addresses'))
.setDesc(
t(
'If enabled, emails and phones will be formatted with their specific types (e.g. email_work, phone_mobile) instead of generic indexes. This is ignored in the Array naming strategy.'
'If enabled, emails, phones and addresses will be formatted with their specific types (e.g. email_work, phone_mobile, address_home) instead of generic indexes. This is ignored in the Array naming strategy.'
)
)
.addToggle((toggle) =>

View file

@ -62,7 +62,7 @@ export interface ContactSyncSettings {
/** Whether to skip contacts that have no name and would fall back to using their ID */
skipNamelessContacts: boolean;
/** Whether to use contact types for emails and phones instead of index */
/** Whether to use contact types for emails, phones and addresses instead of index */
useContactTypes: boolean;
}