mirror of
https://github.com/nathonius/obsidian-github-link.git
synced 2026-07-22 09:20:25 +00:00
✨ feat: Basic query search support
Still a lot to do, but able to render a table with info from a search of issues
This commit is contained in:
parent
39ae421590
commit
69014fb27e
12 changed files with 4529 additions and 14 deletions
162
.better-commits.json
Normal file
162
.better-commits.json
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
{
|
||||
"check_status": true,
|
||||
"commit_type": {
|
||||
"enable": true,
|
||||
"initial_value": "feat",
|
||||
"infer_type_from_branch": true,
|
||||
"append_emoji_to_label": true,
|
||||
"append_emoji_to_commit": true,
|
||||
"options": [
|
||||
{
|
||||
"value": "feat",
|
||||
"label": "feat",
|
||||
"hint": "A new feature",
|
||||
"emoji": "✨",
|
||||
"trailer": "Changelog: feature"
|
||||
},
|
||||
{
|
||||
"value": "fix",
|
||||
"label": "fix",
|
||||
"hint": "A bug fix",
|
||||
"emoji": "🐛",
|
||||
"trailer": "Changelog: fix"
|
||||
},
|
||||
{
|
||||
"value": "docs",
|
||||
"label": "docs",
|
||||
"hint": "Documentation only changes",
|
||||
"emoji": "📚",
|
||||
"trailer": "Changelog: documentation"
|
||||
},
|
||||
{
|
||||
"value": "refactor",
|
||||
"label": "refactor",
|
||||
"hint": "A code change that neither fixes a bug nor adds a feature",
|
||||
"emoji": "🔨",
|
||||
"trailer": "Changelog: refactor"
|
||||
},
|
||||
{
|
||||
"value": "release",
|
||||
"label": "release",
|
||||
"hint": "A code change to release a new version",
|
||||
"emoji": "🚀",
|
||||
"trailer": "Changelog: release"
|
||||
},
|
||||
{
|
||||
"value": "test",
|
||||
"label": "test",
|
||||
"hint": "Adding missing tests or correcting existing tests",
|
||||
"emoji": "🚨",
|
||||
"trailer": "Changelog: test"
|
||||
},
|
||||
{
|
||||
"value": "build",
|
||||
"label": "build",
|
||||
"hint": "Changes that affect the build system or external dependencies",
|
||||
"emoji": "🚧",
|
||||
"trailer": "Changelog: build"
|
||||
},
|
||||
{
|
||||
"value": "ci",
|
||||
"label": "ci",
|
||||
"hint": "Changes to our CI configuration files and scripts",
|
||||
"emoji": "🤖",
|
||||
"trailer": "Changelog: ci"
|
||||
},
|
||||
{
|
||||
"value": "chore",
|
||||
"label": "chore",
|
||||
"hint": "Other changes that do not modify src or test files",
|
||||
"emoji": "🧹",
|
||||
"trailer": "Changelog: chore"
|
||||
},
|
||||
{
|
||||
"value": "",
|
||||
"label": "none"
|
||||
}
|
||||
]
|
||||
},
|
||||
"commit_scope": {
|
||||
"enable": false,
|
||||
"custom_scope": false,
|
||||
"initial_value": "app",
|
||||
"options": [
|
||||
{
|
||||
"value": "app",
|
||||
"label": "app"
|
||||
},
|
||||
{
|
||||
"value": "shared",
|
||||
"label": "shared"
|
||||
},
|
||||
{
|
||||
"value": "server",
|
||||
"label": "server"
|
||||
},
|
||||
{
|
||||
"value": "tools",
|
||||
"label": "tools"
|
||||
},
|
||||
{
|
||||
"value": "",
|
||||
"label": "none"
|
||||
}
|
||||
]
|
||||
},
|
||||
"check_ticket": {
|
||||
"infer_ticket": true,
|
||||
"confirm_ticket": true,
|
||||
"add_to_title": true,
|
||||
"append_hashtag": false,
|
||||
"surround": "",
|
||||
"title_position": "start"
|
||||
},
|
||||
"commit_title": {
|
||||
"max_size": 70
|
||||
},
|
||||
"commit_body": {
|
||||
"enable": true,
|
||||
"required": false
|
||||
},
|
||||
"commit_footer": {
|
||||
"enable": true,
|
||||
"initial_value": [],
|
||||
"options": ["closes", "trailer", "breaking-change", "deprecated", "custom"]
|
||||
},
|
||||
"breaking_change": {
|
||||
"add_exclamation_to_title": true
|
||||
},
|
||||
"confirm_commit": true,
|
||||
"print_commit_output": true,
|
||||
"branch_pre_commands": [],
|
||||
"branch_post_commands": [],
|
||||
"worktree_pre_commands": [],
|
||||
"worktree_post_commands": [],
|
||||
"branch_user": {
|
||||
"enable": true,
|
||||
"required": false,
|
||||
"separator": "/"
|
||||
},
|
||||
"branch_type": {
|
||||
"enable": true,
|
||||
"separator": "/"
|
||||
},
|
||||
"branch_version": {
|
||||
"enable": false,
|
||||
"required": false,
|
||||
"separator": "/"
|
||||
},
|
||||
"branch_ticket": {
|
||||
"enable": true,
|
||||
"required": false,
|
||||
"separator": "-"
|
||||
},
|
||||
"branch_description": {
|
||||
"max_length": 70,
|
||||
"separator": ""
|
||||
},
|
||||
"branch_action_default": "branch",
|
||||
"branch_order": ["user", "version", "type", "ticket", "description"],
|
||||
"enable_worktrees": true,
|
||||
"overrides": {}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import type { CodeResponse, IssueResponse, PullResponse } from "./response";
|
||||
import type { CodeResponse, IssueResponse, PullResponse, SearchIssueResponse, SearchRepoResponse } from "./response";
|
||||
|
||||
import type { RequestUrlParam } from "obsidian";
|
||||
import { requestUrl } from "obsidian";
|
||||
|
|
@ -48,8 +48,20 @@ async function getCode(org: string, repo: string, path: string, branch: string,
|
|||
return result.json as CodeResponse;
|
||||
}
|
||||
|
||||
async function searchRepos(query: string, token?: string): Promise<SearchRepoResponse> {
|
||||
const result = await githubRequest({ url: `${baseApi}/search/repositories?q=${encodeURIComponent(query)}` }, token);
|
||||
return result.json as SearchRepoResponse;
|
||||
}
|
||||
|
||||
async function searchIssues(query: string, token?: string): Promise<SearchIssueResponse> {
|
||||
const result = await githubRequest({ url: `${baseApi}/search/issues?q=${encodeURIComponent(query)}` }, token);
|
||||
return result.json as SearchIssueResponse;
|
||||
}
|
||||
|
||||
export const api = {
|
||||
getIssue,
|
||||
getPullRequest,
|
||||
getCode,
|
||||
searchIssues,
|
||||
searchRepos,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { IssueResponse, PullResponse } from "./response";
|
||||
import type { IssueResponse, PullResponse, SearchIssueResponse, SearchRepoResponse } from "./response";
|
||||
|
||||
class CacheEntry<T> {
|
||||
constructor(
|
||||
|
|
@ -14,6 +14,11 @@ class CacheEntry<T> {
|
|||
}
|
||||
}
|
||||
|
||||
class QueryCache {
|
||||
public readonly issueCache: Record<string, CacheEntry<SearchIssueResponse>> = {};
|
||||
public readonly repoCache: Record<string, CacheEntry<SearchRepoResponse>> = {};
|
||||
}
|
||||
|
||||
class RepoCache {
|
||||
public readonly issueCache: Record<number, CacheEntry<IssueResponse>> = {};
|
||||
public readonly pullCache: Record<string, CacheEntry<PullResponse>> = {};
|
||||
|
|
@ -25,6 +30,7 @@ class OrgCache {
|
|||
|
||||
export class Cache {
|
||||
public readonly orgs: Record<string, OrgCache> = {};
|
||||
public readonly queries = new QueryCache();
|
||||
|
||||
getIssue(org: string, repo: string, issue: number): IssueResponse | null {
|
||||
const repoCache = this.getRepoCache(org, repo);
|
||||
|
|
@ -60,6 +66,22 @@ export class Cache {
|
|||
}
|
||||
}
|
||||
|
||||
getIssueQuery(query: string): SearchIssueResponse | null {
|
||||
return this.getCacheValue(this.queries.issueCache[query] ?? null);
|
||||
}
|
||||
|
||||
setIssueQuery(query: string, result: SearchIssueResponse): void {
|
||||
this.queries.issueCache[query] = new CacheEntry<SearchIssueResponse>(result);
|
||||
}
|
||||
|
||||
getRepoQuery(query: string): SearchRepoResponse | null {
|
||||
return this.getCacheValue(this.queries.repoCache[query] ?? null);
|
||||
}
|
||||
|
||||
setRepoQuery(query: string, result: SearchRepoResponse): void {
|
||||
this.queries.repoCache[query] = new CacheEntry<SearchRepoResponse>(result);
|
||||
}
|
||||
|
||||
private getRepoCache(org: string, repo: string) {
|
||||
let orgCache = this.orgs[org];
|
||||
if (!orgCache) {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import type { IssueResponse, PullResponse } from "./response";
|
||||
import type { IssueResponse, PullResponse, SearchIssueResponse, SearchRepoResponse } from "./response";
|
||||
|
||||
import { Cache } from "./cache";
|
||||
import { api } from "./api";
|
||||
import { PluginSettings } from "src/plugin";
|
||||
import { api } from "./api";
|
||||
|
||||
const cache = new Cache();
|
||||
|
||||
function getToken(org: string): string | undefined {
|
||||
function getToken(org?: string): string | undefined {
|
||||
const account =
|
||||
PluginSettings.accounts.find((acc) => acc.orgs.some((savedOrg) => savedOrg === org)) ??
|
||||
PluginSettings.accounts.find((acc) => acc.id === PluginSettings.defaultAccount);
|
||||
|
|
@ -34,3 +34,25 @@ export async function getPullRequest(org: string, repo: string, pullRequest: num
|
|||
cache.setPullRequest(org, repo, response);
|
||||
return response;
|
||||
}
|
||||
|
||||
export async function searchIssues(query: string, org?: string): Promise<SearchIssueResponse> {
|
||||
const cachedResponse = cache.getIssueQuery(query);
|
||||
if (cachedResponse) {
|
||||
return Promise.resolve(cachedResponse);
|
||||
}
|
||||
|
||||
const response = await api.searchIssues(query, getToken(org));
|
||||
cache.setIssueQuery(query, response);
|
||||
return response;
|
||||
}
|
||||
|
||||
export async function searchRepos(query: string, org?: string): Promise<SearchRepoResponse> {
|
||||
const cachedResponse = cache.getRepoQuery(query);
|
||||
if (cachedResponse) {
|
||||
return Promise.resolve(cachedResponse);
|
||||
}
|
||||
|
||||
const response = await api.searchRepos(query, getToken(org));
|
||||
cache.setRepoQuery(query, response);
|
||||
return response;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,15 @@
|
|||
import type { RestEndpointMethodTypes } from "@octokit/plugin-rest-endpoint-methods";
|
||||
|
||||
export enum IssueStatus {
|
||||
Open = "open",
|
||||
Closed = "closed",
|
||||
Done = "done",
|
||||
}
|
||||
|
||||
export type IssueResponse = RestEndpointMethodTypes["issues"]["get"]["response"]["data"];
|
||||
export type PullResponse = RestEndpointMethodTypes["pulls"]["get"]["response"]["data"];
|
||||
export type CodeResponse = RestEndpointMethodTypes["repos"]["getContent"]["response"]["data"];
|
||||
export type SearchRepoParams = RestEndpointMethodTypes["search"]["repos"]["parameters"];
|
||||
export type SearchRepoResponse = RestEndpointMethodTypes["search"]["repos"]["response"]["data"];
|
||||
export type SearchIssueParams = RestEndpointMethodTypes["search"]["issuesAndPullRequests"]["parameters"];
|
||||
export type SearchIssueResponse = RestEndpointMethodTypes["search"]["issuesAndPullRequests"]["response"]["data"];
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ import type { GithubLinkPluginSettings } from "./settings";
|
|||
import { GithubLinkPluginSettingsTab } from "./settings";
|
||||
import { InlineRenderer } from "./inline/inline";
|
||||
import { Plugin } from "obsidian";
|
||||
import { QueryProcessor } from "./query/processor";
|
||||
import { createInlineViewPlugin } from "./inline/view-plugin";
|
||||
import { getIssue } from "./github/github";
|
||||
|
||||
export let PluginSettings: GithubLinkPluginSettings = { accounts: [] };
|
||||
|
||||
|
|
@ -13,13 +13,6 @@ export class GithubLinkPlugin extends Plugin {
|
|||
this.addSettingTab(new GithubLinkPluginSettingsTab(this.app, this));
|
||||
this.registerMarkdownPostProcessor(InlineRenderer);
|
||||
this.registerEditorExtension(createInlineViewPlugin(this));
|
||||
this.addCommand({
|
||||
id: "get-issue",
|
||||
name: "Get GitHub issue",
|
||||
callback: async () => {
|
||||
const result = await getIssue("nathonius", "obsidian-trello", 4);
|
||||
console.log(result);
|
||||
},
|
||||
});
|
||||
this.registerMarkdownCodeBlockProcessor("github-query", QueryProcessor);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
24
src/query/output.ts
Normal file
24
src/query/output.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import type { SearchIssueResponse, SearchRepoResponse } from "src/github/response";
|
||||
|
||||
import type { TableParams } from "./params";
|
||||
|
||||
export function renderTable<T extends SearchIssueResponse | SearchRepoResponse>(
|
||||
params: TableParams,
|
||||
result: T,
|
||||
el: HTMLElement,
|
||||
) {
|
||||
const table = el.createEl("table", { cls: "github-link-query-table" });
|
||||
const thead = table.createEl("thead");
|
||||
for (const col of params.columns) {
|
||||
thead.createEl("th", { text: col });
|
||||
}
|
||||
const tbody = table.createEl("tbody");
|
||||
for (const row of result.items) {
|
||||
const tr = tbody.createEl("tr");
|
||||
for (const col of params.columns) {
|
||||
const cell = tr.createEl("td");
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
cell.setText((row as any)[col] ?? "");
|
||||
}
|
||||
}
|
||||
}
|
||||
50
src/query/params.ts
Normal file
50
src/query/params.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import type { SearchIssueParams, SearchRepoParams } from "src/github/response";
|
||||
|
||||
import { parseYaml } from "obsidian";
|
||||
|
||||
export enum OutputType {
|
||||
Table = "table",
|
||||
}
|
||||
|
||||
export enum QueryType {
|
||||
PullRequest = "pull-request",
|
||||
Issue = "issue",
|
||||
Repo = "repo",
|
||||
}
|
||||
|
||||
export interface BaseParams {
|
||||
outputType: OutputType;
|
||||
queryType: QueryType;
|
||||
query: string;
|
||||
}
|
||||
|
||||
export type PullRequestParams = Omit<SearchIssueParams, "q"> & BaseParams;
|
||||
export type IssueParams = Omit<SearchIssueParams, "q"> & BaseParams;
|
||||
export type RepoParams = Omit<SearchRepoParams, "q"> & BaseParams;
|
||||
|
||||
export type TableParams = { columns: string[] } & BaseParams;
|
||||
|
||||
export function processParams(source: string): BaseParams | null {
|
||||
let params: BaseParams;
|
||||
try {
|
||||
params = parseYaml(source);
|
||||
} catch (e) {
|
||||
console.error(`Github Link: YAML Parsing failed, attempting simplistic parsing\n${e}`);
|
||||
params = Object.fromEntries(source.split("\n").map((l) => l.split(/:\s?/)));
|
||||
}
|
||||
|
||||
return params ?? null;
|
||||
}
|
||||
|
||||
export function isPullRequestParams(params: BaseParams): params is PullRequestParams {
|
||||
return params.queryType === QueryType.PullRequest;
|
||||
}
|
||||
export function isIssueParams(params: BaseParams): params is IssueParams {
|
||||
return params.queryType === QueryType.Issue;
|
||||
}
|
||||
export function isRepoParams(params: BaseParams): params is RepoParams {
|
||||
return params.queryType === QueryType.Repo;
|
||||
}
|
||||
export function isTableParams(params: BaseParams): params is TableParams {
|
||||
return params.outputType === OutputType.Table;
|
||||
}
|
||||
25
src/query/processor.ts
Normal file
25
src/query/processor.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { type MarkdownPostProcessorContext } from "obsidian";
|
||||
import { isTableParams, processParams } from "./params";
|
||||
import samplePRResponse from "./samplePRResponse";
|
||||
import { renderTable } from "./output";
|
||||
import type { SearchIssueResponse } from "src/github/response";
|
||||
|
||||
export async function QueryProcessor(
|
||||
source: string,
|
||||
el: HTMLElement,
|
||||
_ctx: MarkdownPostProcessorContext,
|
||||
): Promise<void> {
|
||||
const params = processParams(source);
|
||||
|
||||
if (!params) {
|
||||
// TODO: show an error instead
|
||||
el.setText(source);
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Get result
|
||||
|
||||
if (isTableParams(params)) {
|
||||
renderTable(params, samplePRResponse as SearchIssueResponse, el);
|
||||
}
|
||||
}
|
||||
2047
src/query/sampleIssueResponse.ts
Normal file
2047
src/query/sampleIssueResponse.ts
Normal file
File diff suppressed because it is too large
Load diff
2126
src/query/samplePRResponse.ts
Normal file
2126
src/query/samplePRResponse.ts
Normal file
File diff suppressed because one or more lines are too long
22
src/util.ts
22
src/util.ts
|
|
@ -1,3 +1,25 @@
|
|||
export function valueWithin(value: number, min: number, max: number) {
|
||||
return value >= min && value <= max;
|
||||
}
|
||||
|
||||
export function safeJSONParse<T>(value: string, props: Record<keyof T, boolean>): T | null {
|
||||
// Handle parsing with try / catch
|
||||
let parsed: T;
|
||||
try {
|
||||
parsed = JSON.parse(value);
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Validate object shape
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const result: any = {};
|
||||
for (const [_prop, include] of Object.entries(props)) {
|
||||
const prop = _prop as keyof T;
|
||||
if (include && parsed[prop as keyof T]) {
|
||||
result[prop] = parsed[prop];
|
||||
}
|
||||
}
|
||||
|
||||
return result as T;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue