mirror of
https://github.com/broekema41/obsidian-vcf-contacts.git
synced 2026-07-22 05:42:58 +00:00
Refs #8: we have a settings page that can check the connection and save the settings if ok
This commit is contained in:
parent
12aa4453ba
commit
0810c8af51
14 changed files with 466 additions and 50 deletions
15
README.md
15
README.md
|
|
@ -351,6 +351,19 @@ npm run test:coverage
|
|||
For a breakdown of our testing approach, structure, and goals, see:
|
||||
👉 [our testing strategy](assets/docs/testing-strategy.md)
|
||||
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 🌟 Support & Get Involved
|
||||
|
||||
We rely on our community to keep this project free and moving forward. If you believe in what we’re building, now’s the perfect time to step out of the shadows and help shape the future of this plugin!
|
||||
|
||||
Spread the word, connect us with sponsors, or simply share your enthusiasm, your support matters.
|
||||
|
||||
Ready to make a difference? [Sign up here.](https://tally.so/r/mR6rKj)
|
||||
|
||||
---
|
||||
|
||||
<!-- TOC --><a name="-acknowledgements"></a>
|
||||
|
|
@ -358,5 +371,5 @@ For a breakdown of our testing approach, structure, and goals, see:
|
|||
|
||||
This plugin started as a fork of **Vadim Beskrovnov’s Contacts plugin**. While the codebase has since evolved significantly, his original work laid the foundation. Immense thanks to Vadim for the early inspiration and groundwork.
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
|
|
|||
30
prompting.md
Normal file
30
prompting.md
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
We are building on a obsidian plugin that can manage, organize, and interact with your contacts inside Obsidian. Import, export, and structure vCard (VCF) files seamlessly while keeping all contact details accessible in your knowledge base. Includes click-to-call, right-click copy, structured metadata, and more!
|
||||
|
||||
Our approach is shaped by ideas from A Philosophy of Software Design by John Ousterhout. A recommended read if you're into practical thinking about clarity, change, and modularity.
|
||||
|
||||
While the book doesn’t define a testing method, it strongly influences how we code. We care about what modules do. not how they do it. Using deep interfaces. This makes tests more stable and more useful during redesigns.
|
||||
|
||||
1) Separate ui tsx from core modules like parsing, adapters and utillities.
|
||||
2) separate obsidian interfaces where we can so that testing becomes easier.
|
||||
3) Avoid the use of fetch to interact with api's as this results in cors errors
|
||||
4) Desktop in obsidion is built with Electron and the app is based on Capacitor
|
||||
|
||||
## Code Structure Suggestions
|
||||
|
||||
- **Move business logic out of UI components:**
|
||||
Keep your `.tsx` files focused on rendering and user interaction. Avoid nested ifs and complex logic in UI code.
|
||||
|
||||
- **Centralize business rules and configuration:**
|
||||
Place business logic, configuration, and state management in core modules or in `settings.ts`. This keeps logic reusable and easier to test.
|
||||
|
||||
- **Use clear module boundaries:**
|
||||
Parsing, adapters, and utilities should be in their own files. UI should call into these modules, not contain their logic.
|
||||
|
||||
- **Benefits:**
|
||||
- Easier to test business logic without UI dependencies
|
||||
- Cleaner, more maintainable UI code
|
||||
- Simpler redesigns and refactoring
|
||||
|
||||
- **Example:**
|
||||
- Move validation, data transformation, and API interaction logic from UI components to `settings.ts` or a dedicated service module.
|
||||
- UI components should only call functions from these modules and display results.
|
||||
|
|
@ -44,7 +44,7 @@ export const UidProcessor: InsightProcessor = {
|
|||
settingDefaultValue: true,
|
||||
|
||||
async process(contact:Contact): Promise<InsightQueItem | undefined> {
|
||||
const activeProcessor = getSettings()[`${this.settingPropertyName}`] as boolean;
|
||||
const activeProcessor = getSettings().processors[`${this.settingPropertyName}`] as boolean;
|
||||
if (!activeProcessor || contact.data['UID']) {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import "src/insights/insightLoading";
|
||||
|
||||
import { Plugin } from 'obsidian';
|
||||
import { ContactsSettingTab, DEFAULT_SETTINGS } from 'src/ui/settings/settingsView';
|
||||
import { DEFAULT_SETTINGS } from "src/settings/setting";
|
||||
import { ContactsSettingTab } from 'src/ui/settings/settingsView';
|
||||
import { ContactsView } from "src/ui/sidebar/sidebarView";
|
||||
import { CONTACTS_VIEW_CONFIG } from "src/util/constants";
|
||||
import myScrollTo from "src/util/myScrollTo";
|
||||
|
|
|
|||
23
src/settings/setting.ts
Normal file
23
src/settings/setting.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { insightService } from "src/insights/insightService";
|
||||
import { ContactsPluginSettings } from "src/settings/settings";
|
||||
|
||||
const insightsSetting = insightService.settings();
|
||||
const insightsSettingDefaults = insightsSetting.reduce((acc:Record<string, string|boolean>, setting) => {
|
||||
acc[setting.settingPropertyName] = setting.settingDefaultValue;
|
||||
return acc;
|
||||
}, {} as Record<string, string>);
|
||||
|
||||
export const DEFAULT_SETTINGS: ContactsPluginSettings = {
|
||||
contactsFolder: '',
|
||||
defaultHashtag: '',
|
||||
processors: insightsSettingDefaults,
|
||||
syncSelected: 'None',
|
||||
CardDAV: {
|
||||
addressBookUrl: '',
|
||||
syncEnabled: false,
|
||||
syncInterval: 900,
|
||||
authKey: '',
|
||||
authType: 'apikey'
|
||||
}
|
||||
}
|
||||
|
||||
21
src/settings/settings.d.ts
vendored
21
src/settings/settings.d.ts
vendored
|
|
@ -1,7 +1,24 @@
|
|||
|
||||
export type AuthType = "basic" | "apikey";
|
||||
export type SyncSelected = "None" | "CardDAV";
|
||||
|
||||
export interface ContactsPluginSettings {
|
||||
contactsFolder: string;
|
||||
defaultHashtag: string;
|
||||
[key: string]: string|boolean;
|
||||
|
||||
processors: ProcessorsSettings
|
||||
syncSelected: SyncSelected;
|
||||
CardDAV: CardDavSyncSettings;
|
||||
}
|
||||
|
||||
|
||||
interface CardDavSyncSettings {
|
||||
addressBookUrl: string;
|
||||
syncEnabled: boolean;
|
||||
syncInterval: number;
|
||||
authKey: string;
|
||||
authType: AuthType;
|
||||
}
|
||||
|
||||
interface ProcessorsSettings {
|
||||
[key: string]: string|boolean;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import { VCardMeta, VCardRaw } from "src/sync/adapters/adapter";
|
||||
import { CarddavSettingsInterface } from "src/ui/settings/components/carddavSettings";
|
||||
import { AppHttpResponse } from "src/util/platformHttpClient";
|
||||
|
||||
export interface AdapterInterface {
|
||||
/**
|
||||
|
|
@ -25,7 +27,7 @@ export interface AdapterInterface {
|
|||
* Check if the remote party is reachable with current settings.
|
||||
* @returns Promise resolving to true if connection succeeds, false otherwise
|
||||
*/
|
||||
checkConnectivity(): Promise<boolean>;
|
||||
checkConnectivity(settings: CarddavSettingsInterface): Promise<AppHttpResponse>
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,21 +1,23 @@
|
|||
import { getSettings, onSettingsChange } from "src/context/sharedSettingsContext";
|
||||
import { VCardMeta, VCardRaw } from "src/sync/adapters/adapter";
|
||||
import { AdapterInterface } from "src/sync/adapters/adapterInterface";
|
||||
import { CarddavSettingsInterface } from "src/ui/settings/components/carddavSettings";
|
||||
import { AppHttpResponse, PlatformHttpClient } from "src/util/platformHttpClient";
|
||||
|
||||
export class CarddavGenericAdapter implements AdapterInterface {
|
||||
private bearerToken: string;
|
||||
private addressBookUrl: string;
|
||||
|
||||
constructor() {
|
||||
this.bearerToken = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiIxIiwianRpIjoiMDFkZjgwZmMwNmQ2ZTUzZTYxMjA4MGQ1MTRhMGRkYThmMGYxNzBkOGFiM2MxYmM0ZDIxM2Y4NDk0MzFjNDNmZGQxZTc4YzMxYTgxZDY2OGEiLCJpYXQiOjE3NTAzMzY3NzQuNjg4ODc2LCJuYmYiOjE3NTAzMzY3NzQuNjg4ODc4LCJleHAiOjE3ODE4NzI3NzQuNjgzMDM5LCJzdWIiOiIxIiwic2NvcGVzIjpbXX0.KwCHqO-9wBxmo86W-CLc4uEdFelVX2ZSHuG_e-6Gem3cRadw9ARP19OfusVanE9OPP88-8LCPTHM9ZAYu9sEVAC5kCEKI0HQ9AIvmLDlzgTTmmm_UZMYbOj86h2tevvxdttHlhJYbMl-9L9URy9AfHtxafB4oBiNLA9fsXWnC00cAuxNyLXVtcxedvlDemNXSRr6GZlTfrwKs0Unaf3TFVQlDznOn78lBSUkM53sxq6UP7zCNrfxIjY1lG75C0oFtEkalQXqnSu0-Cd6rPecCjK76mZoWNrcLK04ss2UtOyKWyytSwdnllF2CMXnB995xKOkmzxTWtnz7WuToHVMad7ED87QiVXVld3lJXmR-1NuAfLeWGN1AY4VUu5tq7uu38Px5XYWUi-D_Tkog3uNT9J015RnfRJQvQO6SaUFjUYRNlx2uZ78TqDwrQmruHAhmj4RxaLVKdVcNhiLGk1b-fnr1GV4ki7nrxGgdxudEGnB-A5GAPAjVyM7ML4r2wT0RR-q2QZMUi2D-vuPHM1EPL21hPIssDmrBxhaXh9NUaKk16BnzRr8RaHXWlZMtwe9V5Piy0U5QU9fIDA1aR2qBuW-37toVZDP6H6rNShe8x72c5OSUqyxuvaXDhEsbxZ4DecaP2ILFHtNG0b59eACMcW6drzW4YZx_v1LZFppc7c' ;
|
||||
this.addressBookUrl = 'http://localhost:8080/dav/addressbooks/broekema41@gmail.com/contacts' ;
|
||||
}
|
||||
export function carddavGenericAdapter(): AdapterInterface {
|
||||
|
||||
private getAuthHeader() {
|
||||
let settings = getSettings();
|
||||
onSettingsChange(()=> {
|
||||
settings = getSettings();
|
||||
});
|
||||
|
||||
const getAuthHeader = () => {
|
||||
return 'Bearer ' + this.bearerToken;
|
||||
}
|
||||
|
||||
private async doFetch(options: RequestInit): Promise<Response> {
|
||||
return fetch(this.addressBookUrl, {
|
||||
const doFetch = async (options: RequestInit): Promise<Response> =>{
|
||||
return fetch(settings.CardDAV.addressBookUrl, {
|
||||
...options,
|
||||
headers: {
|
||||
...options.headers,
|
||||
|
|
@ -24,8 +26,8 @@ export class CarddavGenericAdapter implements AdapterInterface {
|
|||
});
|
||||
}
|
||||
|
||||
private async doPush(options: RequestInit, vcard: VCardRaw): Promise<void> {
|
||||
const vcfUrl = this.addressBookUrl + `${vcard.uid}.vcf`;
|
||||
const doPush = async (options: RequestInit, vcard: VCardRaw): Promise<void> => {
|
||||
const vcfUrl = settings.CardDAV.addressBookUrl + `${vcard.uid}.vcf`;
|
||||
const res = await fetch(vcfUrl, {
|
||||
method: 'PUT',
|
||||
body: vcard.raw,
|
||||
|
|
@ -38,26 +40,38 @@ export class CarddavGenericAdapter implements AdapterInterface {
|
|||
if (!res.ok) throw new Error(`Push failed: ${res.statusText}`);
|
||||
}
|
||||
|
||||
async checkConnectivity(): Promise<boolean> {
|
||||
try {
|
||||
const res = await this.doFetch({ method: 'OPTIONS' });
|
||||
return res.ok;
|
||||
} catch {
|
||||
return false;
|
||||
const checkConnectivity = async (settings: CarddavSettingsInterface): Promise<AppHttpResponse> => {
|
||||
const headers = { Authorization : ''};
|
||||
if (settings.authKey) {
|
||||
headers.Authorization = `Bearer ${settings.authKey}`;
|
||||
} else {
|
||||
headers.Authorization = 'Basic ' + btoa(settings.username + ":" + settings.password);
|
||||
}
|
||||
return await PlatformHttpClient.request({
|
||||
url: settings.addressBookUrl,
|
||||
method: 'OPTIONS',
|
||||
headers,
|
||||
});
|
||||
}
|
||||
|
||||
getList(): Promise<VCardMeta[]> {
|
||||
const getList = async (): Promise<VCardMeta[]> => {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
pull(uid: string): Promise<VCardRaw | undefined> {
|
||||
const pull = async (uid: string): Promise<VCardRaw | undefined> => {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
push(vcard: VCardRaw): Promise<void> {
|
||||
const push = async (vcard: VCardRaw): Promise<void> => {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
return {
|
||||
checkConnectivity,
|
||||
getList,
|
||||
pull,
|
||||
push,
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
94
src/ui/settings/components/carddavSettings.tsx
Normal file
94
src/ui/settings/components/carddavSettings.tsx
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
// In CarddavSettings.jsx:
|
||||
|
||||
|
||||
|
||||
import * as React from "react";
|
||||
|
||||
|
||||
export interface CarddavSettingsInterface {
|
||||
addressBookUrl: string;
|
||||
username: string;
|
||||
password: string;
|
||||
authKey: string;
|
||||
}
|
||||
|
||||
interface CarddavSettingsProps {
|
||||
carddavSettings: CarddavSettingsInterface;
|
||||
setCarddavSettings: (settings: CarddavSettingsInterface) => void;
|
||||
}
|
||||
|
||||
export default function CarddavSettings({ carddavSettings, setCarddavSettings }: CarddavSettingsProps) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="setting-item">
|
||||
<div className="setting-item-info">
|
||||
<div className="setting-item-name">Address Book URL</div>
|
||||
<div className="setting-item-description">
|
||||
URL of your CardDAV address book.
|
||||
</div>
|
||||
</div>
|
||||
<div className="setting-item-control">
|
||||
<input
|
||||
className="textfield"
|
||||
type="text"
|
||||
placeholder="https://example.com/carddav"
|
||||
value={carddavSettings.addressBookUrl}
|
||||
onChange={e => setCarddavSettings({ ...carddavSettings, addressBookUrl: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="setting-item">
|
||||
<div className="setting-item-info">
|
||||
<div className="setting-item-name">Username</div>
|
||||
<div className="setting-item-description">
|
||||
account username.
|
||||
</div>
|
||||
</div>
|
||||
<div className="setting-item-control">
|
||||
<input
|
||||
className="textfield"
|
||||
type="text"
|
||||
placeholder="username"
|
||||
value={carddavSettings.username}
|
||||
onChange={e => setCarddavSettings({ ...carddavSettings, username: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="setting-item">
|
||||
<div className="setting-item-info">
|
||||
<div className="setting-item-name">Password</div>
|
||||
<div className="setting-item-description">
|
||||
CardDAV account password.
|
||||
</div>
|
||||
</div>
|
||||
<div className="setting-item-control">
|
||||
<input
|
||||
className="textfield"
|
||||
type="password"
|
||||
placeholder="password"
|
||||
value={carddavSettings.password}
|
||||
onChange={e => setCarddavSettings({ ...carddavSettings, password: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="setting-item">
|
||||
<div className="setting-item-info">
|
||||
<div className="setting-item-name">API Key</div>
|
||||
<div className="setting-item-description">
|
||||
API key used instead of username and password.
|
||||
</div>
|
||||
</div>
|
||||
<div className="setting-item-control">
|
||||
<input
|
||||
className="textfield"
|
||||
type="text"
|
||||
placeholder="API key"
|
||||
value={carddavSettings.authKey}
|
||||
onChange={e => setCarddavSettings({ ...carddavSettings, authKey: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -2,22 +2,22 @@ import { Setting } from "obsidian";
|
|||
import * as React from "react";
|
||||
import { setSettings } from "src/context/sharedSettingsContext";
|
||||
import { InsighSettingProperties } from "src/insights/insight";
|
||||
import { insightService } from "src/insights/insightService";
|
||||
import ContactsPlugin from "src/main";
|
||||
|
||||
interface InsightSettingsProps {
|
||||
plugin: ContactsPlugin;
|
||||
insightsSetting: InsighSettingProperties[]
|
||||
}
|
||||
|
||||
export function InsightSettings({ plugin, insightsSetting }: InsightSettingsProps) {
|
||||
|
||||
export function InsightSettings({ plugin }: InsightSettingsProps) {
|
||||
const insightsSetting = insightService.settings();
|
||||
const containerRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (containerRef.current) {
|
||||
insightsSetting.forEach((settingProps :InsighSettingProperties) => {
|
||||
const settingKey = settingProps.settingPropertyName;
|
||||
const currentValue = plugin.settings[settingKey];
|
||||
const currentValue = plugin.settings.processors[settingKey];
|
||||
|
||||
if (typeof currentValue === 'boolean' && containerRef.current) {
|
||||
new Setting(containerRef.current)
|
||||
|
|
@ -27,7 +27,7 @@ export function InsightSettings({ plugin, insightsSetting }: InsightSettingsProp
|
|||
toggle
|
||||
.setValue(currentValue)
|
||||
.onChange(async (value) => {
|
||||
plugin.settings[settingKey] = value;
|
||||
plugin.settings.processors[settingKey] = value;
|
||||
await plugin.saveSettings();
|
||||
setSettings(plugin.settings);
|
||||
}));
|
||||
|
|
@ -41,7 +41,7 @@ export function InsightSettings({ plugin, insightsSetting }: InsightSettingsProp
|
|||
<div className="setting-item-spacer" ref={containerRef}>
|
||||
<div className="setting-item setting-item-heading">
|
||||
<div className="setting-item-info">
|
||||
<div className="setting-item-name">Insights Processors</div>
|
||||
<div className="setting-item-name">Insights processors</div>
|
||||
<div className="setting-item-description"></div>
|
||||
</div>
|
||||
<div className="setting-item-control"></div>
|
||||
|
|
|
|||
|
|
@ -1 +1,130 @@
|
|||
export {}
|
||||
import { App, Setting } from "obsidian";
|
||||
import * as React from "react";
|
||||
import ContactsPlugin from "src/main";
|
||||
import { SyncSelected } from "src/settings/settings";
|
||||
import { carddavGenericAdapter } from "src/sync/adapters/carddavGeneric";
|
||||
import CarddavSettings from "src/ui/settings/components/carddavSettings";
|
||||
|
||||
|
||||
interface SynchronizationSettingsProps {
|
||||
plugin: ContactsPlugin;
|
||||
app: App;
|
||||
}
|
||||
|
||||
const initCardavSettings = {
|
||||
addressBookUrl: "",
|
||||
username: "",
|
||||
password: "",
|
||||
authKey: ""
|
||||
};
|
||||
|
||||
export function SynchronizationSettings({plugin, app}: SynchronizationSettingsProps) {
|
||||
|
||||
const [warning, setWarning] = React.useState('');
|
||||
const [syncEnabled, setSyncEnabled] = React.useState<boolean>(plugin.settings.CardDAV.syncEnabled);
|
||||
const [syncSelected, setSyncSelected] = React.useState<SyncSelected>(plugin.settings.syncSelected);
|
||||
const [carddavSettings, setCarddavSettings] = React.useState({
|
||||
...initCardavSettings,
|
||||
addressBookUrl: plugin.settings.CardDAV.addressBookUrl
|
||||
});
|
||||
|
||||
const enableSync = async () => {
|
||||
if (syncSelected !== 'CardDAV') {
|
||||
setWarning('Kindly select a synchronization method and provide the required connection information. Thank you!');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await carddavGenericAdapter().checkConnectivity(carddavSettings);
|
||||
if (result.errorMessage) {
|
||||
setWarning(`failed to enable connection! ${result.errorMessage}. Please check your connection settings.`);
|
||||
return;
|
||||
}
|
||||
if (!(result.status >= 200 && result.status < 300)) {
|
||||
setWarning('failed to enable connection! unknown error. Please check your connection settings.');
|
||||
return;
|
||||
}
|
||||
|
||||
plugin.settings.CardDAV = {
|
||||
addressBookUrl: carddavSettings.addressBookUrl,
|
||||
syncEnabled: true,
|
||||
syncInterval: 900,
|
||||
authType: carddavSettings.authKey ? 'apikey' : 'basic',
|
||||
authKey: carddavSettings.authKey ? carddavSettings.authKey : btoa(`${carddavSettings.username}:${carddavSettings.password}`)
|
||||
};
|
||||
setCarddavSettings({
|
||||
...initCardavSettings,
|
||||
addressBookUrl: plugin.settings.CardDAV.addressBookUrl
|
||||
});
|
||||
setSyncEnabled(true);
|
||||
setWarning('');
|
||||
await plugin.saveSettings();
|
||||
} catch (err: any) {
|
||||
setWarning(`failed to enable connection! ${err?.message || err || 'Unknown error'}.`);
|
||||
}
|
||||
};
|
||||
|
||||
const disableSync = () => {
|
||||
plugin.settings.CardDAV.syncEnabled = false;
|
||||
plugin.saveSettings();
|
||||
setSyncEnabled(false);
|
||||
setSyncSelected('None');
|
||||
}
|
||||
|
||||
// React.useEffect(() => {
|
||||
// console.log(carddavSettings);
|
||||
//
|
||||
// },[carddavSettings]);
|
||||
|
||||
React.useEffect(() => {
|
||||
plugin.settings.syncSelected = syncSelected;
|
||||
plugin.saveSettings();
|
||||
}, [syncSelected]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="setting-item js-keep">
|
||||
<div className="setting-item-info">
|
||||
<div className="setting-item-name">Synchronization</div>
|
||||
<div className="setting-item-description">
|
||||
{syncEnabled ? <div className="mod-success">CardDAV sync Enabled</div> : ''}
|
||||
<div className="mod-warning">{warning}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="setting-item-control">
|
||||
{syncEnabled ?
|
||||
<button className="mod-destructive" onClick={disableSync}>Disable</button>
|
||||
:
|
||||
<button className="mod-cta" onClick={enableSync}>Enable</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div className="setting-item js-keep">
|
||||
<div className="setting-item-info">
|
||||
<div className="setting-item-name">Sync method</div>
|
||||
<div className="setting-item-description">Choose how you want to synchronize your contacts.</div>
|
||||
</div>
|
||||
<div className="setting-item-control">
|
||||
<select
|
||||
className="dropdown"
|
||||
value={syncSelected}
|
||||
onChange={e => {
|
||||
disableSync();
|
||||
setSyncSelected(e.target.value as SyncSelected);
|
||||
}}>
|
||||
<option value="None">No synchronization</option>
|
||||
<option value="CardDAV">CardDAV address book</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{syncSelected === "CardDAV" && (
|
||||
<CarddavSettings
|
||||
carddavSettings={carddavSettings}
|
||||
setCarddavSettings={setCarddavSettings}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,23 +1,11 @@
|
|||
import { App, PluginSettingTab } from "obsidian";
|
||||
import * as React from "react";
|
||||
import { createRoot, Root } from "react-dom/client";
|
||||
import { insightService } from "src/insights/insightService";
|
||||
import ContactsPlugin from "src/main";
|
||||
import { ContactsPluginSettings } from "src/settings/settings"
|
||||
import { InsightSettings } from "src/ui/settings/components/insightsSettings";
|
||||
import { MasterSetting } from "src/ui/settings/components/masterSettings";
|
||||
import { SynchronizationSettings } from "src/ui/settings/components/synchronizationSettings";
|
||||
|
||||
const insightsSetting = insightService.settings();
|
||||
const insightsSettingDefaults = insightsSetting.reduce((acc:Record<string, string|boolean>, setting) => {
|
||||
acc[setting.settingPropertyName] = setting.settingDefaultValue;
|
||||
return acc;
|
||||
}, {} as Record<string, string>);
|
||||
|
||||
export const DEFAULT_SETTINGS: ContactsPluginSettings = {
|
||||
contactsFolder: '',
|
||||
defaultHashtag: '',
|
||||
...insightsSettingDefaults
|
||||
}
|
||||
|
||||
export class ContactsSettingTab extends PluginSettingTab {
|
||||
plugin: ContactsPlugin;
|
||||
|
|
@ -43,9 +31,11 @@ export class ContactsSettingTab extends PluginSettingTab {
|
|||
/>
|
||||
<InsightSettings
|
||||
plugin={this.plugin}
|
||||
insightsSetting={insightsSetting}
|
||||
/>
|
||||
|
||||
<SynchronizationSettings
|
||||
app={this.app}
|
||||
plugin={this.plugin}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
93
src/util/platformHttpClient.ts
Normal file
93
src/util/platformHttpClient.ts
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
export interface AppHttpRequest {
|
||||
url: string;
|
||||
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'OPTIONS';
|
||||
headers?: Record<string, string>;
|
||||
body?: any;
|
||||
}
|
||||
|
||||
export type AppHttpResponse = {
|
||||
status: number;
|
||||
data: any;
|
||||
headers: Record<string, string>;
|
||||
errorMessage: string;
|
||||
};
|
||||
|
||||
const HTTP_STATUS_TITLES: Record<number, string> = {
|
||||
400: '400 Bad Request',
|
||||
401: '401 Unauthorized',
|
||||
403: '403 Forbidden',
|
||||
404: '404 Not Found',
|
||||
500: '500 Internal Server Error',
|
||||
// Add more as needed
|
||||
};
|
||||
|
||||
export const PlatformHttpClient = {
|
||||
async request({ url, method = 'GET', headers = {}, body }: AppHttpRequest) {
|
||||
try {
|
||||
const response = await nodeRequest(url, method, body, headers);
|
||||
return {
|
||||
...response,
|
||||
errorMessage: HTTP_STATUS_TITLES[response.status]? HTTP_STATUS_TITLES[response.status] : ''
|
||||
};
|
||||
|
||||
} catch (err: any) {
|
||||
return {
|
||||
status: 500,
|
||||
data: '',
|
||||
headers: {},
|
||||
errorMessage: HTTP_STATUS_TITLES[500]
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
async function nodeRequest(
|
||||
url: string,
|
||||
method = 'GET',
|
||||
body?: any,
|
||||
headers: Record<string, string> = {}
|
||||
): Promise<AppHttpResponse> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { URL } = require('url');
|
||||
const parsedUrl = new URL(url);
|
||||
const protocol = parsedUrl.protocol === 'https:' ? 'node:https' : 'node:http';
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const client = require(protocol);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const options = {
|
||||
method,
|
||||
hostname: parsedUrl.hostname,
|
||||
port: parsedUrl.port,
|
||||
path: parsedUrl.pathname + parsedUrl.search,
|
||||
headers,
|
||||
};
|
||||
|
||||
const req = client.request(options, (res: any) => {
|
||||
let data = '';
|
||||
res.setEncoding('utf8');
|
||||
res.on('data', (chunk: string) => (data += chunk));
|
||||
res.on('end', () => {
|
||||
resolve({
|
||||
status: res.statusCode || 0,
|
||||
data,
|
||||
headers: res.headers,
|
||||
errorMessage: ''
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', () => {
|
||||
reject();
|
||||
});
|
||||
|
||||
if (body) {
|
||||
if (typeof body === 'object' && !Buffer.isBuffer(body)) {
|
||||
req.write(JSON.stringify(body));
|
||||
} else {
|
||||
req.write(body);
|
||||
}
|
||||
}
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
|
@ -12,7 +12,17 @@ import { afterEach,describe, expect, it, vi } from 'vitest';
|
|||
const mockSettings: ContactsPluginSettings = {
|
||||
contactsFolder: 'Contacts',
|
||||
defaultHashtag: '',
|
||||
enableSync: true,
|
||||
processors: {
|
||||
someRandomProcessor: true,
|
||||
},
|
||||
syncSelected: 'None',
|
||||
CardDAV: {
|
||||
addressBookUrl: '',
|
||||
syncEnabled: false,
|
||||
syncInterval: 900,
|
||||
authKey: '',
|
||||
authType: 'apikey'
|
||||
}
|
||||
};
|
||||
|
||||
describe('sharedSppContext', () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue