diff --git a/html/css/office.css b/html/css/office.css
index a98c18e..1774cb5 100644
--- a/html/css/office.css
+++ b/html/css/office.css
@@ -741,7 +741,17 @@ header.office-header h1 {
font-size: 0.9rem;
}
+.office-modal-item .office-modal-description {
+ white-space: pre-line;
+}
+
+.office-modal-repo {
+ color: #64748b;
+ font-size: 0.85rem;
+}
+
.office-modal-item-heading {
+ flex-wrap: wrap;
display: flex;
align-items: center;
gap: 8px;
diff --git a/html/js/office-render.js b/html/js/office-render.js
index b00cc44..1c04cd3 100644
--- a/html/js/office-render.js
+++ b/html/js/office-render.js
@@ -1419,6 +1419,23 @@ function renderTicketModalBody(body, content) {
const heading = document.createElement("div");
heading.className = "office-modal-item-heading";
+ if (item.repo) {
+ const repoEl = document.createElement("span");
+ repoEl.className = "office-modal-repo";
+ repoEl.textContent = item.repo;
+ heading.appendChild(repoEl);
+ }
+ if (item.number) {
+ const numEl = item.link ? document.createElement("a") : document.createElement("span");
+ numEl.className = "office-modal-ticket-number";
+ numEl.textContent = `#${item.number}`;
+ if (item.link) {
+ numEl.href = item.link;
+ numEl.target = "_blank";
+ numEl.rel = "noopener noreferrer";
+ }
+ heading.appendChild(numEl);
+ }
const titleEl = document.createElement("strong");
titleEl.textContent = item.title;
heading.appendChild(titleEl);
@@ -1433,17 +1450,10 @@ function renderTicketModalBody(body, content) {
if (item.description) {
const desc = document.createElement("p");
+ desc.className = "office-modal-description";
desc.textContent = item.description;
li.appendChild(desc);
}
- if (item.link) {
- const a = document.createElement("a");
- a.href = item.link;
- a.target = "_blank";
- a.rel = "noopener noreferrer";
- a.textContent = "Auf Codeberg öffnen";
- li.appendChild(a);
- }
list.appendChild(li);
}
body.appendChild(list);
diff --git a/html/js/office-scene.js b/html/js/office-scene.js
index 6a3d0a4..877b752 100644
--- a/html/js/office-scene.js
+++ b/html/js/office-scene.js
@@ -190,6 +190,22 @@ export function groupTicketsByColumn(tickets = []) {
return groups;
}
+// Issue #88: Beschreibungen im Ticket-Modal werden auf diese Zeilenzahl gekuerzt.
+export const TICKET_DESCRIPTION_MAX_LINES = 10;
+
+/**
+ * Kuerzt einen Text auf maxLines Zeilen; ist er laenger, ersetzt "..." den Rest.
+ *
+ * @param {string} text
+ * @param {number} [maxLines]
+ * @returns {string}
+ */
+export function truncateDescriptionLines(text, maxLines = TICKET_DESCRIPTION_MAX_LINES) {
+ const lines = String(text || "").replace(/\s+$/, "").split(/\r?\n/);
+ if (lines.length <= maxLines) return lines.join("\n");
+ return `${lines.slice(0, maxLines).join("\n")}\n...`;
+}
+
/**
* Baut das Inhaltsmodell fuer das Board-Spalten-Modal (Visualization.md 4.2/4.5): echter
* Titel/Beschreibung je Ticket, plus Link auf die Codeberg-Quelle sofern das Ticket per
@@ -210,9 +226,15 @@ export function buildTicketModalContent(columnKey, tickets = [], opts = {}) {
items: tickets.map((ticket) => ({
id: ticket.id,
title: ticket.title,
- description: ticket.description || "",
+ description: truncateDescriptionLines(ticket.description),
type: ticket.type || null,
color: ticket.color || null,
+ // Issue #88: Repo und Ticket-Nummer stehen in der Ueberschrift, die Nummer ist der Link.
+ repo:
+ ticket.source === "codeberg" && ticket.external_ref
+ ? ticket.repo || `${codebergOwner}/${codebergRepo}`
+ : null,
+ number: ticket.source === "codeberg" && ticket.external_ref ? String(ticket.external_ref) : null,
link:
ticket.source === "codeberg" && ticket.external_ref
? `https://codeberg.org/${ticket.repo || `${codebergOwner}/${codebergRepo}`}/issues/${ticket.external_ref}`
diff --git a/tests/frontend/office-scene.test.js b/tests/frontend/office-scene.test.js
index 3670383..d81fa2e 100644
--- a/tests/frontend/office-scene.test.js
+++ b/tests/frontend/office-scene.test.js
@@ -904,3 +904,24 @@ test("systemsWithoutHostConfig kommt ohne Hosts und ohne system_ids zurecht", ()
assert.deepEqual(systemsWithoutHostConfig([{ id: "a" }], [{ hostname: "a" }]), [{ id: "a" }]);
assert.deepEqual(systemsWithoutHostConfig(), []);
});
+
+test("buildTicketModalContent liefert Repo und Ticket-Nummer fuer die Ueberschrift (Issue #88)", () => {
+ const content = buildTicketModalContent("backlog", [
+ { id: 1, title: "A", source: "codeberg", external_ref: "3", repo: "fds2610/Steuerkram" },
+ { id: 2, title: "B", source: "api", external_ref: null },
+ ]);
+ assert.equal(content.items[0].repo, "fds2610/Steuerkram");
+ assert.equal(content.items[0].number, "3");
+ assert.equal(content.items[1].repo, null);
+ assert.equal(content.items[1].number, null);
+});
+
+test("buildTicketModalContent kuerzt Beschreibungen auf 10 Zeilen mit '...' (Issue #88)", () => {
+ const lines = Array.from({ length: 12 }, (_, i) => `Zeile ${i + 1}`);
+ const [long, exact] = buildTicketModalContent("backlog", [
+ { id: 1, title: "A", description: lines.join("\n") },
+ { id: 2, title: "B", description: lines.slice(0, 10).join("\n") },
+ ]).items;
+ assert.equal(long.description, `${lines.slice(0, 10).join("\n")}\n...`);
+ assert.equal(exact.description, lines.slice(0, 10).join("\n"));
+});