mirror of
https://github.com/nathonius/obsidian-github-link.git
synced 2026-07-22 09:20:25 +00:00
working multi-account support and parsing of links
This commit is contained in:
parent
f2feb5d897
commit
5efda44294
7 changed files with 306 additions and 113 deletions
3
.prettierrc
Normal file
3
.prettierrc
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"printWidth": 120
|
||||
}
|
||||
|
|
@ -41,25 +41,12 @@ export async function githubRequest(config: RequestUrlParam, token?: string) {
|
|||
}
|
||||
}
|
||||
|
||||
export async function getIssue(
|
||||
org: string,
|
||||
repo: string,
|
||||
issue: number,
|
||||
token?: string
|
||||
) {
|
||||
const result = await githubRequest(
|
||||
{ url: `${baseApi}/repos/${org}/${repo}/issues/${issue}` },
|
||||
token
|
||||
);
|
||||
export async function getIssue(org: string, repo: string, issue: number, token?: string) {
|
||||
const result = await githubRequest({ url: `${baseApi}/repos/${org}/${repo}/issues/${issue}` }, token);
|
||||
return result.json as RestEndpointMethodTypes["issues"]["get"]["response"]["data"];
|
||||
}
|
||||
|
||||
export async function getPullRequest(
|
||||
org: string,
|
||||
repo: string,
|
||||
pr: number,
|
||||
token?: string
|
||||
) {
|
||||
export async function getPullRequest(org: string, repo: string, pr: number, token?: string) {
|
||||
const result = await githubRequest(
|
||||
{
|
||||
url: `${baseApi}/repos/${org}/${repo}/pulls/${pr}`,
|
||||
|
|
@ -69,13 +56,7 @@ export async function getPullRequest(
|
|||
return result.json as RestEndpointMethodTypes["pulls"]["get"]["response"]["data"];
|
||||
}
|
||||
|
||||
export async function getCode(
|
||||
org: string,
|
||||
repo: string,
|
||||
path: string,
|
||||
branch: string,
|
||||
token?: string
|
||||
) {
|
||||
export async function getCode(org: string, repo: string, path: string, branch: string, token?: string) {
|
||||
const result = await githubRequest(
|
||||
{
|
||||
url: `${baseApi}/repos/${org}/${repo}/contents/${path}?ref=${branch}`,
|
||||
|
|
@ -99,6 +80,7 @@ export function parseUrl(urlString: string): ParsedUrl {
|
|||
const parsedUrl: ParsedUrl = { url: urlString, host: url.hostname };
|
||||
|
||||
const urlParts = url.pathname.split("/");
|
||||
console.log(urlParts);
|
||||
if (urlParts.length >= 4) {
|
||||
switch (urlParts[3].toLowerCase()) {
|
||||
case "issues":
|
||||
|
|
@ -141,18 +123,14 @@ export function parseUrl(urlString: string): ParsedUrl {
|
|||
parsedUrl.org = urlParts[1];
|
||||
}
|
||||
|
||||
console.log(parsedUrl);
|
||||
return parsedUrl;
|
||||
}
|
||||
|
||||
async function doFetch(
|
||||
url: RequestInfo | URL,
|
||||
options?: RequestInit | undefined
|
||||
): Promise<Response> {
|
||||
async function doFetch(url: RequestInfo | URL, options?: RequestInit | undefined): Promise<Response> {
|
||||
// Octokit always uses a url + options, not a Request object
|
||||
if (typeof url !== "string") {
|
||||
throw new Error(
|
||||
"Something has gone horribly wrong and fetch has received unexpected arguments."
|
||||
);
|
||||
throw new Error("Something has gone horribly wrong and fetch has received unexpected arguments.");
|
||||
}
|
||||
if (options === undefined) {
|
||||
throw new Error("No options given to fetch.");
|
||||
|
|
@ -177,22 +155,12 @@ async function doFetch(
|
|||
json: () => Promise.resolve(result.json),
|
||||
} as Omit<
|
||||
Response,
|
||||
| "ok"
|
||||
| "body"
|
||||
| "statusText"
|
||||
| "redirected"
|
||||
| "type"
|
||||
| "clone"
|
||||
| "bodyUsed"
|
||||
| "blob"
|
||||
| "formData"
|
||||
"ok" | "body" | "statusText" | "redirected" | "type" | "clone" | "bodyUsed" | "blob" | "formData"
|
||||
>;
|
||||
return partialResult as Response;
|
||||
}
|
||||
|
||||
function getHeaders(
|
||||
headers: HeadersInit | undefined
|
||||
): Record<string, string> | undefined {
|
||||
function getHeaders(headers: HeadersInit | undefined): Record<string, string> | undefined {
|
||||
if (!headers) {
|
||||
return undefined;
|
||||
}
|
||||
|
|
@ -204,16 +172,12 @@ function getHeaders(
|
|||
return result;
|
||||
}
|
||||
if (Array.isArray(headers)) {
|
||||
throw new Error(
|
||||
"Got array headers, we don't know what to do with this yet."
|
||||
);
|
||||
throw new Error("Got array headers, we don't know what to do with this yet.");
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
function getBody(
|
||||
body: BodyInit | null | undefined
|
||||
): string | ArrayBuffer | undefined {
|
||||
function getBody(body: BodyInit | null | undefined): string | ArrayBuffer | undefined {
|
||||
if (!body) {
|
||||
return undefined;
|
||||
}
|
||||
|
|
|
|||
64
src/inline.ts
Normal file
64
src/inline.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import { MarkdownPostProcessorContext, setIcon } from "obsidian";
|
||||
import { getIssue, getPullRequest, parseUrl } from "./github";
|
||||
|
||||
import { PluginSettings } from "./plugin";
|
||||
|
||||
// import { PluginSettings } from "./plugin";
|
||||
|
||||
export async function InlineRenderer(el: HTMLElement, ctx: MarkdownPostProcessorContext) {
|
||||
const githubLinks = el.querySelectorAll<HTMLAnchorElement>(`a.external-link[href^="https://github.com"]`);
|
||||
for (const anchor of Array.from(githubLinks)) {
|
||||
const url = anchor.href;
|
||||
const parsedUrl = parseUrl(url);
|
||||
|
||||
let token: string | undefined;
|
||||
// Try and find the matching token
|
||||
if (parsedUrl.org) {
|
||||
let account = PluginSettings.accounts.find((acc) => acc.orgs.some((org) => org === parsedUrl.org));
|
||||
// Fall back to default token if available
|
||||
if (!account && PluginSettings.defaultAccount) {
|
||||
account = PluginSettings.accounts.find((acc) => acc.id === PluginSettings.defaultAccount);
|
||||
}
|
||||
token = account?.token;
|
||||
}
|
||||
|
||||
const container = createEl("a", { cls: "gh-link-inline", href: url });
|
||||
const icon = container.createSpan({ cls: "gh-link-inline-icon" });
|
||||
setIcon(icon, "github"); // TODO: Set correct icon
|
||||
if (parsedUrl.repo) {
|
||||
container.createSpan({
|
||||
cls: "gh-link-inline-repo",
|
||||
text: parsedUrl.repo,
|
||||
});
|
||||
} else if (parsedUrl.org) {
|
||||
container.createSpan({
|
||||
cls: "gh-link-inline-org",
|
||||
text: parsedUrl.org,
|
||||
});
|
||||
}
|
||||
if (parsedUrl.repo && parsedUrl.org) {
|
||||
if (parsedUrl.issue !== undefined) {
|
||||
const issue = await getIssue(parsedUrl.org, parsedUrl.repo, parsedUrl.issue, token);
|
||||
if (issue.title) {
|
||||
setIcon(icon, "square-dot");
|
||||
container.createSpan({
|
||||
cls: "gh-link-inline-issue-title",
|
||||
text: issue.title,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (parsedUrl.pr !== undefined) {
|
||||
const pull = await getPullRequest(parsedUrl.org, parsedUrl.repo, parsedUrl.pr, token);
|
||||
if (pull.title) {
|
||||
setIcon(icon, "git-pull-request-arrow");
|
||||
container.createSpan({
|
||||
cls: "gh-link-inline-pr-title",
|
||||
text: pull.title,
|
||||
});
|
||||
}
|
||||
}
|
||||
// TODO: add support for other stuff here
|
||||
}
|
||||
anchor.replaceWith(container);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,25 +1,21 @@
|
|||
import {
|
||||
GithubLinkPluginSettings,
|
||||
GithubLinkPluginSettingsTab,
|
||||
} from "./settings";
|
||||
import { GithubLinkPluginSettings, GithubLinkPluginSettingsTab } from "./settings";
|
||||
|
||||
import { InlineRenderer } from "./inline";
|
||||
import { Plugin } from "obsidian";
|
||||
import { getIssue } from "./github";
|
||||
|
||||
export let PluginSettings: GithubLinkPluginSettings = { accounts: [] };
|
||||
|
||||
export class GithubLinkPlugin extends Plugin {
|
||||
public settings: GithubLinkPluginSettings = {};
|
||||
async onload() {
|
||||
this.settings = Object.assign({}, await this.loadData());
|
||||
PluginSettings = Object.assign({}, await this.loadData());
|
||||
this.addSettingTab(new GithubLinkPluginSettingsTab(this.app, this));
|
||||
this.registerMarkdownPostProcessor(InlineRenderer);
|
||||
this.addCommand({
|
||||
id: "get-issue",
|
||||
name: "Get GitHub issue",
|
||||
callback: async () => {
|
||||
const result = await getIssue(
|
||||
"nathonius",
|
||||
"obsidian-trello",
|
||||
4
|
||||
);
|
||||
const result = await getIssue("nathonius", "obsidian-trello", 4);
|
||||
console.log(result);
|
||||
},
|
||||
});
|
||||
|
|
|
|||
208
src/settings.ts
208
src/settings.ts
|
|
@ -1,16 +1,26 @@
|
|||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import { App, PluginSettingTab, Setting } from "obsidian";
|
||||
import { GithubLinkPlugin, PluginSettings } from "./plugin";
|
||||
|
||||
import { AuthModal } from "./auth-modal";
|
||||
import { GithubLinkPlugin } from "./plugin";
|
||||
import { Verification } from "@octokit/auth-oauth-device/dist-types/types";
|
||||
import { auth } from "./github";
|
||||
|
||||
export interface GithubAccount {
|
||||
id: string;
|
||||
name: string;
|
||||
orgs: string[];
|
||||
token: string;
|
||||
}
|
||||
|
||||
export interface GithubLinkPluginSettings {
|
||||
token?: string;
|
||||
accounts: GithubAccount[];
|
||||
defaultAccount?: string;
|
||||
}
|
||||
|
||||
export class GithubLinkPluginSettingsTab extends PluginSettingTab {
|
||||
authModal: AuthModal | null = null;
|
||||
newAccount: GithubAccount | null = null;
|
||||
|
||||
constructor(public app: App, private readonly plugin: GithubLinkPlugin) {
|
||||
super(app, plugin);
|
||||
|
|
@ -21,41 +31,175 @@ export class GithubLinkPluginSettingsTab extends PluginSettingTab {
|
|||
containerEl.empty();
|
||||
containerEl.createEl("h2", { text: "GitHub authentication" });
|
||||
|
||||
containerEl.createEl(
|
||||
"p",
|
||||
"No authentication is required to reference public repositories. Providing a token allows referencing private repos, but the token is stored in plain text."
|
||||
);
|
||||
containerEl.createEl("p", {
|
||||
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("Token")
|
||||
.setDesc(
|
||||
"A GitHub token, which can be generated automatically (recommended) or by creating a personal access token (not recommended)."
|
||||
)
|
||||
.addButton((button) => {
|
||||
button.setButtonText("Generate Token");
|
||||
button.onClick(this.generateToken.bind(this));
|
||||
})
|
||||
.addText((text) => {
|
||||
text.setPlaceholder("Personal Access Token");
|
||||
if (this.plugin.settings.token) {
|
||||
text.setValue(this.plugin.settings.token);
|
||||
}
|
||||
new Setting(containerEl).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();
|
||||
});
|
||||
});
|
||||
new Setting(containerEl)
|
||||
.setName("Default account")
|
||||
.setDesc("The account that will be used if no other users or organizations match.")
|
||||
.addDropdown((dropdown) => {
|
||||
const options = PluginSettings.accounts.reduce<Record<string, string>>((acc, account) => {
|
||||
acc[account.id] = account.name;
|
||||
return acc;
|
||||
}, {});
|
||||
dropdown.addOptions(options);
|
||||
dropdown.setValue(PluginSettings.defaultAccount ?? "");
|
||||
dropdown.onChange(async (value) => {
|
||||
console.log(value);
|
||||
const selectedAccount = PluginSettings.accounts.find((acc) => acc.id === value);
|
||||
if (selectedAccount) {
|
||||
PluginSettings.defaultAccount = selectedAccount.id;
|
||||
await this.saveSettings();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 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.setTooltip("Save account");
|
||||
button.setIcon("save");
|
||||
button.onClick(async () => {
|
||||
if (!this.newAccount || !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();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private saveSettings() {
|
||||
return this.plugin.saveData(this.plugin.settings);
|
||||
}
|
||||
|
||||
private async generateToken() {
|
||||
const authResult = await auth(this.tokenVerification.bind(this))({
|
||||
type: "oauth",
|
||||
});
|
||||
this.authModal?.close();
|
||||
this.authModal = null;
|
||||
this.plugin.settings.token = authResult.token;
|
||||
await this.saveSettings();
|
||||
this.display();
|
||||
return this.plugin.saveData(PluginSettings);
|
||||
}
|
||||
|
||||
private tokenVerification(verification: Verification) {
|
||||
|
|
|
|||
28
styles.css
28
styles.css
|
|
@ -14,3 +14,31 @@
|
|||
.auth-code {
|
||||
font-size: 3em;
|
||||
}
|
||||
|
||||
.gh-link-inline {
|
||||
display: flex;
|
||||
background-color: green;
|
||||
border-radius: 4px;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.gh-link-inline-icon {
|
||||
background-color: orange;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.gh-link-inline-repo {
|
||||
background-color: blue;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.gh-link-inline-pr-title {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-wrap: nowrap;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,24 +1,18 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"inlineSourceMap": true,
|
||||
"inlineSources": true,
|
||||
"module": "ESNext",
|
||||
"target": "ES6",
|
||||
"allowJs": true,
|
||||
"noImplicitAny": true,
|
||||
"moduleResolution": "node",
|
||||
"importHelpers": true,
|
||||
"isolatedModules": true,
|
||||
"strictNullChecks": true,
|
||||
"lib": [
|
||||
"DOM",
|
||||
"ES5",
|
||||
"ES6",
|
||||
"ES7"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts"
|
||||
]
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"inlineSourceMap": true,
|
||||
"inlineSources": true,
|
||||
"module": "ESNext",
|
||||
"target": "ES6",
|
||||
"allowJs": true,
|
||||
"noImplicitAny": true,
|
||||
"moduleResolution": "node",
|
||||
"importHelpers": true,
|
||||
"isolatedModules": true,
|
||||
"strict": true,
|
||||
"strictNullChecks": true,
|
||||
"lib": ["DOM", "ES5", "ES6", "ES7"]
|
||||
},
|
||||
"include": ["**/*.ts"]
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue