Add repo issue comments panel
Build / build (push) Successful in 11s

This commit is contained in:
2026-07-17 16:12:51 -05:00
parent 32b81264ef
commit 89790fa9d7
9 changed files with 578 additions and 35 deletions
+164 -14
View File
@@ -8,7 +8,7 @@ import {
type AppView,
type ScreenshotAttachment
} from "./store";
import type { GiteaRepoSummary } from "./api";
import type { GiteaIssueSummary, GiteaRepoSummary } from "./api";
import type { KnownUrl, LocalContext, LocalWorkspace } from "./localContext";
const status = document.querySelector<HTMLElement>("#status");
@@ -43,9 +43,9 @@ function escapeHtml(value: string): string {
});
}
function formatDate(value: string): string {
function formatDate(value: string, emptyText = "No date"): string {
if (!value) {
return "No commit date";
return emptyText;
}
return new Intl.DateTimeFormat(undefined, {
@@ -122,7 +122,7 @@ function renderRepos(repos: GiteaRepoSummary[]): string {
<a class="item-title" href="${repo.webUrl}" target="_blank" rel="noreferrer">${escapeHtml(repo.fullName)}</a>
<span class="commit-message">${escapeHtml(repo.lastCommitMessage)}</span>
</div>
<span class="repo-meta">${formatDate(repo.lastCommitAt)}</span>
<span class="repo-meta">${formatDate(repo.lastCommitAt, "No commit date")}</span>
<span class="repo-meta">${formatSha(repo.lastCommitSha)}</span>
<span class="repo-link-cell">${getRepoWorkspaceLink(repo)}</span>
</article>
@@ -294,11 +294,13 @@ function renderAttachmentList(items: string[], emptyText: string): string {
`;
}
function renderScreenshotList(screenshots: ScreenshotAttachment[]): string {
function renderScreenshotList(screenshots: ScreenshotAttachment[], deleteAction: "issue" | "comment"): string {
if (!screenshots.length) {
return `<span class="muted">No screenshots captured.</span>`;
}
const attribute = deleteAction === "issue" ? "data-delete-screenshot" : "data-delete-comment-screenshot";
return `
<div class="screenshot-list">
${screenshots
@@ -307,7 +309,7 @@ function renderScreenshotList(screenshots: ScreenshotAttachment[]): string {
<figure class="screenshot-item">
<img src="${screenshot.dataUrl}" alt="${escapeHtml(screenshot.name)}" />
<figcaption>${escapeHtml(screenshot.name)}</figcaption>
<button class="danger-button compact-button" type="button" data-delete-screenshot="${screenshot.id}">Delete</button>
<button class="danger-button compact-button" type="button" ${attribute}="${screenshot.id}">Delete</button>
</figure>
`
)
@@ -316,6 +318,93 @@ function renderScreenshotList(screenshots: ScreenshotAttachment[]): string {
`;
}
function renderIssueRows(issues: GiteaIssueSummary[], selectedIssueNumber: number | null): string {
if (!issues.length) {
return `<div class="empty-state">No issues found for this repo.</div>`;
}
return `
<div class="issue-list">
${issues
.map(
(issue) => `
<button class="issue-row" type="button" data-select-issue="${issue.number}" data-selected="${issue.number === selectedIssueNumber ? "true" : "false"}">
<span class="issue-number">#${issue.number}</span>
<span class="issue-title">${escapeHtml(issue.title)}</span>
<span class="issue-state">${escapeHtml(issue.state)}</span>
<span class="issue-meta">${formatDate(issue.updatedAt)}</span>
</button>
`
)
.join("")}
</div>
`;
}
function renderIssueCommentForm(issue: GiteaIssueSummary): string {
const state = appStore.getState();
const draft = state.issueCommentDraft;
const canSubmit =
Boolean(draft.content.trim() || draft.screenshots.length || draft.files.length) && !state.isCreatingIssueComment;
return `
<article class="item-card issue-comment-card">
<div class="item-heading">
<a class="item-title" href="${issue.webUrl}" target="_blank" rel="noreferrer">#${issue.number} ${escapeHtml(issue.title)}</a>
<span class="issue-state">${escapeHtml(issue.state)}</span>
</div>
<div class="form-stack">
<label>
<span>Comment</span>
<textarea id="issue-comment-content" rows="6" placeholder="Add a comment">${escapeHtml(draft.content)}</textarea>
</label>
<section class="attachment-section">
<div class="attachment-heading">
<span>Screenshots</span>
<button id="capture-comment-screenshot" class="secondary-button" type="button" ${state.isCapturingIssueCommentScreenshot ? "disabled" : ""}>
${state.isCapturingIssueCommentScreenshot ? "Taking..." : "Take snapshot"}
</button>
</div>
${renderScreenshotList(draft.screenshots, "comment")}
</section>
<label>
<span>File attachments</span>
<input id="issue-comment-files" type="file" multiple />
</label>
${renderAttachmentList(
draft.files.map((file) => file.name),
"No files selected."
)}
<button id="submit-issue-comment" class="primary-button" type="button" ${canSubmit ? "" : "disabled"}>
${state.isCreatingIssueComment ? "Commenting..." : "Comment"}
</button>
</div>
</article>
`;
}
function renderTargetIssues(): string {
const state = appStore.getState();
const selectedIssue = state.targetIssues.find((issue) => issue.number === state.selectedIssueNumber) ?? null;
return `
<section class="issue-section">
<div class="section-heading">
<span>Issues</span>
<button id="refresh-target-issues" class="secondary-button compact-button" type="button" ${state.isLoadingTargetIssues ? "disabled" : ""}>
${state.isLoadingTargetIssues ? "Loading..." : "Refresh"}
</button>
</div>
${state.isLoadingTargetIssues && !state.targetIssues.length ? `<div class="empty-state">Loading issues...</div>` : renderIssueRows(state.targetIssues, state.selectedIssueNumber)}
${selectedIssue ? renderIssueCommentForm(selectedIssue) : ""}
</section>
`;
}
function renderAddIssue(): string {
const state = appStore.getState();
const knownUrl = getActiveKnownUrl();
@@ -357,7 +446,7 @@ function renderAddIssue(): string {
${state.isCapturingScreenshot ? "Taking..." : "Take snapshot"}
</button>
</div>
${renderScreenshotList(draft.screenshots)}
${renderScreenshotList(draft.screenshots, "issue")}
</section>
<label>
@@ -371,7 +460,7 @@ function renderAddIssue(): string {
<label class="checkbox-row">
<input id="issue-ready" type="checkbox" ${draft.ready ? "checked" : ""} />
<span>Apply ready label after creation and file uploads</span>
<span>Ready</span>
</label>
<button id="create-issue" class="primary-button" type="button" ${draft.subject.trim() && !state.isCreatingIssue ? "" : "disabled"}>
@@ -379,6 +468,7 @@ function renderAddIssue(): string {
</button>
</div>
</article>
${renderTargetIssues()}
`;
}
@@ -464,6 +554,46 @@ function bindViewActions(): void {
});
});
viewContent?.querySelectorAll<HTMLButtonElement>("[data-select-issue]").forEach((button) => {
button.addEventListener("click", () => {
const issueNumber = Number(button.dataset.selectIssue);
if (Number.isFinite(issueNumber)) {
appStore.getState().selectIssue(issueNumber);
}
});
});
viewContent?.querySelector<HTMLButtonElement>("#refresh-target-issues")?.addEventListener("click", () => {
void appStore.getState().loadTargetIssues(true);
});
viewContent?.querySelector<HTMLTextAreaElement>("#issue-comment-content")?.addEventListener("input", (event) => {
appStore.getState().setIssueCommentDraft({
content: (event.target as HTMLTextAreaElement).value
});
});
viewContent?.querySelector<HTMLInputElement>("#issue-comment-files")?.addEventListener("change", (event) => {
appStore.getState().setIssueCommentFiles(Array.from((event.target as HTMLInputElement).files ?? []));
});
viewContent?.querySelector<HTMLButtonElement>("#capture-comment-screenshot")?.addEventListener("click", () => {
void appStore.getState().captureIssueCommentScreenshot();
});
viewContent?.querySelectorAll<HTMLButtonElement>("[data-delete-comment-screenshot]").forEach((button) => {
button.addEventListener("click", () => {
const id = button.dataset.deleteCommentScreenshot;
if (id) {
appStore.getState().deleteIssueCommentScreenshot(id);
}
});
});
viewContent?.querySelector<HTMLButtonElement>("#submit-issue-comment")?.addEventListener("click", () => {
void appStore.getState().createIssueComment();
});
viewContent?.querySelector<HTMLButtonElement>("#create-issue")?.addEventListener("click", () => {
void appStore.getState().createIssue();
});
@@ -492,12 +622,16 @@ function renderView(): void {
workspaces: `Authoritative workspaces from ${context?.workspaceRoot || "workspace_Father"}.`
};
viewHeader.innerHTML = `
<div>
<h2>${titles[state.activeView]}</h2>
<p>${escapeHtml(descriptions[state.activeView])}</p>
</div>
`;
viewHeader.hidden = state.activeView === "add-issue";
viewHeader.innerHTML =
state.activeView === "add-issue"
? ""
: `
<div>
<h2>${titles[state.activeView]}</h2>
<p>${escapeHtml(descriptions[state.activeView])}</p>
</div>
`;
if (state.activeView === "repos") {
viewContent.innerHTML = renderRepos(state.repos);
@@ -543,6 +677,22 @@ function renderChrome(): void {
function render(): void {
renderChrome();
renderView();
ensureTargetIssuesLoaded();
}
function ensureTargetIssuesLoaded(): void {
const state = appStore.getState();
const knownUrl = getActiveKnownUrl();
const fullName = knownUrl?.giteaRepo?.fullName ?? "";
if (
state.activeView === "add-issue" &&
fullName &&
state.issueListRepoFullName !== fullName &&
!state.isLoadingTargetIssues
) {
void appStore.getState().loadTargetIssues();
}
}
viewButtons.forEach((button) => {