mirror of
https://github.com/nathonius/obsidian-github-link.git
synced 2026-07-22 09:20:25 +00:00
332 lines
9.2 KiB
TypeScript
332 lines
9.2 KiB
TypeScript
import { setIcon } from "obsidian";
|
|
import { searchIssues, getIssuesForRepo, getMyIssues, getPullRequestsForRepo } from "src/github/github";
|
|
import type {
|
|
IssueListParams,
|
|
IssueListResponse,
|
|
IssueSearchParams,
|
|
IssueSearchResponse,
|
|
MaybePaginated,
|
|
PaginationMeta,
|
|
PullListParams,
|
|
PullListResponse,
|
|
} from "src/github/response";
|
|
import { parseYaml } from "obsidian";
|
|
import { PluginSettings, logger } from "src/plugin";
|
|
import { ALL_COLUMNS, DEFAULT_COLUMNS } from "./column/defaults";
|
|
import { getProp, isEqual, titleCase } from "src/util";
|
|
|
|
export type TableResult = IssueSearchResponse["items"] | IssueListResponse | PullListResponse;
|
|
|
|
export enum OutputType {
|
|
Table = "table",
|
|
}
|
|
|
|
export enum QueryType {
|
|
PullRequest = "pull-request",
|
|
Issue = "issue",
|
|
Repo = "repo",
|
|
}
|
|
|
|
export interface BaseParams {
|
|
refresh?: boolean;
|
|
}
|
|
|
|
export function searchSortFromQuery(params: QueryParams): IssueSearchParams["sort"] {
|
|
if (params.sort !== "popularity" && params.sort !== "long-running") {
|
|
return params.sort;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
export function issueListSortFromQuery(params: QueryParams): IssueListParams["sort"] {
|
|
if (params.sort && ["created", "updated", "comments"].includes(params.sort)) {
|
|
return params.sort as IssueListParams["sort"];
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
export function pullListSortFromQuery(params: QueryParams): PullListParams["sort"] {
|
|
if (params.sort && ["created", "updated", "popularity", "long-running"].includes(params.sort)) {
|
|
return params.sort as PullListParams["sort"];
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
/**
|
|
* Not all fields are supported by all query types
|
|
*/
|
|
export interface QueryParams {
|
|
outputType: OutputType;
|
|
queryType: QueryType;
|
|
columns: string[];
|
|
|
|
/**
|
|
* Custom query. This will override most other options.
|
|
*/
|
|
query?: string;
|
|
|
|
/**
|
|
* Pagination page size
|
|
*/
|
|
per_page?: number;
|
|
|
|
/**
|
|
* Pagination page number
|
|
*/
|
|
page?: number;
|
|
|
|
/**
|
|
* Repository name
|
|
*/
|
|
repo?: string;
|
|
|
|
/**
|
|
* Organization or user name
|
|
*/
|
|
org?: string;
|
|
milestone?: string;
|
|
state?: "open" | "closed" | "all";
|
|
assignee?: "none" | "*" | string;
|
|
creator?: string;
|
|
mentioned?: string;
|
|
labels?: string | string[];
|
|
|
|
/**
|
|
* "comments" - Issues only
|
|
* "popularity", "long-running" - Pull requests only
|
|
* "reactions" and "interactions" - Search only
|
|
*/
|
|
sort?:
|
|
| "created"
|
|
| "updated"
|
|
| "comments"
|
|
| "popularity"
|
|
| "long-running"
|
|
| "reactions"
|
|
| "reactions-+1"
|
|
| "reactions--1"
|
|
| "reactions-smile"
|
|
| "reactions-thinking_face"
|
|
| "reactions-heart"
|
|
| "reactions-tada"
|
|
| "interactions";
|
|
|
|
/**
|
|
* Sort direction, for most queries
|
|
*/
|
|
direction?: "asc" | "desc";
|
|
/**
|
|
* Sort direction, for search queries
|
|
*/
|
|
order?: "asc" | "desc";
|
|
since?: string;
|
|
|
|
/**
|
|
* Issue filter type
|
|
*/
|
|
filter?: "assigned" | "created" | "mentioned" | "subscribed" | "repos" | "all";
|
|
|
|
/**
|
|
* Pull request branch
|
|
*/
|
|
head?: string;
|
|
|
|
/**
|
|
* Pull request target
|
|
*/
|
|
base?: string;
|
|
}
|
|
|
|
export class GithubQuery {
|
|
private params!: QueryParams;
|
|
private result: TableResult | null = null;
|
|
private resultMeta: PaginationMeta | null = null;
|
|
|
|
constructor(private readonly hostElement: HTMLElement) {}
|
|
|
|
public async init(source: string): Promise<void> {
|
|
const parsedParams = this.parseCodeblock(source);
|
|
if (!parsedParams) {
|
|
console.error(`Github Link: simplistic parsing failed`);
|
|
} else {
|
|
await this.setParams(parsedParams);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Setting the parameters triggers calling the API
|
|
*/
|
|
public async setParams(newParams: QueryParams = this.params, forceUpdate = false): Promise<void> {
|
|
const currentParams = this.params;
|
|
this.params = newParams;
|
|
if (forceUpdate || !isEqual(currentParams, newParams)) {
|
|
const result = await this.executeQuery(forceUpdate);
|
|
if (result) {
|
|
this.setResult(result.response, result.meta);
|
|
}
|
|
}
|
|
}
|
|
|
|
public setResult(result: TableResult, meta: PaginationMeta): void {
|
|
this.result = result;
|
|
this.resultMeta = meta;
|
|
this.render();
|
|
}
|
|
|
|
public parseCodeblock(source: string): QueryParams | null {
|
|
let params: QueryParams | null;
|
|
try {
|
|
params = parseYaml(source) as QueryParams;
|
|
} catch (e) {
|
|
console.error(`Github Link: YAML Parsing failed, attempting simplistic parsing`);
|
|
console.error(e);
|
|
params = Object.fromEntries(source.split("\n").map((l) => l.split(/:\s?/))) as QueryParams;
|
|
}
|
|
return params ?? null;
|
|
}
|
|
|
|
public async executeQuery(skipCache = false): Promise<MaybePaginated<TableResult> | null> {
|
|
const params = this.params;
|
|
if (params.outputType === OutputType.Table) {
|
|
// Custom Query
|
|
if (params.query && (params.queryType === QueryType.Issue || params.queryType === QueryType.PullRequest)) {
|
|
const { meta, response } = await searchIssues(params, params.query, params.org, skipCache);
|
|
return { meta, response: response.items };
|
|
}
|
|
// Issue query with org and repo provided
|
|
else if (params.queryType === QueryType.Issue && params.org && params.repo) {
|
|
return await getIssuesForRepo(params, params.org, params.repo, skipCache);
|
|
}
|
|
// Issue query without org or repo provided
|
|
else if (params.queryType === QueryType.Issue) {
|
|
return await getMyIssues(params, params.org, skipCache);
|
|
}
|
|
// Pull request query with org and repo provided
|
|
else if (params.queryType === QueryType.PullRequest && params.org && params.repo) {
|
|
return await getPullRequestsForRepo(params, params.org, params.repo, skipCache);
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public render(): void {
|
|
if (!this.result) {
|
|
throw new Error("Attempted to render table before there was a result.");
|
|
}
|
|
const params = this.params;
|
|
const el = this.hostElement;
|
|
const result = this.result;
|
|
const meta = this.resultMeta;
|
|
|
|
el.empty();
|
|
const tableWrapper = el.createDiv({ cls: "github-link-table-wrapper" });
|
|
const tableScrollWrapper = tableWrapper.createDiv({ cls: "github-link-table-scroll-wrapper" });
|
|
const table = tableScrollWrapper.createEl("table", { cls: "github-link-table" });
|
|
|
|
// Create footer
|
|
const footer = tableWrapper.createDiv({ cls: "github-link-table-footer" });
|
|
|
|
// Add external link to footer if available
|
|
const externalLink = this.getExternalLink(params, result);
|
|
if (externalLink) {
|
|
footer.createEl("a", {
|
|
cls: "github-link-table-footer-external-link",
|
|
text: "View on GitHub",
|
|
href: externalLink,
|
|
attr: { target: "_blank" },
|
|
});
|
|
}
|
|
|
|
// Add pagination to footer if enabled
|
|
logger.debug("Meta for query");
|
|
logger.debug(meta);
|
|
if (PluginSettings.showPagination && this.hasSomeRel(meta)) {
|
|
const pagination = footer.createDiv({ cls: "github-link-table-pagination" });
|
|
|
|
// First, previous
|
|
if (meta?.first && (!meta.prev || meta.prev.page !== meta.first.page)) {
|
|
const first = pagination.createEl("a", { text: "<<", href: "#", attr: { role: "button" } });
|
|
first.addEventListener("click", () => {
|
|
void this.setParams({ ...this.params, page: meta.first?.page });
|
|
});
|
|
}
|
|
if (meta?.prev) {
|
|
const prev = pagination.createEl("a", { text: meta.prev.page.toString(), href: "#", attr: { role: "button" } });
|
|
prev.addEventListener("click", () => {
|
|
void this.setParams({ ...this.params, page: meta.prev?.page });
|
|
});
|
|
}
|
|
|
|
// Current Page
|
|
pagination.createSpan({ text: (this.params.page ?? 1).toString() });
|
|
|
|
// Next, last
|
|
if (meta?.next) {
|
|
const next = pagination.createEl("a", { text: meta.next.page.toString(), href: "#", attr: { role: "button" } });
|
|
next.addEventListener("click", () => {
|
|
void this.setParams({ ...this.params, page: meta.next?.page });
|
|
});
|
|
}
|
|
if (meta?.last && (!meta.next || meta.next.page !== meta.last.page)) {
|
|
const last = pagination.createEl("a", { text: ">>", href: "#", attr: { role: "button" } });
|
|
last.addEventListener("click", () => {
|
|
void this.setParams({ ...this.params, page: meta.last?.page });
|
|
});
|
|
}
|
|
}
|
|
|
|
// TODO: Add this to query params / settings?
|
|
const showRefresh = true;
|
|
if (showRefresh) {
|
|
const refreshButton = footer.createEl("button", {
|
|
cls: "clickable-icon",
|
|
attr: { "aria-label": "Refresh Results" },
|
|
});
|
|
refreshButton.addEventListener("click", () => {
|
|
void this.setParams(this.params, true);
|
|
});
|
|
setIcon(refreshButton, "refresh-cw");
|
|
}
|
|
|
|
const thead = table.createEl("thead");
|
|
let columns = params.columns;
|
|
if (!columns || columns.length === 0) {
|
|
columns = DEFAULT_COLUMNS[params.queryType];
|
|
}
|
|
|
|
// Ensure columns are lowercase
|
|
columns = columns.map((c) => c.toLowerCase());
|
|
|
|
for (const col of columns) {
|
|
const th = thead.createEl("th");
|
|
// Get predefined header if available
|
|
th.setText(ALL_COLUMNS[params.queryType][col]?.header ?? titleCase(col));
|
|
}
|
|
const tbody = table.createEl("tbody");
|
|
for (const row of result) {
|
|
const tr = tbody.createEl("tr");
|
|
for (const col of columns) {
|
|
const cell = tr.createEl("td");
|
|
const renderer = ALL_COLUMNS[params.queryType][col];
|
|
if (renderer) {
|
|
void renderer.cell(row, cell);
|
|
} else {
|
|
const cellVal = getProp(row, col);
|
|
if (cellVal !== null) {
|
|
cell.setText(typeof cellVal === "string" ? cellVal : JSON.stringify(cellVal));
|
|
} else {
|
|
cell.setText("");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private hasSomeRel(meta: PaginationMeta | null): boolean {
|
|
return Boolean(meta && (meta.first || meta.prev || meta.next || meta.last));
|
|
}
|
|
|
|
private getExternalLink(params: QueryParams, result: TableResult): string | null {
|
|
return null;
|
|
}
|
|
}
|