forked from retoor/devplacepy
feat: add file attachment support to issue tracker with Gitea mirroring
Implement full attachment CRUD for issues and comments, restricted to open issues only. Attachments are stored locally and mirrored to the Gitea tracker via new `mirror_attachment_to_gitea` and `set_gitea_asset_id` functions. Add `gitea_asset_id` column to the attachments table, extend `IssueForm` and `IssueCommentForm` with `attachment_uids` field, and expose new API endpoints for listing, adding, and deleting issue attachments. Update agent configuration to include web research tools, and document the new capability in README, AGENTS.md, and CLAUDE.md.
This commit is contained in:
@@ -217,3 +217,29 @@
|
||||
gap: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.attachment-gallery-item .issue-att-delete {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
color: #fff;
|
||||
font-size: 1rem;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
z-index: 1;
|
||||
}
|
||||
.attachment-gallery-item .issue-att-delete:hover {
|
||||
background: var(--danger, #c0392b);
|
||||
}
|
||||
.issue-attach-form {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-top: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ import { DeviiTerminal } from "./DeviiTerminal.js";
|
||||
import { ZipDownloader } from "./ZipDownloader.js";
|
||||
import { ProjectForker } from "./ProjectForker.js";
|
||||
import { IssueReporter } from "./IssueReporter.js";
|
||||
import { IssueAttachments } from "./IssueAttachments.js";
|
||||
import { PlanningGenerator } from "./PlanningGenerator.js";
|
||||
import { MediaGallery } from "./MediaGallery.js";
|
||||
import WindowManager from "./components/WindowManager.js";
|
||||
@@ -79,6 +80,7 @@ class Application {
|
||||
this.zipDownloader = new ZipDownloader();
|
||||
this.projectForker = new ProjectForker();
|
||||
this.issueReporter = new IssueReporter();
|
||||
this.issueAttachments = new IssueAttachments();
|
||||
this.planningGenerator = new PlanningGenerator();
|
||||
this.mediaGallery = new MediaGallery();
|
||||
this.liveNotifications = new LiveNotifications(this.pubsub, this.toast);
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
// retoor <retoor@molodetz.nl>
|
||||
|
||||
import { Http } from "./Http.js";
|
||||
|
||||
export class IssueAttachments {
|
||||
constructor() {
|
||||
this.active = false;
|
||||
document.addEventListener("submit", (event) => {
|
||||
const form = event.target.closest("form[data-issue-attach]");
|
||||
if (!form) return;
|
||||
event.preventDefault();
|
||||
this.add(form);
|
||||
});
|
||||
document.addEventListener("click", (event) => {
|
||||
const button = event.target.closest("[data-attachment-delete]");
|
||||
if (!button) return;
|
||||
event.preventDefault();
|
||||
this.remove(button);
|
||||
});
|
||||
}
|
||||
|
||||
async add(form) {
|
||||
if (this.active) return;
|
||||
const uidsInput = form.querySelector("[name='attachment_uids']");
|
||||
const uids = uidsInput ? uidsInput.value.trim() : "";
|
||||
if (!uids) {
|
||||
window.app.toast.show("Select a file to attach first", { type: "info" });
|
||||
return;
|
||||
}
|
||||
this.active = true;
|
||||
const button = form.querySelector("button[type='submit']");
|
||||
if (button) button.disabled = true;
|
||||
try {
|
||||
await Http.send(form.dataset.action, { attachment_uids: uids });
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
this.active = false;
|
||||
if (button) button.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async remove(button) {
|
||||
const confirmed = await window.app.dialog.confirm({
|
||||
title: "Delete attachment",
|
||||
message: "Delete this attachment? It is also removed from the tracker.",
|
||||
});
|
||||
if (!confirmed) return;
|
||||
try {
|
||||
const response = await fetch(button.dataset.action, {
|
||||
method: "DELETE",
|
||||
headers: { "Accept": "application/json" },
|
||||
});
|
||||
const data = await response.json().catch(() => ({}));
|
||||
if (!response.ok || data.ok === false) {
|
||||
const message = (data.error && data.error.message) || "Could not delete the attachment";
|
||||
throw new Error(message);
|
||||
}
|
||||
const item = button.closest(".attachment-gallery-item");
|
||||
if (item) item.remove();
|
||||
} catch (error) {
|
||||
Http.notifyError(error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,8 @@ export class IssueReporter {
|
||||
window.app.toast.show("Title and description are required", { type: "error" });
|
||||
return;
|
||||
}
|
||||
const uidsInput = form.querySelector("[name='attachment_uids']");
|
||||
const attachmentUids = uidsInput ? uidsInput.value : "";
|
||||
this.active = true;
|
||||
const button = form.querySelector("button[type='submit']");
|
||||
if (button) {
|
||||
@@ -33,7 +35,11 @@ export class IssueReporter {
|
||||
ms: 5000,
|
||||
});
|
||||
try {
|
||||
const job = await Http.sendForm(form.action, { title, description });
|
||||
const job = await Http.sendForm(form.action, {
|
||||
title,
|
||||
description,
|
||||
attachment_uids: attachmentUids,
|
||||
});
|
||||
await this.poll(job.status_url);
|
||||
} catch (error) {
|
||||
window.app.toast.show("Could not submit the report", { type: "error" });
|
||||
|
||||
Reference in New Issue
Block a user