From 3913ec116b6a5e5ac151d29fd02f80d6c2b6d65b Mon Sep 17 00:00:00 2001 From: Nathan Smith Date: Thu, 1 Feb 2024 17:42:50 +0000 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat:=20#11=20Add=20predefined=20PR?= =?UTF-8?q?=20table=20columns?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Columns will automatically link, set text, add icons, etc. Closes: #11 --- .better-commits.json | 2 +- package-lock.json | 18 ++++----- src/github/response.ts | 20 ++++++++++ src/github/url-parse.ts | 6 +++ src/icon.ts | 11 ++++++ src/inline/inline.ts | 26 ++++++------- src/query/column/base.ts | 48 +++++++++++++++++++++++ src/query/column/issue.ts | 4 ++ src/query/column/pull-request.ts | 45 ++++++++++++++++++++++ src/query/column/repo.ts | 4 ++ src/query/output.ts | 33 ++++++++++++---- src/util.ts | 15 ++++++++ styles.css | 66 ++++++++++++++++++++++++-------- 13 files changed, 250 insertions(+), 48 deletions(-) create mode 100644 src/icon.ts create mode 100644 src/query/column/base.ts create mode 100644 src/query/column/issue.ts create mode 100644 src/query/column/pull-request.ts create mode 100644 src/query/column/repo.ts diff --git a/.better-commits.json b/.better-commits.json index 4d7df72..1403e39 100644 --- a/.better-commits.json +++ b/.better-commits.json @@ -107,7 +107,7 @@ "infer_ticket": true, "confirm_ticket": true, "add_to_title": true, - "append_hashtag": false, + "append_hashtag": true, "surround": "", "title_position": "start" }, diff --git a/package-lock.json b/package-lock.json index 0a665ac..b7f88d9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/src/github/response.ts b/src/github/response.ts index 91e5897..b6fbc13 100644 --- a/src/github/response.ts +++ b/src/github/response.ts @@ -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; + } +} diff --git a/src/github/url-parse.ts b/src/github/url-parse.ts index 99ced31..bd20148 100644 --- a/src/github/url-parse.ts +++ b/src/github/url-parse.ts @@ -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 diff --git a/src/icon.ts b/src/icon.ts new file mode 100644 index 0000000..75342b6 --- /dev/null +++ b/src/icon.ts @@ -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; +} diff --git a/src/inline/inline.ts b/src/inline/inline.ts index 38605b7..f8fbf92 100644 --- a/src/inline/inline.ts +++ b/src/inline/inline.ts @@ -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) { diff --git a/src/query/column/base.ts b/src/query/column/base.ts new file mode 100644 index 0000000..7a967a3 --- /dev/null +++ b/src/query/column/base.ts @@ -0,0 +1,48 @@ +import type { SearchIssueResponse } from "src/github/response"; +import { DateFormat } from "src/util"; + +export interface ColumnGetter { + header: string; + cell: (row: T, el: HTMLTableCellElement) => void | Promise; +} +export type ColumnsMap = Record>; + +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 = { + 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); + }, + }, +}; diff --git a/src/query/column/issue.ts b/src/query/column/issue.ts new file mode 100644 index 0000000..1b6fdc6 --- /dev/null +++ b/src/query/column/issue.ts @@ -0,0 +1,4 @@ +import type { SearchIssueResponse } from "src/github/response"; +import { CommonIssuePRColumns, type ColumnsMap } from "./base"; + +export const IssueColumns: ColumnsMap = { ...CommonIssuePRColumns }; diff --git a/src/query/column/pull-request.ts b/src/query/column/pull-request.ts new file mode 100644 index 0000000..9f5e857 --- /dev/null +++ b/src/query/column/pull-request.ts @@ -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 = { + ...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) }); + }, + }, +}; diff --git a/src/query/column/repo.ts b/src/query/column/repo.ts new file mode 100644 index 0000000..8cae7ff --- /dev/null +++ b/src/query/column/repo.ts @@ -0,0 +1,4 @@ +import type { SearchRepoResponse } from "src/github/response"; +import type { ColumnsMap } from "./base"; + +export const RepoColumns: ColumnsMap = {}; diff --git a/src/query/output.ts b/src/query/output.ts index 7dbf6ea..bddd99b 100644 --- a/src/query/output.ts +++ b/src/query/output.ts @@ -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( +const columns = { + [QueryType.PullRequest]: PullRequestColumns, + [QueryType.Issue]: IssueColumns, + [QueryType.Repo]: RepoColumns, +}; + +export async function renderTable( 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(""); + } } } } diff --git a/src/util.ts b/src/util.ts index 4369325..8305c4b 100644 --- a/src/util.ts +++ b/src/util.ts @@ -49,3 +49,18 @@ export function safeJSONParse(value: string, props: Record) 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, + }), +}; diff --git a/styles.css b/styles.css index 755602c..aac1a6f 100644 --- a/styles.css +++ b/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; +}