mirror of
https://github.com/nathonius/obsidian-github-link.git
synced 2026-07-22 09:20:25 +00:00
✨ feat: #11 Add predefined PR table columns
Columns will automatically link, set text, add icons, etc. Closes: #11
This commit is contained in:
parent
c4ab669a54
commit
3913ec116b
13 changed files with 250 additions and 48 deletions
|
|
@ -107,7 +107,7 @@
|
|||
"infer_ticket": true,
|
||||
"confirm_ticket": true,
|
||||
"add_to_title": true,
|
||||
"append_hashtag": false,
|
||||
"append_hashtag": true,
|
||||
"surround": "",
|
||||
"title_position": "start"
|
||||
},
|
||||
|
|
|
|||
18
package-lock.json
generated
18
package-lock.json
generated
|
|
@ -2334,15 +2334,6 @@
|
|||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/moment": {
|
||||
"version": "2.29.4",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
|
||||
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
|
|
@ -2369,6 +2360,15 @@
|
|||
"@codemirror/view": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/obsidian/node_modules/moment": {
|
||||
"version": "2.29.4",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
|
||||
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/octokit": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/octokit/-/octokit-3.1.2.tgz",
|
||||
|
|
|
|||
|
|
@ -13,3 +13,23 @@ export type SearchRepoParams = RestEndpointMethodTypes["search"]["repos"]["param
|
|||
export type SearchRepoResponse = RestEndpointMethodTypes["search"]["repos"]["response"]["data"];
|
||||
export type SearchIssueParams = RestEndpointMethodTypes["search"]["issuesAndPullRequests"]["parameters"];
|
||||
export type SearchIssueResponse = RestEndpointMethodTypes["search"]["issuesAndPullRequests"]["response"]["data"];
|
||||
|
||||
export function getSearchResultPRStatus(pr: SearchIssueResponse["items"][number]): IssueStatus {
|
||||
if (pr.pull_request?.merged_at) {
|
||||
return IssueStatus.Done;
|
||||
} else if (pr.closed_at) {
|
||||
return IssueStatus.Closed;
|
||||
} else {
|
||||
return IssueStatus.Open;
|
||||
}
|
||||
}
|
||||
|
||||
export function getPRStatus(pr: PullResponse): IssueStatus {
|
||||
if (pr.merged) {
|
||||
return IssueStatus.Done;
|
||||
} else if (pr.closed_at) {
|
||||
return IssueStatus.Closed;
|
||||
} else {
|
||||
return IssueStatus.Open;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,12 @@ export interface ParsedUrl {
|
|||
commit?: string;
|
||||
}
|
||||
|
||||
const apiRegex = /(https:\/\/)?api\.github\.com\/repos\//;
|
||||
|
||||
export function repoAPIToBrowserUrl(urlString: string): string {
|
||||
return urlString.replace(apiRegex, "https://github.com/");
|
||||
}
|
||||
|
||||
export function parseUrl(urlString: string): ParsedUrl {
|
||||
// Some potential URLs:
|
||||
// https://github.com/nathonius/alloy-theme
|
||||
|
|
|
|||
11
src/icon.ts
Normal file
11
src/icon.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { setIcon } from "obsidian";
|
||||
import { IssueStatus } from "./github/response";
|
||||
|
||||
export function setPRIcon(icon: HTMLElement, status: IssueStatus) {
|
||||
if (status !== IssueStatus.Closed) {
|
||||
setIcon(icon, "git-pull-request-arrow");
|
||||
} else {
|
||||
setIcon(icon, "git-pull-request-closed");
|
||||
}
|
||||
icon.dataset.status = status;
|
||||
}
|
||||
|
|
@ -1,20 +1,24 @@
|
|||
import { getPRStatus } from "src/github/response";
|
||||
import { getIssue, getPullRequest } from "../github/github";
|
||||
|
||||
import { parseUrl } from "../github/url-parse";
|
||||
import { setIcon } from "obsidian";
|
||||
import { setPRIcon } from "src/icon";
|
||||
|
||||
export async function createTag(href: string) {
|
||||
const parsedUrl = parseUrl(href);
|
||||
const container = createEl("a", { cls: "gh-link-inline-tag", href });
|
||||
const container = createEl("a", { cls: "github-link-inline", href });
|
||||
|
||||
// Add icon
|
||||
const icon = createTagSection(container).createSpan({ cls: "gh-link-inline-tag-icon" });
|
||||
const icon = createTagSection(container).createSpan({
|
||||
cls: ["github-link-status-icon", "github-link-inline-icon"],
|
||||
});
|
||||
setIcon(icon, "github");
|
||||
|
||||
// Add repo
|
||||
if (parsedUrl.repo) {
|
||||
createTagSection(container).createSpan({
|
||||
cls: "gh-link-inline-tag-repo",
|
||||
cls: "github-link-inline-repo",
|
||||
text: parsedUrl.repo,
|
||||
});
|
||||
}
|
||||
|
|
@ -22,7 +26,7 @@ export async function createTag(href: string) {
|
|||
// fall back to org if no repo found
|
||||
else if (parsedUrl.org) {
|
||||
createTagSection(container).createSpan({
|
||||
cls: "gh-link-inline-tag-org",
|
||||
cls: "github-link-inline-org",
|
||||
text: parsedUrl.org,
|
||||
});
|
||||
}
|
||||
|
|
@ -39,7 +43,7 @@ export async function createTag(href: string) {
|
|||
icon.dataset.status = issue.state;
|
||||
}
|
||||
createTagSection(container).createSpan({
|
||||
cls: "gh-link-inline-tag-issue-title",
|
||||
cls: "github-link-inline-issue-title",
|
||||
text: issue.title,
|
||||
});
|
||||
}
|
||||
|
|
@ -49,14 +53,10 @@ export async function createTag(href: string) {
|
|||
if (parsedUrl.pr !== undefined) {
|
||||
const pull = await getPullRequest(parsedUrl.org, parsedUrl.repo, parsedUrl.pr);
|
||||
if (pull.title) {
|
||||
setIcon(icon, "git-pull-request-arrow");
|
||||
if (pull.merged) {
|
||||
icon.dataset.status = "done";
|
||||
} else {
|
||||
icon.dataset.status = pull.state;
|
||||
}
|
||||
const status = getPRStatus(pull);
|
||||
setPRIcon(icon, status);
|
||||
createTagSection(container).createSpan({
|
||||
cls: "gh-link-inline-tag-pr-title",
|
||||
cls: "github-link-inline-pr-title",
|
||||
text: pull.title,
|
||||
});
|
||||
}
|
||||
|
|
@ -67,7 +67,7 @@ export async function createTag(href: string) {
|
|||
}
|
||||
|
||||
function createTagSection(parent: HTMLElement): HTMLElement {
|
||||
return parent.createDiv({ cls: "gh-link-inline-tag-section" });
|
||||
return parent.createDiv({ cls: "github-link-inline-section" });
|
||||
}
|
||||
|
||||
export async function InlineRenderer(el: HTMLElement) {
|
||||
|
|
|
|||
48
src/query/column/base.ts
Normal file
48
src/query/column/base.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import type { SearchIssueResponse } from "src/github/response";
|
||||
import { DateFormat } from "src/util";
|
||||
|
||||
export interface ColumnGetter<T> {
|
||||
header: string;
|
||||
cell: (row: T, el: HTMLTableCellElement) => void | Promise<void>;
|
||||
}
|
||||
export type ColumnsMap<T> = Record<string, ColumnGetter<T>>;
|
||||
|
||||
export function DateCell(value: string | undefined | null, el: HTMLTableCellElement) {
|
||||
el.classList.add("github-link-table-date");
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const asDate = new Date(value);
|
||||
if (isNaN(asDate.valueOf())) {
|
||||
el.setText(value);
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Allow formatting this date via options.
|
||||
el.setText(DateFormat.DATE_SHORT.format(asDate));
|
||||
}
|
||||
|
||||
/**
|
||||
* Issue and PR columns share types, so some columns are shared
|
||||
*/
|
||||
export const CommonIssuePRColumns: ColumnsMap<SearchIssueResponse["items"][number]> = {
|
||||
created: {
|
||||
header: "Created",
|
||||
cell: (row, el) => {
|
||||
DateCell(row.created_at, el);
|
||||
},
|
||||
},
|
||||
updated: {
|
||||
header: "Updated",
|
||||
cell: (row, el) => {
|
||||
DateCell(row.updated_at, el);
|
||||
},
|
||||
},
|
||||
closed: {
|
||||
header: "Closed",
|
||||
cell: (row, el) => {
|
||||
DateCell(row.closed_at, el);
|
||||
},
|
||||
},
|
||||
};
|
||||
4
src/query/column/issue.ts
Normal file
4
src/query/column/issue.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
import type { SearchIssueResponse } from "src/github/response";
|
||||
import { CommonIssuePRColumns, type ColumnsMap } from "./base";
|
||||
|
||||
export const IssueColumns: ColumnsMap<SearchIssueResponse["items"][number]> = { ...CommonIssuePRColumns };
|
||||
45
src/query/column/pull-request.ts
Normal file
45
src/query/column/pull-request.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import { getSearchResultPRStatus, IssueStatus, type SearchIssueResponse } from "src/github/response";
|
||||
import { parseUrl, repoAPIToBrowserUrl } from "src/github/url-parse";
|
||||
import { CommonIssuePRColumns, type ColumnsMap } from "./base";
|
||||
import { setPRIcon } from "src/icon";
|
||||
import { titleCase } from "src/util";
|
||||
|
||||
export const PullRequestColumns: ColumnsMap<SearchIssueResponse["items"][number]> = {
|
||||
...CommonIssuePRColumns,
|
||||
number: {
|
||||
header: "PR",
|
||||
cell: (row, el) => {
|
||||
el.classList.add("github-link-table-pr");
|
||||
el.createEl("a", { text: `#${row.number}`, href: row.html_url });
|
||||
},
|
||||
},
|
||||
repo: {
|
||||
header: "Repo",
|
||||
cell: (row, el) => {
|
||||
el.classList.add("github-link-table-repo");
|
||||
const url = repoAPIToBrowserUrl(row.repository_url);
|
||||
const parsed = parseUrl(url);
|
||||
el.createEl("a", { text: parsed.repo, href: url });
|
||||
},
|
||||
},
|
||||
author: {
|
||||
header: "Author",
|
||||
cell: (row, el) => {
|
||||
const anchor = el.createEl("a", { cls: "github-link-table-author" });
|
||||
if (row.user?.avatar_url) {
|
||||
anchor.createEl("img", { cls: "github-link-table-avatar", attr: { src: row.user.avatar_url } });
|
||||
}
|
||||
anchor.createSpan({ text: row.user?.login });
|
||||
},
|
||||
},
|
||||
status: {
|
||||
header: "Status",
|
||||
cell: (row, el) => {
|
||||
const wrapper = el.createDiv({ cls: "github-link-table-status" });
|
||||
const status = getSearchResultPRStatus(row);
|
||||
const icon = wrapper.createSpan({ cls: "github-link-status-icon" });
|
||||
setPRIcon(icon, status);
|
||||
wrapper.createSpan({ text: status === IssueStatus.Done ? "Merged" : titleCase(status) });
|
||||
},
|
||||
},
|
||||
};
|
||||
4
src/query/column/repo.ts
Normal file
4
src/query/column/repo.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
import type { SearchRepoResponse } from "src/github/response";
|
||||
import type { ColumnsMap } from "./base";
|
||||
|
||||
export const RepoColumns: ColumnsMap<SearchRepoResponse["items"][number]> = {};
|
||||
|
|
@ -1,28 +1,45 @@
|
|||
import type { SearchIssueResponse, SearchRepoResponse } from "src/github/response";
|
||||
import { getProp, titleCase } from "src/util";
|
||||
|
||||
import type { TableParams } from "./params";
|
||||
import { QueryType, type TableParams } from "./params";
|
||||
import { PullRequestColumns } from "./column/pull-request";
|
||||
import { IssueColumns } from "./column/issue";
|
||||
import { RepoColumns } from "./column/repo";
|
||||
|
||||
export function renderTable<T extends SearchIssueResponse | SearchRepoResponse>(
|
||||
const columns = {
|
||||
[QueryType.PullRequest]: PullRequestColumns,
|
||||
[QueryType.Issue]: IssueColumns,
|
||||
[QueryType.Repo]: RepoColumns,
|
||||
};
|
||||
|
||||
export async function renderTable<T extends SearchIssueResponse | SearchRepoResponse>(
|
||||
params: TableParams,
|
||||
result: T,
|
||||
el: HTMLElement,
|
||||
) {
|
||||
const table = el.createEl("table", { cls: "github-link-query-table" });
|
||||
const table = el.createEl("table", { cls: "github-link-table" });
|
||||
const thead = table.createEl("thead");
|
||||
for (const col of params.columns) {
|
||||
thead.createEl("th", { text: titleCase(col) });
|
||||
const th = thead.createEl("th");
|
||||
// Get predefined header if available
|
||||
th.setText(columns[params.queryType][col]?.header ?? titleCase(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");
|
||||
const cellVal = getProp(row, col);
|
||||
if (cellVal !== null) {
|
||||
cell.setText(typeof cellVal === "string" ? cellVal : JSON.stringify(cellVal));
|
||||
const renderer = columns[params.queryType][col];
|
||||
if (renderer) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
renderer.cell(row as any, cell);
|
||||
} else {
|
||||
cell.setText("");
|
||||
const cellVal = getProp(row, col);
|
||||
if (cellVal !== null) {
|
||||
cell.setText(typeof cellVal === "string" ? cellVal : JSON.stringify(cellVal));
|
||||
} else {
|
||||
cell.setText("");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
15
src/util.ts
15
src/util.ts
|
|
@ -49,3 +49,18 @@ export function safeJSONParse<T>(value: string, props: Record<keyof T, boolean>)
|
|||
|
||||
return result as T;
|
||||
}
|
||||
|
||||
// Reference for more formats: https://github.com/moment/luxon/blob/master/src/impl/formats.js
|
||||
const n = "numeric";
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||
const s = "short";
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||
const l = "long";
|
||||
|
||||
export const DateFormat = {
|
||||
DATE_SHORT: new Intl.DateTimeFormat(undefined, {
|
||||
year: n,
|
||||
month: n,
|
||||
day: n,
|
||||
}),
|
||||
};
|
||||
|
|
|
|||
66
styles.css
66
styles.css
|
|
@ -146,7 +146,7 @@ body.theme-dark {
|
|||
font-size: 3em;
|
||||
}
|
||||
|
||||
.gh-link-inline-tag {
|
||||
.github-link-inline {
|
||||
display: inline-flex;
|
||||
background-color: var(--gh-color-canvas-default);
|
||||
border-radius: 4px;
|
||||
|
|
@ -158,12 +158,12 @@ body.theme-dark {
|
|||
font-size: 0.75em;
|
||||
}
|
||||
|
||||
.gh-link-inline-tag:hover {
|
||||
.github-link-inline:hover {
|
||||
color: var(--gh-color-fg-default);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.gh-link-inline-tag-section {
|
||||
.github-link-inline-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
|
@ -171,55 +171,87 @@ body.theme-dark {
|
|||
overflow: hidden;
|
||||
}
|
||||
|
||||
.gh-link-inline-tag-icon {
|
||||
display: inline-flex;
|
||||
.github-link-inline-icon {
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
.gh-link-inline-tag-icon > svg {
|
||||
.github-link-inline-icon > svg {
|
||||
width: var(--icon-s);
|
||||
height: var(--icon-s);
|
||||
stroke-width: var(--icon-s-stroke-width);
|
||||
}
|
||||
|
||||
.gh-link-inline-tag-section:has(> .gh-link-inline-tag-icon) {
|
||||
.github-link-inline-section:has(> .github-link-inline-icon) {
|
||||
background-color: var(--gh-color-canvas-inset);
|
||||
}
|
||||
|
||||
.gh-link-inline-tag-section:has(> .gh-link-inline-tag-icon[data-status="open"]) {
|
||||
background-color: var(--gh-color-open-subtle);
|
||||
.github-link-status-icon {
|
||||
display: inline-flex;
|
||||
}
|
||||
.github-link-status-icon[data-status="open"] {
|
||||
color: var(--gh-color-open-fg);
|
||||
}
|
||||
.github-link-status-icon[data-status="closed"] {
|
||||
color: var(--gh-color-closed-fg);
|
||||
}
|
||||
.github-link-status-icon[data-status="done"] {
|
||||
color: var(--gh-color-done-fg);
|
||||
}
|
||||
|
||||
.github-link-inline-section:has(> .github-link-inline-icon[data-status="open"]) {
|
||||
background-color: var(--gh-color-open-subtle);
|
||||
border-color: var(--gh-color-open-emphasis);
|
||||
}
|
||||
|
||||
.gh-link-inline-tag-section:has(> .gh-link-inline-tag-icon[data-status="closed"]) {
|
||||
.github-link-inline-section:has(> .github-link-inline-icon[data-status="closed"]) {
|
||||
background-color: var(--gh-color-closed-subtle);
|
||||
color: var(--gh-color-closed-fg);
|
||||
border-color: var(--gh-color-closed-emphasis);
|
||||
}
|
||||
|
||||
.gh-link-inline-tag-section:has(> .gh-link-inline-tag-icon[data-status="done"]) {
|
||||
.github-link-inline-section:has(> .github-link-inline-icon[data-status="done"]) {
|
||||
background-color: var(--gh-color-done-subtle);
|
||||
color: var(--gh-color-done-fg);
|
||||
border-color: var(--gh-color-done-emphasis);
|
||||
}
|
||||
|
||||
.gh-link-inline-tag-section:has(> .gh-link-inline-tag-repo) {
|
||||
.github-link-inline-section:has(> .github-link-inline-repo) {
|
||||
background-color: var(--gh-color-accent-subtle);
|
||||
color: var(--gh-color-accent-fg);
|
||||
border-color: var(--gh-color-accent-emphasis);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.gh-link-inline-tag-repo {
|
||||
.github-link-inline-repo {
|
||||
text-wrap: nowrap;
|
||||
line-height: var(--line-height-normal);
|
||||
}
|
||||
|
||||
.gh-link-inline-tag-pr-title,
|
||||
.gh-link-inline-tag-issue-title {
|
||||
.github-link-inline-pr-title,
|
||||
.github-link-inline-issue-title {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-wrap: nowrap;
|
||||
line-height: var(--line-height-normal);
|
||||
}
|
||||
|
||||
.github-link-table th {
|
||||
font-weight: var(--font-bold);
|
||||
}
|
||||
|
||||
.github-link-table a {
|
||||
text-decoration: none;
|
||||
}
|
||||
.github-link-table a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.github-link-table-avatar {
|
||||
width: max(24px, var(--icon-l));
|
||||
height: max(24px, var(--icon-l));
|
||||
}
|
||||
|
||||
.github-link-table-status,
|
||||
.github-link-table-author {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue