From 144013cee93f81e307d7160e7ac3b86d9aed34a9 Mon Sep 17 00:00:00 2001 From: Nathan Smith Date: Sat, 6 Apr 2024 13:31:27 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=A8=20refactor:=20#84=20Move=20account?= =?UTF-8?q?=20settings=20into=20new=20class?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugin.ts | 37 ++- src/settings/account.ts | 172 ++++++++++++++ src/settings/index.ts | 3 + src/{settings.ts => settings/settings-tab.ts} | 220 +++--------------- src/settings/types.ts | 36 +++ 5 files changed, 274 insertions(+), 194 deletions(-) create mode 100644 src/settings/account.ts create mode 100644 src/settings/index.ts rename src/{settings.ts => settings/settings-tab.ts} (56%) create mode 100644 src/settings/types.ts diff --git a/src/plugin.ts b/src/plugin.ts index e34330d..f35a8c3 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -1,7 +1,7 @@ import { DEFAULT_SETTINGS, GithubLinkPluginSettingsTab } from "./settings"; import { Logger } from "./logger"; -import type { GithubLinkPluginSettings } from "./settings"; +import type { GithubLinkPluginData, GithubLinkPluginSettings } from "./settings"; import { InlineRenderer } from "./inline/inline"; import { Plugin } from "obsidian"; import { QueryProcessor } from "./query/processor"; @@ -9,6 +9,7 @@ import { createInlineViewPlugin } from "./inline/view-plugin"; import { RequestCache } from "./github/cache"; export const PluginSettings: GithubLinkPluginSettings = { ...DEFAULT_SETTINGS }; +export const PluginData: GithubLinkPluginData = { cache: null, settings: PluginSettings }; export const logger = new Logger(); let cache: RequestCache; export function getCache(): RequestCache { @@ -18,16 +19,40 @@ export function getCache(): RequestCache { export class GithubLinkPlugin extends Plugin { public cacheInterval: number | undefined; async onload() { - Object.assign(PluginSettings, await this.loadData()); + let data = await this.loadData(); + + // Migrate settings from data root to settings -- remove in v1.0.0 + if (data.accounts) { + const newSettings: GithubLinkPluginSettings = { + accounts: data.accounts ?? PluginSettings.accounts, + cacheIntervalSeconds: data.cacheIntervalSeconds ?? PluginSettings.cacheIntervalSeconds, + defaultPageSize: data.defaultPageSize ?? PluginSettings.defaultPageSize, + logLevel: data.logLevel ?? PluginSettings.logLevel, + maxCacheAgeHours: data.maxCacheAgeHours ?? PluginSettings.maxCacheAgeHours, + minRequestSeconds: data.minRequestSeconds ?? PluginSettings.minRequestSeconds, + tagShowPRMergeable: data.tagShowPRMergeable ?? PluginSettings.tagShowPRMergeable, + tagTooltips: data.tagTooltips ?? PluginSettings.tagTooltips, + defaultAccount: data.defaultAccount ?? PluginSettings.defaultAccount, + }; + const newData: GithubLinkPluginData = { + cache: data.cache ?? PluginData.cache, + settings: newSettings, + }; + await this.saveData(newData); + data = newData; + } + + Object.assign(PluginSettings, data.settings); + Object.assign(PluginData, data); logger.logLevel = PluginSettings.logLevel; - cache = new RequestCache(PluginSettings.cache); + cache = new RequestCache(PluginData.cache); // Clean cache const maxAge = new Date(new Date().getTime() - PluginSettings.maxCacheAgeHours * 60 * 60 * 1000); const entriesDeleted = cache.clean(maxAge); if (entriesDeleted > 0) { - PluginSettings.cache = cache.toJSON(); + PluginData.cache = cache.toJSON(); await this.saveData(PluginSettings); logger.info(`Cleaned ${entriesDeleted} entries from request cache.`); } @@ -50,10 +75,10 @@ export class GithubLinkPlugin extends Plugin { window.setInterval(async () => { logger.debug("Checking if cache needs a save."); if (cache.cacheUpdated) { - PluginSettings.cache = cache.toJSON(); + PluginData.cache = cache.toJSON(); await this.saveData(PluginSettings); cache.cacheUpdated = false; - logger.info(`Saved request cache with ${PluginSettings.cache?.length} items.`); + logger.info(`Saved request cache with ${PluginData.cache?.length} items.`); } }, PluginSettings.cacheIntervalSeconds * 1000), ); diff --git a/src/settings/account.ts b/src/settings/account.ts new file mode 100644 index 0000000..5fecd82 --- /dev/null +++ b/src/settings/account.ts @@ -0,0 +1,172 @@ +import type { App } from "obsidian"; +import { Setting } from "obsidian"; +import type { GithubAccount } from "./types"; +import { AuthModal } from "src/auth-modal"; +import { auth } from "src/github/auth"; +import type { Verification } from "@octokit/auth-oauth-device/dist-types/types"; + +export class AccountSettings { + authModal: AuthModal | null = null; + newAccount: GithubAccount | null = null; + + constructor( + private readonly app: App, + private readonly container: HTMLElement, + private readonly saveCallback: () => Promise, + private readonly displayCallback: () => void, + private readonly removeCallback: (account: GithubAccount) => void, + ) {} + + public render(accounts: GithubAccount[]): void { + for (const account of accounts) { + this.renderAccountSetting(account); + } + } + + public renderNewAccount( + container: HTMLElement, + saveNewAccountCallback: (account: GithubAccount) => Promise, + ): void { + this.newAccount = { id: crypto.randomUUID(), name: "", orgs: [], token: "" }; + // TODO: Combine the new account and existing account rendering to reduce duplication + const accountContainer = container.createDiv(); + const header = accountContainer.createEl("h3", { text: "New account" }); + + new Setting(accountContainer) + .setName("Account name") + .setDesc("Required.") + .addText((text) => { + text.setValue(this.newAccount!.name); + text.onChange((value) => { + this.newAccount!.name = value; + header.setText(value ?? "New account"); + }); + }) + .addButton((button) => { + button.setIcon("trash"); + button.setTooltip("Delete account"); + button.onClick(() => { + this.newAccount = null; + this.displayCallback(); + }); + }); + + new Setting(accountContainer) + .setName("Orgs and users") + .setDesc( + "A comma separated list of the GitHub organizations and users this account should be used for. Optional.", + ) + .addTextArea((text) => { + text.setValue(this.newAccount!.orgs.join(", ")); + text.onChange((value) => { + this.newAccount!.orgs = value.split(","); + }); + }); + + new Setting(accountContainer) + .setName("Token") + .setDesc( + "A GitHub token, which can be generated automatically (recommended) or by creating a personal access token (not recommended unless org does not allow OAuth tokens). Required.", + ) + .addButton((button) => { + button.setButtonText("Generate Token"); + button.onClick(async () => { + const authResult = await auth(this.tokenVerification.bind(this))({ + type: "oauth", + }); + this.authModal?.close(); + this.authModal = null; + this.newAccount!.token = authResult.token; + this.displayCallback(); + }); + }) + .addText((text) => { + text.setPlaceholder("Personal Access Token / OAuth Token"); + text.setValue(this.newAccount!.token); + text.onChange((value) => { + this.newAccount!.token = value; + }); + }); + + new Setting(accountContainer).addButton((button) => { + button.setButtonText("Save account"); + button.setTooltip("Save account"); + button.setIcon("save"); + button.onClick(async () => { + if (!this.newAccount?.name || !this.newAccount.token) { + return; + } + await saveNewAccountCallback(this.newAccount); + this.newAccount = null; + this.displayCallback(); + }); + }); + } + + public renderAccountSetting(account: GithubAccount, parent: HTMLElement = this.container): void { + const accountContainer = parent.createDiv(); + accountContainer.createEl("h3", { text: account.name }); + + new Setting(accountContainer) + .setName("Account name") + .addText((text) => { + text.setValue(account.name); + text.onChange((value) => { + account.name = value; + this.saveCallback(); + }); + }) + .addButton((button) => { + button.setIcon("trash"); + button.setTooltip("Delete account"); + button.onClick(async () => { + this.removeCallback(account); + await this.saveCallback(); + this.displayCallback(); + }); + }); + + new Setting(accountContainer) + .setName("Orgs and users") + .setDesc("A comma separated list of the GitHub organizations and users this account should be used for.") + .addTextArea((text) => { + text.setValue(account.orgs.join(", ")); + text.onChange((value) => { + account.orgs = value.split(",").map((org) => org.trim()); + this.saveCallback(); + }); + }); + + new Setting(accountContainer) + .setName("Token") + .setDesc( + "A GitHub token, which can be generated automatically (recommended) or by creating a personal access token (not recommended unless org does not allow OAuth tokens).", + ) + .addButton((button) => { + button.setButtonText("Generate Token"); + button.onClick(async () => { + const authResult = await auth(this.tokenVerification.bind(this))({ + type: "oauth", + }); + this.authModal?.close(); + this.authModal = null; + account.token = authResult.token; + await this.saveCallback(); + this.displayCallback(); + }); + }) + .addText((text) => { + text.setPlaceholder("Personal Access Token / OAuth Token"); + text.setValue(account.token); + text.onChange((value) => { + account.token = value; + this.saveCallback(); + }); + }); + } + + private tokenVerification(verification: Verification) { + this.authModal = new AuthModal(this.app, verification); + this.authModal.open(); + } +} diff --git a/src/settings/index.ts b/src/settings/index.ts new file mode 100644 index 0000000..ea50da3 --- /dev/null +++ b/src/settings/index.ts @@ -0,0 +1,3 @@ +export type { GithubAccount, GithubLinkPluginData, GithubLinkPluginSettings } from "./types"; +export { DEFAULT_SETTINGS } from "./types"; +export { GithubLinkPluginSettingsTab } from "./settings-tab"; diff --git a/src/settings.ts b/src/settings/settings-tab.ts similarity index 56% rename from src/settings.ts rename to src/settings/settings-tab.ts index c2e06f7..77b2753 100644 --- a/src/settings.ts +++ b/src/settings/settings-tab.ts @@ -2,49 +2,21 @@ import { Notice, PluginSettingTab, Setting } from "obsidian"; import type { App } from "obsidian"; -import { AuthModal } from "./auth-modal"; -import type { GithubLinkPlugin } from "./plugin"; -import { LogLevel } from "./logger"; -import { PluginSettings, getCache } from "./plugin"; -import type { Verification } from "@octokit/auth-oauth-device/dist-types/types"; -import { auth } from "./github/auth"; - -export interface GithubAccount { - id: string; - name: string; - orgs: string[]; - token: string; -} - -export interface GithubLinkPluginSettings { - accounts: GithubAccount[]; - defaultAccount?: string; - defaultPageSize: number; - logLevel: LogLevel; - tagTooltips: boolean; - tagShowPRMergeable: boolean; - cacheIntervalSeconds: number; - maxCacheAgeHours: number; - minRequestSeconds: number; - cache: string[] | null; // TODO: Move this out of settings; it's not a setting it's just part of plugin data. -} - -export const DEFAULT_SETTINGS: GithubLinkPluginSettings = { - accounts: [], - defaultPageSize: 10, - logLevel: LogLevel.Error, - tagTooltips: false, - tagShowPRMergeable: false, - cacheIntervalSeconds: 60, - maxCacheAgeHours: 120, - minRequestSeconds: 60, - cache: null, -}; +import type { GithubLinkPlugin } from "../plugin"; +import { LogLevel } from "../logger"; +import { PluginData, PluginSettings, getCache } from "../plugin"; +import type { GithubAccount, GithubLinkPluginData } from "./types"; +import { DEFAULT_SETTINGS } from "./types"; +import { AccountSettings } from "./account"; export class GithubLinkPluginSettingsTab extends PluginSettingTab { - authModal: AuthModal | null = null; - newAccount: GithubAccount | null = null; - + private readonly accountSettings = new AccountSettings( + this.app, + this.containerEl, + this.saveSettings.bind(this), + this.display.bind(this), + this.removeAccount.bind(this), + ); constructor( public app: App, private readonly plugin: GithubLinkPlugin, @@ -52,7 +24,7 @@ export class GithubLinkPluginSettingsTab extends PluginSettingTab { super(app, plugin); } - display() { + public display() { const { containerEl } = this; containerEl.empty(); containerEl.createEl("h2", { text: "GitHub authentication" }); @@ -61,13 +33,13 @@ export class GithubLinkPluginSettingsTab extends PluginSettingTab { text: "No authentication is required to reference public repositories. Providing a token allows referencing private repos, but the token is stored in plain text. You can create multiple accounts for multiple tokens.", }); - new Setting(containerEl).setName("Add account").addButton((button) => { + const newAccountSection = containerEl.createDiv(); + new Setting(newAccountSection).setName("Add account").addButton((button) => { button.setButtonText(""); button.setIcon("plus"); button.setTooltip("Add Account"); button.onClick(() => { - this.newAccount = { id: crypto.randomUUID(), name: "", orgs: [], token: "" }; - this.display(); + this.accountSettings.renderNewAccount(newAccountSection, this.saveNewAccount.bind(this)); }); }); @@ -90,144 +62,7 @@ export class GithubLinkPluginSettingsTab extends PluginSettingTab { }); }); - // TODO: Combine the new account and existing account rendering to reduce duplication - if (this.newAccount !== null) { - const accountContainer = containerEl.createDiv(); - const header = accountContainer.createEl("h3", { text: "New account" }); - - new Setting(accountContainer) - .setName("Account name") - .setDesc("Required.") - .addText((text) => { - text.setValue(this.newAccount!.name); - text.onChange((value) => { - this.newAccount!.name = value; - header.setText(value ?? "New account"); - }); - }) - .addButton((button) => { - button.setIcon("trash"); - button.setTooltip("Delete account"); - button.onClick(() => { - this.newAccount = null; - this.display(); - }); - }); - - new Setting(accountContainer) - .setName("Orgs and users") - .setDesc( - "A comma separated list of the GitHub organizations and users this account should be used for. Optional.", - ) - .addTextArea((text) => { - text.setValue(this.newAccount!.orgs.join(", ")); - text.onChange((value) => { - this.newAccount!.orgs = value.split(","); - }); - }); - - new Setting(accountContainer) - .setName("Token") - .setDesc( - "A GitHub token, which can be generated automatically (recommended) or by creating a personal access token (not recommended unless org does not allow OAuth tokens). Required.", - ) - .addButton((button) => { - button.setButtonText("Generate Token"); - button.onClick(async () => { - const authResult = await auth(this.tokenVerification.bind(this))({ - type: "oauth", - }); - this.authModal?.close(); - this.authModal = null; - this.newAccount!.token = authResult.token; - this.display(); - }); - }) - .addText((text) => { - text.setPlaceholder("Personal Access Token / OAuth Token"); - text.setValue(this.newAccount!.token); - text.onChange((value) => { - this.newAccount!.token = value; - }); - }); - - new Setting(accountContainer).addButton((button) => { - button.setButtonText("Save account"); - button.setTooltip("Save account"); - button.setIcon("save"); - button.onClick(async () => { - if (!this.newAccount?.name || !this.newAccount.token) { - return; - } - PluginSettings.accounts.unshift(this.newAccount); - await this.saveSettings(); - this.newAccount = null; - this.display(); - }); - }); - } - - for (const account of PluginSettings.accounts) { - const accountContainer = containerEl.createDiv(); - accountContainer.createEl("h3", { text: account.name }); - - new Setting(accountContainer) - .setName("Account name") - .addText((text) => { - text.setValue(account.name); - text.onChange((value) => { - account.name = value; - this.saveSettings(); - }); - }) - .addButton((button) => { - button.setIcon("trash"); - button.setTooltip("Delete account"); - button.onClick(async () => { - PluginSettings.accounts.remove(account); - await this.saveSettings(); - this.display(); - }); - }); - - new Setting(accountContainer) - .setName("Orgs and users") - .setDesc("A comma separated list of the GitHub organizations and users this account should be used for.") - .addTextArea((text) => { - text.setValue(account.orgs.join(", ")); - text.onChange((value) => { - account.orgs = value.split(",").map((org) => org.trim()); - this.saveSettings(); - }); - }); - - new Setting(accountContainer) - .setName("Token") - .setDesc( - "A GitHub token, which can be generated automatically (recommended) or by creating a personal access token (not recommended unless org does not allow OAuth tokens).", - ) - .addButton((button) => { - button.setButtonText("Generate Token"); - button.onClick(async () => { - const authResult = await auth(this.tokenVerification.bind(this))({ - type: "oauth", - }); - this.authModal?.close(); - this.authModal = null; - account.token = authResult.token; - await this.saveSettings(); - this.display(); - }); - }) - .addText((text) => { - text.setPlaceholder("Personal Access Token / OAuth Token"); - text.setValue(account.token); - text.onChange((value) => { - account.token = value; - this.saveSettings(); - }); - }); - } + this.accountSettings.render(PluginSettings.accounts); containerEl.createEl("h2", { text: "Other settings" }); @@ -374,7 +209,7 @@ export class GithubLinkPluginSettingsTab extends PluginSettingTab { button.setButtonText("Clear cache"); button.onClick(async () => { const itemsDeleted = getCache().clean(new Date()); - PluginSettings.cache = null; + PluginData.cache = null; await this.saveSettings(); new Notice(`Removed ${itemsDeleted} stored items from GitHub Link cache.`, 3000); }); @@ -408,11 +243,20 @@ export class GithubLinkPluginSettingsTab extends PluginSettingTab { } private saveSettings() { - return this.plugin.saveData(PluginSettings); + const newData: GithubLinkPluginData = { + cache: PluginData.cache, + settings: PluginSettings, + }; + return this.plugin.saveData(newData); } - private tokenVerification(verification: Verification) { - this.authModal = new AuthModal(this.app, verification); - this.authModal.open(); + private async removeAccount(account: GithubAccount): Promise { + PluginSettings.accounts.remove(account); + await this.saveSettings(); + } + + private async saveNewAccount(account: GithubAccount): Promise { + PluginSettings.accounts.unshift(account); + await this.saveSettings(); } } diff --git a/src/settings/types.ts b/src/settings/types.ts new file mode 100644 index 0000000..1e6a328 --- /dev/null +++ b/src/settings/types.ts @@ -0,0 +1,36 @@ +import { LogLevel } from "src/logger"; + +export interface GithubAccount { + id: string; + name: string; + orgs: string[]; + token: string; +} + +export interface GithubLinkPluginData { + settings: GithubLinkPluginSettings; + cache: string[] | null; +} + +export interface GithubLinkPluginSettings { + accounts: GithubAccount[]; + defaultAccount?: string; + defaultPageSize: number; + logLevel: LogLevel; + tagTooltips: boolean; + tagShowPRMergeable: boolean; + cacheIntervalSeconds: number; + maxCacheAgeHours: number; + minRequestSeconds: number; +} + +export const DEFAULT_SETTINGS: GithubLinkPluginSettings = { + accounts: [], + defaultPageSize: 10, + logLevel: LogLevel.Error, + tagTooltips: false, + tagShowPRMergeable: false, + cacheIntervalSeconds: 60, + maxCacheAgeHours: 120, + minRequestSeconds: 60, +};