feat(tasks): Karte zeigt, wenn ein Git-Issue daranhaengt
CI / pruefen (pull_request) Canceled after 0s

Die Verknuepfung stand bisher nur im Bearbeiten-Dialog und in der
projektuebergreifenden Uebersicht. Auf dem Brett eines Projekts - dort,
wo man die Aufgaben im Vorbeigehen sieht - war ihr nichts anzusehen.
Wer wissen wollte, welche Aufgabe schon ein Issue hat, musste jede
einzeln aufmachen.

Das Kennzeichen steht vorn in der Fusszeile der Karte, damit sich eine
Spalte danach ueberfliegen laesst, und traegt dasselbe Symbol wie der
Dialog: ein Sachverhalt, ein Zeichen.

Bewusst kein Link. Die Karte steckt in einem <button>, ein <a> darin
waere ungueltiges Markup; der Weg zum Issue fuehrt ueber den Dialog, der
ihn ohnehin anbietet. Die Nummer allein ergibt vorgelesen keinen Sinn,
deshalb steht "Verknuepft mit" als sr-only davor.

Der Fallstrick lag in der Bedingung darueber: die Fusszeile erschien nur
bei Beschreibung, Frist, Wiederholung oder Labels. Gerade eine aus einem
Issue erzeugte Aufgabe hat davon oft nichts - sie waere leer geblieben.

Abzudunkeln, sobald die Aufgabe erledigt ist, war versucht und wieder
verworfen: axe-core rechnete text-violet-300/50 bei 10px als
Kontrastverstoss ab.

Nachgesehen statt angenommen: Brett mit sechs Aufgaben (verknuepft,
unverknuepft, nackt, vollbepackt, erledigt, vierstellige Nummer) auf
1280 und 375 px - kein seitlicher Ueberlauf, axe-core ohne Verstoss.

Refs #12
This commit is contained in:
2026-08-18 14:04:45 +02:00
parent 77567c06cd
commit 6ee529576d
2 changed files with 108 additions and 2 deletions
+82
View File
@@ -0,0 +1,82 @@
import { describe, expect, it } from "vitest";
import { renderToStaticMarkup } from "react-dom/server";
import type { Task } from "@prisma/client";
import { TaskCard } from "./TaskCard";
/**
* Die Karte ist die einzige Stelle im Projekt, an der eine Aufgabe im
* Vorbeigehen zu sehen ist – der Bearbeiten-Dialog verlangt einen Klick.
* Steht die Verknuepfung nur dort, weiss niemand vorher, welche Aufgabe
* ein Issue hat.
*
* Serverseitig gerendert wie bei Markdown.test.tsx: es geht um das
* erzeugte Markup, nicht um Verhalten im Browser.
*/
function aufgabe(felder: Partial<Task> = {}): Task {
return {
id: "t1",
projectId: "p1",
title: "Etwas tun",
description: null,
status: "TODO",
position: 0,
dueDate: null,
labels: [],
recurrence: null,
sourceIssue: null,
sourceUrl: null,
sourceRepositoryId: null,
createdAt: new Date("2026-08-18T10:00:00Z"),
updatedAt: new Date("2026-08-18T10:00:00Z"),
...felder,
} as Task;
}
const markup = (task: Task) => renderToStaticMarkup(<TaskCard task={task} />);
describe("Kennzeichen fuer ein verknuepftes Issue", () => {
it("zeigt die Nummer, sobald eine Aufgabe an einem Issue haengt", () => {
const html = markup(aufgabe({ sourceIssue: 42 }));
expect(html).toContain("#42");
expect(html).toContain("Verkn");
});
it("erscheint auch ohne Beschreibung, Datum und Labels", () => {
// Der eigentliche Fallstrick: die Fusszeile der Karte wird nur
// gerendert, wenn es etwas zu zeigen gibt. Faellt die Verknuepfung aus
// dieser Bedingung heraus, bleibt genau die nackte Aufgabe unbeschriftet
// – und das ist der haeufige Fall bei einer aus einem Issue erzeugten.
const html = markup(
aufgabe({ sourceIssue: 7, description: null, dueDate: null, labels: [], recurrence: null }),
);
expect(html).toContain("#7");
});
it("bleibt weg, wenn keine Verknuepfung besteht", () => {
const html = markup(aufgabe({ description: "Text" }));
expect(html).not.toContain("Verkn");
});
it("bleibt auf einer erledigten Aufgabe sichtbar", () => {
// Die erledigte Karte streicht ihren Titel durch; das Kennzeichen bleibt
// in voller Helligkeit. Abzudunkeln war versucht und wieder verworfen –
// axe-core rechnete es als Kontrastverstoss ab (10px Text, 3,2:1).
// Dass hier ein Issue mitgeschlossen wurde, ist ohnehin gerade dann die
// interessante Auskunft.
const html = markup(aufgabe({ sourceIssue: 5, status: "DONE" }));
expect(html).toContain("#5");
});
it("baut keinen Link in die Karte", () => {
// Die Karte steckt in einem <button>; ein <a> darin waere ungueltiges
// Markup und im Browser nicht verlaesslich anklickbar.
const html = markup(aufgabe({ sourceIssue: 42, sourceUrl: "https://example.com/issues/42" }));
expect(html).not.toContain("<a ");
});
it("nennt die Verknuepfung auch fuer Vorlesesoftware", () => {
// "#42" allein ergibt vorgelesen keinen Sinn.
const html = markup(aufgabe({ sourceIssue: 42 }));
expect(html).toMatch(/sr-only[^>]*>Verkn/);
});
});
+26 -2
View File
@@ -1,6 +1,12 @@
"use client";
import { AlignLeft, CalendarClock, GripVertical, Repeat } from "lucide-react";
import {
AlignLeft,
CalendarClock,
GitPullRequestArrow,
GripVertical,
Repeat,
} from "lucide-react";
import type { Task } from "@prisma/client";
import { RECURRENCE_META } from "@/lib/tasks";
import { cn, formatDate } from "@/lib/utils";
@@ -70,8 +76,26 @@ export function TaskCard({
{task.title}
</p>
{(task.description || task.dueDate || task.recurrence || task.labels.length > 0) && (
{(task.description ||
task.dueDate ||
task.recurrence ||
task.sourceIssue ||
task.labels.length > 0) && (
<div className="mt-1.5 flex flex-wrap items-center gap-x-2 gap-y-1 text-[10px]">
{/* Steht vorn, damit sich eine Spalte danach ueberfliegen laesst.
Bewusst kein Link: die Karte steckt in einem <button>, und ein
<a> darin waere ungueltig. Der Weg zum Issue fuehrt ueber den
Bearbeiten-Dialog, der ihn ohnehin anbietet. */}
{task.sourceIssue ? (
<span
className="inline-flex items-center gap-1 text-violet-300"
title={`Verknüpft mit Issue #${task.sourceIssue}`}
>
<GitPullRequestArrow className="h-2.5 w-2.5" aria-hidden />
<span className="sr-only">Verknüpft mit </span>#{task.sourceIssue}
</span>
) : null}
{task.description ? (
<AlignLeft className="h-2.5 w-2.5 text-slate-600" aria-label="hat Beschreibung" />
) : null}