feat(tasks): Karte zeigt, wenn ein Git-Issue daranhaengt
CI / pruefen (pull_request) Canceled after 0s
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:
@@ -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/);
|
||||
});
|
||||
});
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user