mirror of
https://github.com/aleksejs1/obsidian-contact-sync-plugin.git
synced 2026-07-22 05:48:16 +00:00
parent
196ecf5221
commit
2718178d8d
13 changed files with 263 additions and 24 deletions
|
|
@ -24,6 +24,7 @@ Each contact becomes a separate note with YAML frontmatter for metadata and free
|
|||
- 🧩 Multiple naming strategies for frontmatter keys:
|
||||
- **Default**: Customizable prefix (e.g., `s_name`, `s_email_2`).
|
||||
- **VCF (vCard)**: Fully compatible with the [VCF Contacts](https://github.com/broekema41/obsidian-vcf-contacts) plugin.
|
||||
- **Array**: Stores multiple values (emails, phones, etc.) as a single array field in the frontmatter.
|
||||
|
||||
- 📇 Supports multiple names, emails, phone numbers, birthdays, addresses, organizations, job titles, department, labels, relations
|
||||
|
||||
|
|
|
|||
112
src/__tests__/core/strategies/ArrayNamingStrategy.test.ts
Normal file
112
src/__tests__/core/strategies/ArrayNamingStrategy.test.ts
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
import { createDefaultFormatter } from '../../../core/Formatter';
|
||||
import { NamingStrategy } from '../../../types/Settings';
|
||||
import { GoogleContact } from '../../../types/Contact';
|
||||
|
||||
describe('ArrayNamingStrategy Integration', () => {
|
||||
const formatter = createDefaultFormatter(NamingStrategy.Array);
|
||||
const contact: GoogleContact = {
|
||||
resourceName: 'people/123',
|
||||
emailAddresses: [
|
||||
{ value: 'test1@example.com' },
|
||||
{ value: 'test2@example.com' },
|
||||
],
|
||||
phoneNumbers: [{ value: '123456789' }, { value: '987654321' }],
|
||||
organizations: [
|
||||
{ name: 'Org1', title: 'Developer', department: 'Engineering' },
|
||||
{ name: 'Org2', title: 'Manager', department: 'Management' },
|
||||
],
|
||||
relations: [
|
||||
{ person: 'Jane', type: 'spouse' },
|
||||
{ person: 'Bob', type: 'friend' },
|
||||
],
|
||||
} as GoogleContact;
|
||||
|
||||
it('should format emails as an array', () => {
|
||||
const result = formatter.generateFrontmatter(contact, 'contact_');
|
||||
expect(result.contact_email).toEqual([
|
||||
'test1@example.com',
|
||||
'test2@example.com',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should format single email as string', () => {
|
||||
const singleContact = {
|
||||
...contact,
|
||||
emailAddresses: [{ value: 'single@example.com' }],
|
||||
} as GoogleContact;
|
||||
const result = formatter.generateFrontmatter(singleContact, 'contact_');
|
||||
expect(result.contact_email).toEqual('single@example.com');
|
||||
});
|
||||
|
||||
it('should format phones as an array', () => {
|
||||
const result = formatter.generateFrontmatter(contact, 'contact_');
|
||||
expect(result.contact_phone).toEqual(['123456789', '987654321']);
|
||||
});
|
||||
|
||||
it('should format single phone as string', () => {
|
||||
const singleContact = {
|
||||
...contact,
|
||||
phoneNumbers: [{ value: '111222333' }],
|
||||
} as GoogleContact;
|
||||
const result = formatter.generateFrontmatter(singleContact, 'contact_');
|
||||
expect(result.contact_phone).toEqual('111222333');
|
||||
});
|
||||
|
||||
it('should format organizations as an array', () => {
|
||||
const result = formatter.generateFrontmatter(contact, 'contact_');
|
||||
expect(result.contact_organization).toEqual(['Org1', 'Org2']);
|
||||
});
|
||||
|
||||
it('should format single organization as string', () => {
|
||||
const singleContact = {
|
||||
...contact,
|
||||
organizations: [{ name: 'SingleOrg', title: 'Dev', department: 'Dep' }],
|
||||
} as GoogleContact;
|
||||
const result = formatter.generateFrontmatter(singleContact, 'contact_');
|
||||
expect(result.contact_organization).toEqual('SingleOrg');
|
||||
});
|
||||
|
||||
it('should format titles as an array', () => {
|
||||
const result = formatter.generateFrontmatter(contact, 'contact_');
|
||||
expect(result.contact_jobtitle).toEqual(['Developer', 'Manager']);
|
||||
});
|
||||
|
||||
it('should format single title as string', () => {
|
||||
const singleContact = {
|
||||
...contact,
|
||||
organizations: [{ name: 'One', title: 'SingleTitle', department: 'Dep' }],
|
||||
} as GoogleContact;
|
||||
const result = formatter.generateFrontmatter(singleContact, 'contact_');
|
||||
expect(result.contact_jobtitle).toEqual('SingleTitle');
|
||||
});
|
||||
|
||||
it('should format departments as an array', () => {
|
||||
const result = formatter.generateFrontmatter(contact, 'contact_');
|
||||
expect(result.contact_department).toEqual(['Engineering', 'Management']);
|
||||
});
|
||||
|
||||
it('should format single department as string', () => {
|
||||
const singleContact = {
|
||||
...contact,
|
||||
organizations: [
|
||||
{ name: 'One', title: 'Title', department: 'SingleDept' },
|
||||
],
|
||||
} as GoogleContact;
|
||||
const result = formatter.generateFrontmatter(singleContact, 'contact_');
|
||||
expect(result.contact_department).toEqual('SingleDept');
|
||||
});
|
||||
|
||||
it('should format relations as an array', () => {
|
||||
const result = formatter.generateFrontmatter(contact, 'contact_');
|
||||
expect(result.contact_relations).toEqual(['Jane (spouse)', 'Bob (friend)']);
|
||||
});
|
||||
|
||||
it('should format single relation as string', () => {
|
||||
const singleContact = {
|
||||
...contact,
|
||||
relations: [{ person: 'SingleRel', type: 'partner' }],
|
||||
} as GoogleContact;
|
||||
const result = formatter.generateFrontmatter(singleContact, 'contact_');
|
||||
expect(result.contact_relations).toEqual('SingleRel (partner)');
|
||||
});
|
||||
});
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import { GoogleContact } from 'src/types/Contact';
|
||||
import { FieldAdapter, KeyNamingStrategy } from './interfaces';
|
||||
import { DefaultNamingStrategy } from './strategies/DefaultNamingStrategy';
|
||||
import { ArrayNamingStrategy } from './strategies/ArrayNamingStrategy';
|
||||
import { NameAdapter } from './adapters/NameAdapter';
|
||||
import { FormattedNameAdapter } from './adapters/FormattedNameAdapter';
|
||||
import { EmailAdapter } from './adapters/EmailAdapter';
|
||||
|
|
@ -44,7 +45,9 @@ export class Formatter {
|
|||
...context,
|
||||
namingStrategy: isVcfStrategy
|
||||
? NamingStrategy.VCF
|
||||
: NamingStrategy.Default,
|
||||
: this.strategy instanceof ArrayNamingStrategy
|
||||
? NamingStrategy.Array
|
||||
: NamingStrategy.Default,
|
||||
};
|
||||
|
||||
for (const [fieldId, adapter] of Object.entries(this.adapters)) {
|
||||
|
|
@ -85,6 +88,8 @@ export function createDefaultFormatter(
|
|||
|
||||
if (strategyType === NamingStrategy.VCF) {
|
||||
strategy = new VcfNamingStrategy();
|
||||
} else if (strategyType === NamingStrategy.Array) {
|
||||
strategy = new ArrayNamingStrategy();
|
||||
} else {
|
||||
strategy = new DefaultNamingStrategy();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,32 @@
|
|||
import { FieldAdapter, ExtractionResult } from '../interfaces';
|
||||
import { GoogleContact } from '../../types/Contact';
|
||||
import { NamingStrategy } from 'src/types/Settings';
|
||||
|
||||
export class DepartmentAdapter implements FieldAdapter {
|
||||
extract(contact: GoogleContact): ExtractionResult[] {
|
||||
return (contact.organizations ?? [])
|
||||
extract(
|
||||
contact: GoogleContact,
|
||||
context?: Record<string, unknown>
|
||||
): ExtractionResult[] {
|
||||
const departments = (contact.organizations ?? [])
|
||||
.filter((item) => item.department)
|
||||
.map((item) => ({ value: item.department }));
|
||||
.map((item) => item.department);
|
||||
|
||||
if (departments.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (context?.namingStrategy === NamingStrategy.Array) {
|
||||
const first = departments[0];
|
||||
return [
|
||||
{
|
||||
value:
|
||||
departments.length === 1 && first !== undefined
|
||||
? first
|
||||
: departments,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return departments.map((value) => ({ value }));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,27 @@
|
|||
import { FieldAdapter, ExtractionResult } from '../interfaces';
|
||||
import { GoogleContact } from '../../types/Contact';
|
||||
import { NamingStrategy } from 'src/types/Settings';
|
||||
|
||||
export class EmailAdapter implements FieldAdapter {
|
||||
extract(contact: GoogleContact): ExtractionResult[] {
|
||||
return (contact.emailAddresses ?? [])
|
||||
extract(
|
||||
contact: GoogleContact,
|
||||
context?: Record<string, unknown>
|
||||
): ExtractionResult[] {
|
||||
const emails = (contact.emailAddresses ?? [])
|
||||
.filter((item) => item.value)
|
||||
.map((item) => ({ value: item.value }));
|
||||
.map((item) => item.value);
|
||||
|
||||
if (emails.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (context?.namingStrategy === NamingStrategy.Array) {
|
||||
const first = emails[0];
|
||||
return [
|
||||
{ value: emails.length === 1 && first !== undefined ? first : emails },
|
||||
];
|
||||
}
|
||||
|
||||
return emails.map((value) => ({ value }));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,27 @@
|
|||
import { FieldAdapter, ExtractionResult } from '../interfaces';
|
||||
import { GoogleContact } from '../../types/Contact';
|
||||
import { NamingStrategy } from 'src/types/Settings';
|
||||
|
||||
export class JobTitleAdapter implements FieldAdapter {
|
||||
extract(contact: GoogleContact): ExtractionResult[] {
|
||||
return (contact.organizations ?? [])
|
||||
extract(
|
||||
contact: GoogleContact,
|
||||
context?: Record<string, unknown>
|
||||
): ExtractionResult[] {
|
||||
const titles = (contact.organizations ?? [])
|
||||
.filter((item) => item.title)
|
||||
.map((item) => ({ value: item.title }));
|
||||
.map((item) => item.title);
|
||||
|
||||
if (titles.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (context?.namingStrategy === NamingStrategy.Array) {
|
||||
const first = titles[0];
|
||||
return [
|
||||
{ value: titles.length === 1 && first !== undefined ? first : titles },
|
||||
];
|
||||
}
|
||||
|
||||
return titles.map((value) => ({ value }));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,12 +9,31 @@ export class OrganizationAdapter implements FieldAdapter {
|
|||
): ExtractionResult[] {
|
||||
const organizationAsLink = context?.organizationAsLink as boolean;
|
||||
const isVcfStrategy = context?.namingStrategy === NamingStrategy.VCF;
|
||||
return (contact.organizations ?? [])
|
||||
|
||||
const organizations = (contact.organizations ?? [])
|
||||
.map((org) => org.name)
|
||||
.filter((name) => !!name)
|
||||
.map((name) => ({
|
||||
.map((name) => {
|
||||
// For VCF strategy, don't use wiki links even if organizationAsLink is true
|
||||
value: organizationAsLink && !isVcfStrategy ? `[[${name}]]` : name,
|
||||
}));
|
||||
return organizationAsLink && !isVcfStrategy ? `[[${name}]]` : name;
|
||||
});
|
||||
|
||||
if (organizations.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (context?.namingStrategy === NamingStrategy.Array) {
|
||||
const first = organizations[0];
|
||||
return [
|
||||
{
|
||||
value:
|
||||
organizations.length === 1 && first !== undefined
|
||||
? first
|
||||
: organizations,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return organizations.map((value) => ({ value }));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,27 @@
|
|||
import { FieldAdapter, ExtractionResult } from '../interfaces';
|
||||
import { GoogleContact } from '../../types/Contact';
|
||||
import { NamingStrategy } from 'src/types/Settings';
|
||||
|
||||
export class PhoneAdapter implements FieldAdapter {
|
||||
extract(contact: GoogleContact): ExtractionResult[] {
|
||||
return (contact.phoneNumbers ?? [])
|
||||
extract(
|
||||
contact: GoogleContact,
|
||||
context?: Record<string, unknown>
|
||||
): ExtractionResult[] {
|
||||
const phones = (contact.phoneNumbers ?? [])
|
||||
.filter((item) => item.value)
|
||||
.map((item) => ({ value: item.value }));
|
||||
.map((item) => item.value);
|
||||
|
||||
if (phones.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (context?.namingStrategy === NamingStrategy.Array) {
|
||||
const first = phones[0];
|
||||
return [
|
||||
{ value: phones.length === 1 && first !== undefined ? first : phones },
|
||||
];
|
||||
}
|
||||
|
||||
return phones.map((value) => ({ value }));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,14 +9,31 @@ export class RelationsAdapter implements FieldAdapter {
|
|||
): ExtractionResult[] {
|
||||
const relationsAsLink = context?.relationsAsLink as boolean;
|
||||
const isVcfStrategy = context?.namingStrategy === NamingStrategy.VCF;
|
||||
return (contact.relations ?? [])
|
||||
|
||||
// First map to strings as per current logic
|
||||
const relations = (contact.relations ?? [])
|
||||
.filter((relation) => !!relation.person)
|
||||
.map((relation) => ({
|
||||
.map((relation) => {
|
||||
// For VCF strategy, don't use wiki links even if organizationsAsLink is true
|
||||
value:
|
||||
relationsAsLink && !isVcfStrategy
|
||||
? `[[${relation.person}|${relation.person} (${relation.type})]]`
|
||||
: `${relation.person} (${relation.type})`,
|
||||
}));
|
||||
return relationsAsLink && !isVcfStrategy
|
||||
? `[[${relation.person}|${relation.person} (${relation.type})]]`
|
||||
: `${relation.person} (${relation.type})`;
|
||||
});
|
||||
|
||||
if (relations.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (context?.namingStrategy === NamingStrategy.Array) {
|
||||
const first = relations[0];
|
||||
return [
|
||||
{
|
||||
value:
|
||||
relations.length === 1 && first !== undefined ? first : relations,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return relations.map((value) => ({ value }));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
7
src/core/strategies/ArrayNamingStrategy.ts
Normal file
7
src/core/strategies/ArrayNamingStrategy.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { DefaultNamingStrategy } from './DefaultNamingStrategy';
|
||||
|
||||
export class ArrayNamingStrategy extends DefaultNamingStrategy {
|
||||
// Inherits generateKey from DefaultNamingStrategy
|
||||
// The magic happens in the adapters which will return a single array value
|
||||
// when they detect this strategy in the context.
|
||||
}
|
||||
|
|
@ -42,6 +42,7 @@ export const ru = {
|
|||
'Стратегия генерации ключей frontmatter из данных контакта',
|
||||
Default: 'Стандартная',
|
||||
'VCF (vCard)': 'VCF (vCard)',
|
||||
Array: 'Массив',
|
||||
'Organization as link': 'Организация как ссылка',
|
||||
'Organization name will be stored as a obsidian link [[...]] instead of plain text':
|
||||
'Название организации будет сохранено как ссылка Obsidian [[...]] вместо обычного текста (игнорируется в стратегии VCF)',
|
||||
|
|
@ -138,6 +139,7 @@ export const lv = {
|
|||
'Stratēģija frontmatter atslēgu ģenerēšanai no kontakta datiem',
|
||||
Default: 'Noklusējuma',
|
||||
'VCF (vCard)': 'VCF (vCard)',
|
||||
Array: 'Masīvs',
|
||||
'Organization as link': 'Organizācija kā saite',
|
||||
'Organization name will be stored as a obsidian link [[...]] instead of plain text':
|
||||
'Organizācijas nosaukums tiks saglabāts kā Obsidian saite [[...]], nevis parasts teksts (tiek ignorēts VCF stratēģijā)',
|
||||
|
|
@ -233,6 +235,7 @@ export const en = {
|
|||
'Strategy to generate frontmatter keys from contact data',
|
||||
Default: 'Default',
|
||||
'VCF (vCard)': 'VCF (vCard)',
|
||||
Array: 'Array',
|
||||
'Organization as link': 'Organization as link',
|
||||
'Organization name will be stored as a obsidian link [[...]] instead of plain text':
|
||||
'Organization name will be stored as a obsidian link [[...]] instead of plain text (ignored in VCF strategy)',
|
||||
|
|
|
|||
|
|
@ -109,6 +109,7 @@ export class ContactSyncSettingTab extends PluginSettingTab {
|
|||
dropdown
|
||||
.addOption('Default', t('Default'))
|
||||
.addOption('VCF', t('VCF (vCard)'))
|
||||
.addOption('Array', t('Array'))
|
||||
.setValue(this.plugin.settings.namingStrategy)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.namingStrategy = value as NamingStrategy;
|
||||
|
|
|
|||
|
|
@ -63,4 +63,5 @@ export interface ContactSyncSettings {
|
|||
export enum NamingStrategy {
|
||||
Default = 'Default',
|
||||
VCF = 'VCF',
|
||||
Array = 'Array',
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue