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
+73 -2
View File
@@ -41,7 +41,14 @@ interface GiteaBranchResponse {
interface GiteaIssueResponse {
number?: number;
title?: string;
state?: string;
html_url?: string;
updated_at?: string;
user?: {
login?: string;
};
comments?: number;
}
interface GiteaLabelResponse {
@@ -49,11 +56,31 @@ interface GiteaLabelResponse {
name: string;
}
interface GiteaAttachmentResponse {
name?: string;
browser_download_url?: string;
}
export interface CreatedGiteaIssue {
number: number;
webUrl: string;
}
export interface GiteaIssueSummary {
number: number;
title: string;
state: string;
webUrl: string;
updatedAt: string;
author: string;
comments: number;
}
export interface GiteaIssueAttachment {
name: string;
downloadUrl: string;
}
export interface GiteaRepoSummary {
name: string;
fullName: string;
@@ -124,18 +151,62 @@ export async function createGiteaIssue(owner: string, repo: string, title: strin
};
}
export async function getGiteaIssues(owner: string, repo: string): Promise<GiteaIssueSummary[]> {
const client = createGiteaClient();
const response = await client.get<GiteaIssueResponse[]>(`/repos/${owner}/${repo}/issues`, {
params: {
limit: 25,
page: 1,
state: "all",
type: "issues"
}
});
return response.data
.filter((issue) => issue.number !== undefined)
.map((issue) => ({
number: issue.number ?? 0,
title: issue.title || "Untitled issue",
state: issue.state || "unknown",
webUrl: issue.html_url || `${giteaConfig.baseUrl.replace(/\/+$/, "")}/${owner}/${repo}/issues/${issue.number}`,
updatedAt: issue.updated_at || "",
author: issue.user?.login || "unknown",
comments: issue.comments ?? 0
}));
}
export async function uploadGiteaIssueAttachment(
owner: string,
repo: string,
issueNumber: number,
file: Blob,
fileName: string
): Promise<void> {
): Promise<GiteaIssueAttachment> {
const client = createGiteaClient();
const formData = new FormData();
formData.append("attachment", file, fileName);
await client.post(`/repos/${owner}/${repo}/issues/${issueNumber}/assets`, formData);
const response = await client.post<GiteaAttachmentResponse>(
`/repos/${owner}/${repo}/issues/${issueNumber}/assets`,
formData
);
return {
name: response.data.name || fileName,
downloadUrl: response.data.browser_download_url || ""
};
}
export async function createGiteaIssueComment(
owner: string,
repo: string,
issueNumber: number,
body: string
): Promise<void> {
const client = createGiteaClient();
await client.post(`/repos/${owner}/${repo}/issues/${issueNumber}/comments`, {
body
});
}
async function getGiteaLabels(owner: string, repo: string): Promise<GiteaLabelResponse[]> {
+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) => {
+258 -16
View File
@@ -1,11 +1,15 @@
import { createStore } from "zustand/vanilla";
import {
addGiteaIssueLabels,
createGiteaIssueComment,
createGiteaIssue,
ensureGiteaLabel,
getCurrentGiteaUser,
getGiteaIssues,
getReposOrderedByLastCommit,
giteaConfig,
type GiteaIssueAttachment,
type GiteaIssueSummary,
type GiteaRepoSummary,
type GiteaUser,
uploadGiteaIssueAttachment
@@ -39,6 +43,12 @@ interface IssueDraft {
files: File[];
}
interface IssueCommentDraft {
content: string;
screenshots: ScreenshotAttachment[];
files: File[];
}
interface AppState {
activeView: AppView;
config: typeof giteaConfig;
@@ -51,10 +61,17 @@ interface AppState {
activeBaseUrl: string;
addUrlDraft: AddUrlDraft;
issueDraft: IssueDraft;
issueCommentDraft: IssueCommentDraft;
targetIssues: GiteaIssueSummary[];
issueListRepoFullName: string;
selectedIssueNumber: number | null;
isLoadingData: boolean;
isLoadingTargetIssues: boolean;
isTestingConnection: boolean;
isCapturingScreenshot: boolean;
isCapturingIssueCommentScreenshot: boolean;
isCreatingIssue: boolean;
isCreatingIssueComment: boolean;
statusMessage: string;
statusTone: StatusTone;
clearStatus: () => void;
@@ -63,6 +80,13 @@ interface AppState {
setIssueDraft: (draft: Partial<Omit<IssueDraft, "screenshots" | "files">>) => void;
setIssueFiles: (files: File[]) => void;
deleteIssueScreenshot: (id: string) => void;
setIssueCommentDraft: (draft: Partial<Omit<IssueCommentDraft, "screenshots" | "files">>) => void;
setIssueCommentFiles: (files: File[]) => void;
deleteIssueCommentScreenshot: (id: string) => void;
selectIssue: (issueNumber: number) => void;
loadTargetIssues: (force?: boolean) => Promise<void>;
captureIssueCommentScreenshot: () => Promise<void>;
createIssueComment: () => Promise<void>;
captureScreenshot: () => Promise<void>;
createIssue: () => Promise<void>;
refreshActiveTab: () => Promise<void>;
@@ -190,6 +214,22 @@ function parseRepoFullName(fullName: string): { owner: string; repo: string } |
return { owner, repo };
}
function getTargetRepo(state: AppState): { knownUrl: KnownUrl; owner: string; repo: string; fullName: string } | null {
const knownUrl = findKnownUrlForBase(state);
const parsed = knownUrl?.giteaRepo ? parseRepoFullName(knownUrl.giteaRepo.fullName) : null;
if (!knownUrl || !knownUrl.giteaRepo || !parsed) {
return null;
}
return {
knownUrl,
owner: parsed.owner,
repo: parsed.repo,
fullName: knownUrl.giteaRepo.fullName
};
}
function inferUrlLink(context: LocalContext, baseUrl: string, draft: AddUrlDraft): KnownUrl | null {
if (!draft.mode || !draft.selectedValue) {
return null;
@@ -302,6 +342,15 @@ function captureVisibleTab(): Promise<{ dataUrl: string; blob: Blob }> {
});
}
function createScreenshotAttachment(dataUrl: string, blob: Blob): ScreenshotAttachment {
return {
id: crypto.randomUUID(),
name: `silma-screenshot-${new Date().toISOString().replace(/[:.]/g, "-")}.png`,
dataUrl,
blob
};
}
function buildIssueBody(state: AppState, knownUrl: KnownUrl): string {
const parts = [
state.issueDraft.content.trim(),
@@ -316,6 +365,27 @@ function buildIssueBody(state: AppState, knownUrl: KnownUrl): string {
return parts.filter(Boolean).join("\n\n");
}
function buildAttachmentMarkdown(attachments: GiteaIssueAttachment[]): string {
const lines = attachments
.filter((attachment) => attachment.downloadUrl)
.map((attachment) => `- [${attachment.name}](${attachment.downloadUrl})`);
return lines.length ? `Attachments:\n${lines.join("\n")}` : "";
}
function buildIssueCommentBody(state: AppState, knownUrl: KnownUrl, attachments: GiteaIssueAttachment[]): string {
const parts = [
state.issueCommentDraft.content.trim(),
buildAttachmentMarkdown(attachments),
"---",
`Source URL: ${state.activeTabUrl || knownUrl.url}`,
`Base URL: ${state.activeBaseUrl || getBaseUrl(knownUrl.url)}`,
`Chrome profile: ${CHROME_PROFILE_NAME}`
];
return parts.filter(Boolean).join("\n\n");
}
export const appStore = createStore<AppState>()((set, get) => ({
activeView: "repos",
config: giteaConfig,
@@ -337,10 +407,21 @@ export const appStore = createStore<AppState>()((set, get) => ({
screenshots: [],
files: []
},
issueCommentDraft: {
content: "",
screenshots: [],
files: []
},
targetIssues: [],
issueListRepoFullName: "",
selectedIssueNumber: null,
isLoadingData: false,
isLoadingTargetIssues: false,
isTestingConnection: false,
isCapturingScreenshot: false,
isCapturingIssueCommentScreenshot: false,
isCreatingIssue: false,
isCreatingIssueComment: false,
statusMessage: "",
statusTone: "neutral",
clearStatus: () => {
@@ -379,18 +460,82 @@ export const appStore = createStore<AppState>()((set, get) => ({
screenshots: state.issueDraft.screenshots.filter((screenshot) => screenshot.id !== id)
}
})),
setIssueCommentDraft: (draft) =>
set((state) => ({
issueCommentDraft: {
...state.issueCommentDraft,
...draft
}
})),
setIssueCommentFiles: (files) =>
set((state) => ({
issueCommentDraft: {
...state.issueCommentDraft,
files
}
})),
deleteIssueCommentScreenshot: (id) =>
set((state) => ({
issueCommentDraft: {
...state.issueCommentDraft,
screenshots: state.issueCommentDraft.screenshots.filter((screenshot) => screenshot.id !== id)
}
})),
selectIssue: (issueNumber) =>
set({
selectedIssueNumber: issueNumber,
issueCommentDraft: {
content: "",
screenshots: [],
files: []
}
}),
loadTargetIssues: async (force = false) => {
const state = appStore.getState();
const target = getTargetRepo(state);
if (!target) {
set({
targetIssues: [],
issueListRepoFullName: "",
selectedIssueNumber: null
});
return;
}
if (!force && state.issueListRepoFullName === target.fullName && state.targetIssues.length > 0) {
return;
}
set({
isLoadingTargetIssues: true,
issueListRepoFullName: target.fullName
});
try {
const targetIssues = await getGiteaIssues(target.owner, target.repo);
set((current) => ({
targetIssues,
issueListRepoFullName: target.fullName,
selectedIssueNumber:
current.selectedIssueNumber && targetIssues.some((issue) => issue.number === current.selectedIssueNumber)
? current.selectedIssueNumber
: null
}));
} catch (error) {
const message = error instanceof Error ? error.message : "Could not load Gitea issues.";
setStatus(set, message, "error");
} finally {
set({ isLoadingTargetIssues: false });
}
},
captureScreenshot: async () => {
set({ isCapturingScreenshot: true });
setStatus(set, "Capturing screenshot...", "neutral", { autoClear: false });
try {
const { dataUrl, blob } = await captureVisibleTab();
const screenshot: ScreenshotAttachment = {
id: crypto.randomUUID(),
name: `silma-screenshot-${new Date().toISOString().replace(/[:.]/g, "-")}.png`,
dataUrl,
blob
};
const screenshot = createScreenshotAttachment(dataUrl, blob);
set((state) => ({
issueDraft: {
@@ -406,13 +551,96 @@ export const appStore = createStore<AppState>()((set, get) => ({
set({ isCapturingScreenshot: false });
}
},
captureIssueCommentScreenshot: async () => {
set({ isCapturingIssueCommentScreenshot: true });
setStatus(set, "Capturing screenshot...", "neutral", { autoClear: false });
try {
const { dataUrl, blob } = await captureVisibleTab();
const screenshot = createScreenshotAttachment(dataUrl, blob);
set((state) => ({
issueCommentDraft: {
...state.issueCommentDraft,
screenshots: [...state.issueCommentDraft.screenshots, screenshot]
}
}));
setStatus(set, `Captured ${screenshot.name}.`, "success");
} catch (error) {
const message = error instanceof Error ? error.message : "Could not capture a screenshot.";
setStatus(set, message, "error");
} finally {
set({ isCapturingIssueCommentScreenshot: false });
}
},
createIssueComment: async () => {
const state = appStore.getState();
const target = getTargetRepo(state);
const selectedIssueNumber = state.selectedIssueNumber;
const hasAttachments =
state.issueCommentDraft.screenshots.length > 0 || state.issueCommentDraft.files.length > 0;
if (!target || !selectedIssueNumber) {
setStatus(set, "Select an issue before commenting.", "error");
return;
}
if (!state.issueCommentDraft.content.trim() && !hasAttachments) {
setStatus(set, "Add a comment or attachment before submitting.", "error");
return;
}
set({ isCreatingIssueComment: true });
setStatus(set, `Adding comment to issue #${selectedIssueNumber}...`, "neutral", { autoClear: false });
try {
const uploads = [
...state.issueCommentDraft.screenshots.map((screenshot) => ({
name: screenshot.name,
blob: screenshot.blob
})),
...state.issueCommentDraft.files.map((file) => ({
name: file.name,
blob: file
}))
];
const attachments: GiteaIssueAttachment[] = [];
for (const upload of uploads) {
attachments.push(
await uploadGiteaIssueAttachment(target.owner, target.repo, selectedIssueNumber, upload.blob, upload.name)
);
}
await createGiteaIssueComment(
target.owner,
target.repo,
selectedIssueNumber,
buildIssueCommentBody(state, target.knownUrl, attachments)
);
set({
issueCommentDraft: {
content: "",
screenshots: [],
files: []
}
});
await appStore.getState().loadTargetIssues(true);
setStatus(set, `Commented on issue #${selectedIssueNumber}.`, "success");
} catch (error) {
const message = error instanceof Error ? error.message : "Could not comment on the Gitea issue.";
setStatus(set, message, "error");
} finally {
set({ isCreatingIssueComment: false });
}
},
createIssue: async () => {
const state = appStore.getState();
const knownUrl = findKnownUrlForBase(state);
const repo = knownUrl?.giteaRepo ? parseRepoFullName(knownUrl.giteaRepo.fullName) : null;
const target = getTargetRepo(state);
const subject = state.issueDraft.subject.trim();
if (!knownUrl || !repo) {
if (!target) {
setStatus(set, "Open a known URL with a linked Gitea repo before creating an issue.", "error");
return;
}
@@ -426,7 +654,7 @@ export const appStore = createStore<AppState>()((set, get) => ({
setStatus(set, "Creating Gitea issue...", "neutral", { autoClear: false });
try {
const issue = await createGiteaIssue(repo.owner, repo.repo, subject, buildIssueBody(state, knownUrl));
const issue = await createGiteaIssue(target.owner, target.repo, subject, buildIssueBody(state, target.knownUrl));
const uploads = [
...state.issueDraft.screenshots.map((screenshot) => ({
name: screenshot.name,
@@ -439,12 +667,12 @@ export const appStore = createStore<AppState>()((set, get) => ({
];
for (const upload of uploads) {
await uploadGiteaIssueAttachment(repo.owner, repo.repo, issue.number, upload.blob, upload.name);
await uploadGiteaIssueAttachment(target.owner, target.repo, issue.number, upload.blob, upload.name);
}
if (state.issueDraft.ready) {
const readyLabel = await ensureGiteaLabel(repo.owner, repo.repo, "ready");
await addGiteaIssueLabels(repo.owner, repo.repo, issue.number, [readyLabel.id]);
const readyLabel = await ensureGiteaLabel(target.owner, target.repo, "ready");
await addGiteaIssueLabels(target.owner, target.repo, issue.number, [readyLabel.id]);
}
set({
@@ -454,9 +682,11 @@ export const appStore = createStore<AppState>()((set, get) => ({
ready: false,
screenshots: [],
files: []
}
},
selectedIssueNumber: issue.number
});
setStatus(set, `Created issue #${issue.number} in ${knownUrl.giteaRepo?.fullName}.`, "success");
await appStore.getState().loadTargetIssues(true);
setStatus(set, `Created issue #${issue.number} in ${target.fullName}.`, "success");
} catch (error) {
const message = error instanceof Error ? error.message : "Could not create the Gitea issue.";
setStatus(set, message, "error");
@@ -475,7 +705,19 @@ export const appStore = createStore<AppState>()((set, get) => ({
addUrlDraft:
activeBaseUrl !== currentBaseUrl
? { mode: null, selectedValue: "" }
: get().addUrlDraft
: get().addUrlDraft,
targetIssues:
activeBaseUrl !== currentBaseUrl
? []
: get().targetIssues,
issueListRepoFullName:
activeBaseUrl !== currentBaseUrl
? ""
: get().issueListRepoFullName,
selectedIssueNumber:
activeBaseUrl !== currentBaseUrl
? null
: get().selectedIssueNumber
});
},
loadData: async () => {
+74
View File
@@ -126,6 +126,10 @@ h1 {
background: #f8fafc;
}
.view-header[hidden] {
display: none;
}
.view-header h2 {
color: #102a43;
font-size: 15px;
@@ -449,6 +453,76 @@ input[readonly] {
gap: 10px;
}
.issue-section {
display: grid;
gap: 7px;
}
.section-heading {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 0 2px;
}
.section-heading > span {
color: #52616f;
font-size: 10px;
font-weight: 850;
text-transform: uppercase;
}
.issue-list {
display: grid;
gap: 0;
border-top: 1px solid #d9e2ec;
}
.issue-row {
display: grid;
grid-template-columns: 40px minmax(0, 1fr) 48px 66px;
align-items: center;
gap: 5px;
width: 100%;
border: 0;
border-bottom: 1px solid #d9e2ec;
padding: 4px 6px;
background: #ffffff;
color: #243b53;
cursor: pointer;
text-align: left;
}
.issue-row[data-selected="true"] {
background: #e6f6ff;
}
.issue-number,
.issue-state,
.issue-meta {
overflow: hidden;
color: #52616f;
font-size: 10px;
font-weight: 800;
text-overflow: ellipsis;
white-space: nowrap;
}
.issue-title {
min-width: 0;
overflow: hidden;
color: #102a43;
font-size: 11px;
font-weight: 800;
text-overflow: ellipsis;
white-space: nowrap;
}
.issue-comment-card {
margin-top: 2px;
}
.target-summary {
display: grid;
gap: 2px;