+164
-14
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user