CI / pruefen (pull_request) Canceled after 0s
Ein Projekt mit vierzig offenen Aufgaben macht das Brett unbrauchbar: die Spalte laeuft ueber die halbe Seite, und die Bloecke darunter - Notizen, Repository, Verlauf - rutschen aus dem Blick. Die Grenze steht in den Admin-Einstellungen und gilt fuer alle Projekte, wie autoSyncBatchSize und staleAfterDays. Kein Feld am Projekt: das haette eine Migration am Project-Modell gekostet und eine Einstellung, die man vierzig Mal pflegt. 0 heisst "ohne Begrenzung", dieselbe Lesart wie bei SESSION_IDLE_HOURS. Begrenzt wird ausschliesslich die Anzeige. Das ist der Kern: persist() schickt je Spalte die vollstaendige Liste an /tasks/reorder, und eine dort schon gekuerzte Liste wuerde beim ersten Ziehen die verborgenen Aufgaben aus dem Brett loeschen. Der Zustand im Brett bleibt deshalb ganz, gekuerzt wird erst beim Rendern der Spalte. Gegengeprobt am echten PATCH-Rumpf: 40 IDs bei 15 sichtbaren Karten. Die Zahl neben der Spaltenueberschrift nennt weiter den vollen Bestand. Nur die sichtbaren Karten stehen im SortableContext - was nicht im DOM ist, kann dnd-kit ohnehin nicht treffen. Die Admin-Sektion zeigt neben der Einstellung die laengste Spalte im Bestand. Ohne die steht die Zahl im Leeren: eine Grenze von 15 sagt nichts, wenn die groesste Spalte aus drei Karten besteht. Refs #16
141 lines
5.0 KiB
TypeScript
141 lines
5.0 KiB
TypeScript
import type { Metadata } from "next";
|
||
import { notFound } from "next/navigation";
|
||
import { FileText } from "lucide-react";
|
||
import { ActivityFeed } from "@/components/ActivityFeed";
|
||
import { GitPanel } from "@/components/GitPanel";
|
||
import { Markdown } from "@/components/Markdown";
|
||
import { NotesPanel } from "@/components/NotesPanel";
|
||
import { ReadmePanel } from "@/components/ReadmePanel";
|
||
import { ProjectHeader } from "@/components/ProjectHeader";
|
||
import { TaskBoard } from "@/components/tasks/TaskBoard";
|
||
import { requireUserPage } from "@/lib/auth/guard";
|
||
import { getSessionUser } from "@/lib/auth/session";
|
||
import { getAppSettings } from "@/lib/auth/settings";
|
||
import { prisma } from "@/lib/db";
|
||
import { connectionSafeSelect, projectDetailInclude, webhookRepositoryIds } from "@/lib/projects";
|
||
import type { ConnectionSafe, ProjectDetail } from "@/types";
|
||
|
||
export const dynamic = "force-dynamic";
|
||
|
||
type PageProps = { params: Promise<{ id: string }> };
|
||
|
||
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
|
||
const user = await getSessionUser();
|
||
if (!user) return { title: "Projekt" };
|
||
|
||
const { id } = await params;
|
||
const project = await prisma.project.findFirst({
|
||
where: { id, ownerId: user.id },
|
||
select: { name: true, summary: true },
|
||
});
|
||
return {
|
||
title: project?.name ?? "Projekt",
|
||
description: project?.summary ?? undefined,
|
||
};
|
||
}
|
||
|
||
export default async function ProjectPage({ params }: PageProps) {
|
||
const user = await requireUserPage();
|
||
const { id } = await params;
|
||
|
||
const [project, connections, settings] = await Promise.all([
|
||
// findFirst mit Besitzerbedingung: ein fremdes Projekt fuehrt zu 404,
|
||
// nicht zu einer Fehlermeldung, die seine Existenz bestaetigt.
|
||
prisma.project.findFirst({
|
||
where: { id, ownerId: user.id },
|
||
include: projectDetailInclude,
|
||
}),
|
||
prisma.gitConnection.findMany({
|
||
where: { ownerId: user.id },
|
||
select: connectionSafeSelect,
|
||
orderBy: { name: "asc" },
|
||
}),
|
||
getAppSettings(),
|
||
]);
|
||
|
||
if (!project) notFound();
|
||
|
||
const detail = project as unknown as ProjectDetail;
|
||
|
||
// Nur die Angabe "hier haengt ein Webhook", nicht das Geheimnis selbst.
|
||
const webhookIds = await webhookRepositoryIds(detail.id);
|
||
|
||
const fuehrend = detail.repositories[0]?.snapshot;
|
||
const readme = fuehrend?.readme
|
||
? {
|
||
content: fuehrend.readme,
|
||
path: fuehrend.readmePath,
|
||
baseUrl: fuehrend.readmeBaseUrl,
|
||
// Die Basis endet auf dem Verzeichnis; angehaengt ergibt das die Datei.
|
||
fileUrl:
|
||
fuehrend.readmeBaseUrl && fuehrend.readmePath
|
||
? `${fuehrend.readmeBaseUrl}${fuehrend.readmePath.split("/").pop()}`
|
||
: (fuehrend.htmlUrl ?? null),
|
||
}
|
||
: null;
|
||
|
||
return (
|
||
<div className="space-y-6 pt-4">
|
||
<ProjectHeader
|
||
project={detail}
|
||
connections={connections as unknown as ConnectionSafe[]}
|
||
/>
|
||
|
||
<TaskBoard
|
||
projectId={detail.id}
|
||
tasks={detail.tasks}
|
||
progressFromTasks={detail.progressFromTasks}
|
||
columnLimit={settings.taskColumnLimit}
|
||
/>
|
||
|
||
{/*
|
||
min-w-0 an beiden Spalten ist hier nicht Kosmetik, sondern der Kern:
|
||
ein Rasterkind hat "min-width: auto" und weigert sich damit, unter
|
||
die Inhaltsbreite seines breitesten Kindes zu schrumpfen. Die
|
||
Heatmap ist ueber 700px breit – auf dem Telefon zog sie damit die
|
||
ganze Spalte und mit ihr die Seite auseinander. Erst mit min-w-0
|
||
greift das overflow-x-auto darin ueberhaupt.
|
||
*/}
|
||
<div className="grid gap-6 lg:grid-cols-12">
|
||
<div className="min-w-0 space-y-6 lg:col-span-5">
|
||
<section className="glass p-5">
|
||
<h2 className="flex items-center gap-2 text-sm font-semibold uppercase tracking-wider text-slate-300">
|
||
<FileText className="h-4 w-4 text-cyan-300" />
|
||
Beschreibung
|
||
</h2>
|
||
{detail.description ? (
|
||
<Markdown className="mt-3">{detail.description}</Markdown>
|
||
) : (
|
||
<p className="mt-3 text-sm text-slate-500">
|
||
Keine Beschreibung hinterlegt. Ueber „Bearbeiten“ kannst du die Idee ausformulieren.
|
||
</p>
|
||
)}
|
||
</section>
|
||
|
||
{/* Das README des fuehrenden Repositories – bei mehreren waere eine
|
||
zweite Vorschau untereinander mehr Laenge als Nutzen. */}
|
||
{readme ? (
|
||
<ReadmePanel
|
||
content={readme.content}
|
||
path={readme.path}
|
||
baseUrl={readme.baseUrl}
|
||
fileUrl={readme.fileUrl}
|
||
/>
|
||
) : null}
|
||
|
||
<NotesPanel projectId={detail.id} notes={detail.notes} />
|
||
</div>
|
||
|
||
<div className="min-w-0 space-y-6 lg:col-span-7">
|
||
<GitPanel
|
||
project={detail}
|
||
connections={connections as unknown as ConnectionSafe[]}
|
||
webhookRepoIds={webhookIds}
|
||
/>
|
||
<ActivityFeed projectId={detail.id} initial={detail.activities} />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|