Merge pull request 'Tickets auf dem Board in der Farbe ihres Projekts zeichnen' (#30) from fds2610-bot/monitor:main into main
Reviewed-on: https://codeberg.org/fds2610/monitor/pulls/30
This commit is contained in:
commit
80d121f703
4 changed files with 178 additions and 28 deletions
|
|
@ -196,6 +196,13 @@ header.office-header h1 {
|
|||
stroke-width: 2;
|
||||
}
|
||||
|
||||
/* Zettel in der Farbe seines Projekts (Issue #27): die Fuellung kommt als Inline-Style aus
|
||||
der Projektkonfiguration, die Kontur muss dann neutral sein - die gelbe Standardkontur
|
||||
passt nur zum gelben Standardzettel. */
|
||||
.note-colored {
|
||||
stroke: rgba(15, 23, 42, 0.35);
|
||||
}
|
||||
|
||||
.note-stack-badge {
|
||||
fill: var(--color-note-stack-badge);
|
||||
stroke: #92400e;
|
||||
|
|
|
|||
|
|
@ -150,10 +150,25 @@ function renderCoffeeCorner() {
|
|||
return g;
|
||||
}
|
||||
|
||||
function renderStackBadge(x, y, count) {
|
||||
// Faerbt einen Zettel in der Farbe seines Quell-Projekts (Issue #26/#27: "welcher Farbe
|
||||
// diese zugeordnet werden"). Ohne Farbe - Ticket ohne Projekt oder Demo-Modus - bleibt es
|
||||
// bei der Standardfarbe aus dem Stylesheet. Die Farbe ist Nutzereingabe aus dem
|
||||
// Projekte-Modal und landet deshalb ueber die style-Property im DOM, nie in einem
|
||||
// zusammengesetzten Attribut-String.
|
||||
function paintNote(rect, color) {
|
||||
if (color) {
|
||||
rect.style.fill = color;
|
||||
// Die gelbe Standardkontur passt nur zum gelben Standardzettel - siehe .note-colored.
|
||||
rect.classList.add("note-colored");
|
||||
}
|
||||
return rect;
|
||||
}
|
||||
|
||||
function renderStackBadge(x, y, count, stackColors = []) {
|
||||
const g = el("g", { class: "note-stack" });
|
||||
[6, 3, 0].forEach((offset, i) => {
|
||||
g.appendChild(
|
||||
paintNote(
|
||||
el("rect", {
|
||||
x: x - offset,
|
||||
y: y - offset,
|
||||
|
|
@ -161,7 +176,9 @@ function renderStackBadge(x, y, count) {
|
|||
height: NOTE_H,
|
||||
rx: 6,
|
||||
class: `note note-stack-layer-${i}`,
|
||||
})
|
||||
}),
|
||||
stackColors[i]
|
||||
)
|
||||
);
|
||||
});
|
||||
const cx = x + NOTE_W / 2;
|
||||
|
|
@ -175,10 +192,11 @@ function renderStackBadge(x, y, count) {
|
|||
|
||||
/**
|
||||
* @param {Record<string, number>} ticketCounts
|
||||
* @param {Record<string, Array<object>>} groupedTickets - liefert die Projektfarbe je Zettel.
|
||||
* @param {(columnKey: string) => void} onColumnClick
|
||||
*/
|
||||
function renderBoard(ticketCounts, onColumnClick) {
|
||||
const columns = buildBoardColumnLayout(ticketCounts);
|
||||
function renderBoard(ticketCounts, groupedTickets, onColumnClick) {
|
||||
const columns = buildBoardColumnLayout(ticketCounts, groupedTickets);
|
||||
const g = el("g", { class: "board" });
|
||||
g.appendChild(el("rect", { ...BOARD_FRAME, rx: 10, class: "board-frame" }));
|
||||
g.appendChild(
|
||||
|
|
@ -227,13 +245,18 @@ function renderBoard(ticketCounts, onColumnClick) {
|
|||
|
||||
for (let i = 0; i < column.visibleNotes; i++) {
|
||||
colGroup.appendChild(
|
||||
el("rect", { x: noteX, y: noteY, width: NOTE_W, height: NOTE_H, rx: 6, class: "note" })
|
||||
paintNote(
|
||||
el("rect", { x: noteX, y: noteY, width: NOTE_W, height: NOTE_H, rx: 6, class: "note" }),
|
||||
column.noteColors[i]
|
||||
)
|
||||
);
|
||||
noteY += NOTE_STEP_Y;
|
||||
}
|
||||
|
||||
if (column.stackCount > 0) {
|
||||
colGroup.appendChild(renderStackBadge(noteX, noteY, column.stackCount));
|
||||
colGroup.appendChild(
|
||||
renderStackBadge(noteX, noteY, column.stackCount, column.stackColors)
|
||||
);
|
||||
}
|
||||
|
||||
g.appendChild(colGroup);
|
||||
|
|
@ -1380,7 +1403,7 @@ export function renderOfficeScene(container, data = {}) {
|
|||
|
||||
const groupedTickets = groupTicketsByColumn(tickets);
|
||||
|
||||
const boardGroup = renderBoard(ticketCounts, (columnKey) => {
|
||||
const boardGroup = renderBoard(ticketCounts, groupedTickets, (columnKey) => {
|
||||
openModal(
|
||||
buildTicketModalContent(columnKey, groupedTickets[columnKey] || []),
|
||||
renderTicketModalBody
|
||||
|
|
|
|||
|
|
@ -26,6 +26,10 @@ export const MAX_SYSTEM_TILES = 9;
|
|||
export const MAX_EVENT_LINES = 5;
|
||||
export const DEFAULT_DESK_COUNT = 8;
|
||||
|
||||
// Ein Stapel wird aus so vielen versetzten Zetteln gezeichnet (siehe renderStackBadge()) -
|
||||
// entsprechend viele Projektfarben kann er zeigen.
|
||||
export const NOTE_STACK_LAYERS = 3;
|
||||
|
||||
/**
|
||||
* Baut aus rohen Ticket-Zaehlern pro Spalte die Darstellung fuer das Board.
|
||||
* Regel (Detailantworten F3): mehr als 5 Tickets in einer Spalte -> die aeltesten 4 werden
|
||||
|
|
@ -33,29 +37,70 @@ export const DEFAULT_DESK_COUNT = 8;
|
|||
* Regel (Detailantworten F2): die Spalte "done" wird immer als ein Stapel (Zeit-Historie)
|
||||
* dargestellt, unabhaengig von der Anzahl.
|
||||
*
|
||||
* Die Farben stammen aus dem Quell-Projekt des Tickets (ticket.color, gepflegt im
|
||||
* Projekte-Modal am PO, Issue #27) und sind optional: ohne groupedTickets - etwa im
|
||||
* Demo-Modus, der nur Zaehler kennt - bleiben die Zettel einfarbig wie zuvor.
|
||||
*
|
||||
* @param {Record<string, number>} ticketCounts - Anzahl Tickets je Spalten-Key.
|
||||
* @returns {Array<{key: string, label: string, count: number, visibleNotes: number, stackCount: number}>}
|
||||
* @param {Record<string, Array<object>>} groupedTickets - Tickets je Spalte in derselben
|
||||
* Reihenfolge wie gezaehlt (siehe groupTicketsByColumn()).
|
||||
* @returns {Array<{key: string, label: string, count: number, visibleNotes: number,
|
||||
* stackCount: number, noteColors: Array<string|null>, stackColors: Array<string>}>}
|
||||
*/
|
||||
export function buildBoardColumnLayout(ticketCounts = {}) {
|
||||
export function buildBoardColumnLayout(ticketCounts = {}, groupedTickets = {}) {
|
||||
return BOARD_COLUMNS.map(({ key, label }) => {
|
||||
const count = Math.max(0, ticketCounts[key] ?? 0);
|
||||
const tickets = groupedTickets[key] ?? [];
|
||||
|
||||
if (key === "done") {
|
||||
return { key, label, count, visibleNotes: 0, stackCount: count };
|
||||
}
|
||||
|
||||
if (count <= MAX_VISIBLE_NOTES_PER_COLUMN + 1) {
|
||||
return { key, label, count, visibleNotes: count, stackCount: 0 };
|
||||
}
|
||||
|
||||
return {
|
||||
const withColors = (visibleNotes, stackCount) => ({
|
||||
key,
|
||||
label,
|
||||
count,
|
||||
visibleNotes: MAX_VISIBLE_NOTES_PER_COLUMN,
|
||||
stackCount: count - MAX_VISIBLE_NOTES_PER_COLUMN,
|
||||
};
|
||||
visibleNotes,
|
||||
stackCount,
|
||||
noteColors: noteColors(tickets.slice(0, visibleNotes), visibleNotes),
|
||||
stackColors: stackCount > 0 ? stackLayerColors(tickets.slice(visibleNotes)) : [],
|
||||
});
|
||||
|
||||
if (key === "done") {
|
||||
return withColors(0, count);
|
||||
}
|
||||
|
||||
if (count <= MAX_VISIBLE_NOTES_PER_COLUMN + 1) {
|
||||
return withColors(count, 0);
|
||||
}
|
||||
|
||||
return withColors(MAX_VISIBLE_NOTES_PER_COLUMN, count - MAX_VISIBLE_NOTES_PER_COLUMN);
|
||||
});
|
||||
}
|
||||
|
||||
// null steht fuer "keine Projektfarbe" (Ticket ohne Quell-Projekt, z.B. per API gemeldet) -
|
||||
// solche Zettel behalten die Standardfarbe aus dem Stylesheet.
|
||||
function noteColors(tickets, length) {
|
||||
return Array.from({ length }, (_, i) => tickets[i]?.color ?? null);
|
||||
}
|
||||
|
||||
// Ein Stapel steht fuer viele Tickets, aber nur fuer NOTE_STACK_LAYERS Zettel: er zeigt
|
||||
// deshalb die ersten verschiedenen Projektfarben darin, statt eine davon dreimal zu
|
||||
// wiederholen. So ist auch an "Done" (immer ein Stapel) zu sehen, aus welchen Projekten die
|
||||
// Tickets stammen.
|
||||
//
|
||||
// Reichen die gefundenen Farben nicht fuer alle Zettel, werden sie zyklisch wiederholt: ein
|
||||
// ungefaerbter Rest-Zettel wuerde sonst wie ein weiteres Projekt aussehen, denn die
|
||||
// Standardfarbe (--color-note) ist von einem gelben Projekt kaum zu unterscheiden. Nur wenn
|
||||
// gar keine Farbe vorkommt, bleibt der Stapel komplett im Standard.
|
||||
function stackLayerColors(tickets) {
|
||||
const found = [];
|
||||
for (const ticket of tickets) {
|
||||
if (ticket.color && !found.includes(ticket.color)) {
|
||||
found.push(ticket.color);
|
||||
if (found.length === NOTE_STACK_LAYERS) break;
|
||||
}
|
||||
}
|
||||
if (found.length === 0) {
|
||||
return [];
|
||||
}
|
||||
return Array.from({ length: NOTE_STACK_LAYERS }, (_, i) => found[i % found.length]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import {
|
|||
MAX_SYSTEM_TILES,
|
||||
MAX_EVENT_LINES,
|
||||
DEFAULT_DESK_COUNT,
|
||||
NOTE_STACK_LAYERS,
|
||||
aggregateTicketsByColumn,
|
||||
groupTicketsByColumn,
|
||||
buildTicketModalContent,
|
||||
|
|
@ -82,6 +83,80 @@ test("buildBoardColumnLayout behandelt fehlende/negative Zaehler als 0", () => {
|
|||
assert.equal(qa.visibleNotes, 0);
|
||||
});
|
||||
|
||||
test("buildBoardColumnLayout faerbt jeden Zettel in der Farbe seines Projekts (Issue #27)", () => {
|
||||
const tickets = [
|
||||
{ column_name: "bereit", color: "#fbca04" },
|
||||
{ column_name: "bereit", color: "#28a745" },
|
||||
{ column_name: "bereit", color: "#fbca04" },
|
||||
];
|
||||
const bereit = buildBoardColumnLayout(
|
||||
aggregateTicketsByColumn(tickets),
|
||||
groupTicketsByColumn(tickets)
|
||||
).find((c) => c.key === "bereit");
|
||||
|
||||
assert.deepEqual(bereit.noteColors, ["#fbca04", "#28a745", "#fbca04"]);
|
||||
});
|
||||
|
||||
test("buildBoardColumnLayout laesst Zettel ohne Projektfarbe ungefaerbt", () => {
|
||||
const tickets = [{ column_name: "qa" }, { column_name: "qa", color: "#0366d6" }];
|
||||
const qa = buildBoardColumnLayout(
|
||||
aggregateTicketsByColumn(tickets),
|
||||
groupTicketsByColumn(tickets)
|
||||
).find((c) => c.key === "qa");
|
||||
|
||||
assert.deepEqual(qa.noteColors, [null, "#0366d6"]);
|
||||
});
|
||||
|
||||
test("buildBoardColumnLayout ohne Ticketliste (Demo-Modus) laesst alle Zettel ungefaerbt", () => {
|
||||
const bereit = buildBoardColumnLayout({ bereit: 2 }).find((c) => c.key === "bereit");
|
||||
assert.deepEqual(bereit.noteColors, [null, null]);
|
||||
assert.deepEqual(bereit.stackColors, []);
|
||||
});
|
||||
|
||||
test("buildBoardColumnLayout zeigt im Stapel die verschiedenen Projektfarben der gestapelten Tickets", () => {
|
||||
// 6 Tickets: die aeltesten 4 einzeln, die restlichen 2 im Stapel - dessen Farben stammen
|
||||
// ausschliesslich aus diesen beiden.
|
||||
const tickets = [
|
||||
...Array.from({ length: 4 }, () => ({ column_name: "backlog", color: "#fbca04" })),
|
||||
{ column_name: "backlog", color: "#28a745" },
|
||||
{ column_name: "backlog", color: "#0366d6" },
|
||||
];
|
||||
const backlog = buildBoardColumnLayout(
|
||||
aggregateTicketsByColumn(tickets),
|
||||
groupTicketsByColumn(tickets)
|
||||
).find((c) => c.key === "backlog");
|
||||
|
||||
assert.equal(backlog.stackCount, 2);
|
||||
// Dritter Zettel: zyklische Wiederholung statt Standardfarbe - siehe stackLayerColors().
|
||||
assert.deepEqual(backlog.stackColors, ["#28a745", "#0366d6", "#28a745"]);
|
||||
});
|
||||
|
||||
test("buildBoardColumnLayout laesst einen Stapel ganz ohne Projektfarben im Standard", () => {
|
||||
const tickets = Array.from({ length: 7 }, () => ({ column_name: "backlog" }));
|
||||
const backlog = buildBoardColumnLayout(
|
||||
aggregateTicketsByColumn(tickets),
|
||||
groupTicketsByColumn(tickets)
|
||||
).find((c) => c.key === "backlog");
|
||||
|
||||
assert.equal(backlog.stackCount, 3);
|
||||
assert.deepEqual(backlog.stackColors, []);
|
||||
});
|
||||
|
||||
test("buildBoardColumnLayout zeigt im Stapel jede Projektfarbe nur einmal und hoechstens NOTE_STACK_LAYERS", () => {
|
||||
const tickets = ["#fbca04", "#fbca04", "#28a745", "#0366d6", "#ec4899"].map((color) => ({
|
||||
column_name: "done",
|
||||
color,
|
||||
}));
|
||||
const done = buildBoardColumnLayout(
|
||||
aggregateTicketsByColumn(tickets),
|
||||
groupTicketsByColumn(tickets)
|
||||
).find((c) => c.key === "done");
|
||||
|
||||
assert.equal(done.stackCount, 5);
|
||||
assert.equal(done.stackColors.length, NOTE_STACK_LAYERS);
|
||||
assert.deepEqual(done.stackColors, ["#fbca04", "#28a745", "#0366d6"]);
|
||||
});
|
||||
|
||||
test("buildDeskLayout erzeugt genau DEFAULT_DESK_COUNT Schreibtische", () => {
|
||||
const desks = buildDeskLayout([]);
|
||||
assert.equal(desks.length, DEFAULT_DESK_COUNT);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue