Add support for "Relations" fields (#29)

* Update target to EcmaScript 2021

According to this: https://forum.obsidian.md/t/what-is-the-most-sensible-ecmascript-build-target-for-a-plugin/103193
There is no reason to go lower right now.

* Start loading Relations from the People API

* Add relationsAsLink setting

* Add relations adapter with VCF support

---------

Co-authored-by: Aleksejs Kovalovs <aleks4444@inbox.lv>
This commit is contained in:
Kamil 2025-12-29 18:40:52 +01:00 committed by GitHub
parent 5845592af3
commit 1f4d8bf078
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 63 additions and 3 deletions

View file

@ -122,6 +122,7 @@ describe('ContactNoteWriter', () => {
folderPath: 'path/to/folder',
noteBody: mockNoteBody,
organizationAsLink: false,
relationsAsLink: false,
trackSyncTime: true,
renameFiles: false,
namingStrategy: NamingStrategy.Default,
@ -172,6 +173,7 @@ describe('ContactNoteWriter', () => {
folderPath: 'path/to/folder',
noteBody: mockNoteBody,
organizationAsLink: false,
relationsAsLink: false,
trackSyncTime: true,
renameFiles: false,
namingStrategy: NamingStrategy.Default,
@ -221,6 +223,7 @@ describe('ContactNoteWriter', () => {
folderPath: 'path/to/folder',
noteBody: mockNoteBody,
organizationAsLink: false,
relationsAsLink: false,
trackSyncTime: true,
renameFiles: false,
namingStrategy: NamingStrategy.Default,
@ -264,6 +267,7 @@ describe('ContactNoteWriter', () => {
folderPath: 'path/to/folder',
noteBody: mockNoteBody,
organizationAsLink: false,
relationsAsLink: false,
trackSyncTime: true,
renameFiles: false,
namingStrategy: NamingStrategy.Default,
@ -305,6 +309,7 @@ describe('ContactNoteWriter', () => {
folderPath: 'path/to/folder',
noteBody: mockNoteBody,
organizationAsLink: false,
relationsAsLink: false,
trackSyncTime: true,
renameFiles: false,
namingStrategy: NamingStrategy.Default,

View file

@ -112,6 +112,7 @@ describe('ContactNoteWriterUpdate', () => {
syncLabel: '',
noteBody: mockNoteBody,
organizationAsLink: false,
relationsAsLink: false,
trackSyncTime: true,
renameFiles: false,
namingStrategy: NamingStrategy.Default,
@ -158,6 +159,7 @@ describe('ContactNoteWriterUpdate', () => {
syncLabel: '',
noteBody: mockNoteBody,
organizationAsLink: false,
relationsAsLink: false,
trackSyncTime: true,
renameFiles: false,
namingStrategy: NamingStrategy.Default,

View file

@ -5,7 +5,7 @@ export const URL_OAUTH_TOKEN = 'https://oauth2.googleapis.com/token';
/** Google People API endpoints */
const URL_PEOPLE_BASE = 'https://people.googleapis.com/v1';
export const URL_PEOPLE_CONNECTIONS = `${URL_PEOPLE_BASE}/people/me/connections`;
export const PERSONAL_FIELDS = `names,emailAddresses,phoneNumbers,birthdays,memberships,metadata,addresses,biographies,organizations`;
export const PERSONAL_FIELDS = `names,emailAddresses,phoneNumbers,birthdays,memberships,metadata,addresses,biographies,organizations,relations`;
export const OTHER_CONTACTS_FIELDS = `names,emailAddresses,phoneNumbers`;
export const URL_CONTACT_GROUPS = `${URL_PEOPLE_BASE}/contactGroups?pageSize=1000`;
@ -38,6 +38,7 @@ export const DEFAULT_SETTINGS: ContactSyncSettings = {
syncOnStartup: false,
trackSyncTime: false,
organizationAsLink: false,
relationsAsLink: true,
renameFiles: false,
namingStrategy: NamingStrategy.Default,
lastFirst: false,

View file

@ -12,6 +12,7 @@ import { JobTitleAdapter } from './adapters/JobTitleAdapter';
import { DepartmentAdapter } from './adapters/DepartmentAdapter';
import { BirthdayAdapter } from './adapters/BirthdayAdapter';
import { LabelAdapter } from './adapters/LabelAdapter';
import { RelationsAdapter } from './adapters/RelationsAdapter';
/**
* Formatter class responsible for coordinating field extraction and key generation.
@ -99,6 +100,7 @@ export function createDefaultFormatter(
jobtitle: new JobTitleAdapter(),
department: new DepartmentAdapter(),
labels: new LabelAdapter(),
relations: new RelationsAdapter(),
};
if (strategyType === NamingStrategy.VCF) {

View file

@ -0,0 +1,19 @@
import { FieldAdapter, ExtractionResult } from '../interfaces';
import { GoogleContact } from '../../types/Contact';
import { NamingStrategy } from 'src/types/Settings';
export class RelationsAdapter implements FieldAdapter {
extract(
contact: GoogleContact,
context?: Record<string, unknown>
): ExtractionResult[] {
const relationsAsLink = context?.relationsAsLink as boolean;
const isVcfStrategy = context?.namingStrategy === NamingStrategy.VCF;
return (contact.relations ?? [])
.filter((relation) => !!relation.person)
.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})`,
}));
}
}

View file

@ -6,6 +6,7 @@ export class VcfNamingStrategy implements KeyNamingStrategy {
phone: 'TEL',
address: 'ADR',
organization: 'ORG',
relations: 'X-RELATION',
jobtitle: 'TITLE',
bio: 'NOTE',
birthday: 'BDAY',

View file

@ -209,6 +209,7 @@ export default class GoogleContactsSyncPlugin extends Plugin {
syncLabel: this.settings.syncLabel,
noteBody: this.settings.noteTemplate || '# Notes\n',
organizationAsLink: this.settings.organizationAsLink,
relationsAsLink: this.settings.relationsAsLink,
trackSyncTime: this.settings.trackSyncTime,
renameFiles: this.settings.renameFiles,
namingStrategy: this.settings.namingStrategy,

View file

@ -147,6 +147,22 @@ export class ContactSyncSettingTab extends PluginSettingTab {
})
);
new Setting(containerEl)
.setName(t('Relations as links'))
.setDesc(
t(
'Related people names will be stored as a obsidian link [[...]] instead of plain text'
)
)
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.relationsAsLink)
.onChange(async (value) => {
this.plugin.settings.relationsAsLink = value;
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName(t('Label to sync'))
.setDesc(

View file

@ -146,6 +146,7 @@ export class ContactNoteWriter {
invertedLabelMap,
config.namingStrategy,
config.organizationAsLink,
config.relationsAsLink,
config.trackSyncTime
)
)
@ -266,6 +267,7 @@ export class ContactNoteWriter {
invertedLabelMap: Record<string, string>,
namingStrategy: NamingStrategy,
organizationAsLink = false,
relationsAsLink = false,
trackSyncTime = false
): Record<string, string | string[]> {
const formatter = createDefaultFormatter(namingStrategy);
@ -275,6 +277,7 @@ export class ContactNoteWriter {
{
labelMap: invertedLabelMap,
organizationAsLink: organizationAsLink,
relationsAsLink: relationsAsLink,
namingStrategy: namingStrategy,
}
);

View file

@ -56,6 +56,12 @@ export interface GoogleContact {
title: string;
department: string;
}[];
relations?: {
person: string;
type: string;
metadata: string;
}[];
}
/**

View file

@ -7,6 +7,7 @@ export interface ContactNoteConfig {
syncLabel: string;
noteBody: string;
organizationAsLink: boolean;
relationsAsLink: boolean;
trackSyncTime: boolean;
renameFiles: boolean;
namingStrategy: NamingStrategy;

View file

@ -47,6 +47,9 @@ export interface ContactSyncSettings {
/** Save organization as obsidian link */
organizationAsLink: boolean;
/** Save relationships as links */
relationsAsLink: boolean;
/** Whether to rename existing contact files in case if name has been changed based on current naming settings */
renameFiles: boolean;

View file

@ -1,8 +1,8 @@
{
"compilerOptions": {
"target": "es2017",
"target": "es2021",
"lib": [
"es2017",
"es2021",
"dom"
],
"module": "esnext",