From 94e632681e6dba9e634d654ea95408e84d7f6f1c Mon Sep 17 00:00:00 2001 From: Rajpreet0 Date: Tue, 15 Sep 2026 13:53:27 +0200 Subject: [PATCH] see comments --- .../courses/actions/publish-drafts-actions.ts | 36 ++++---- .../ui/components/PublishDraftsDialog.tsx | 16 +++- .../courses/ui/views/course-list-view.tsx | 13 +-- tests/courses/publish-drafts-actions.test.ts | 84 +++++++++++++------ 4 files changed, 98 insertions(+), 51 deletions(-) diff --git a/src/modules/courses/actions/publish-drafts-actions.ts b/src/modules/courses/actions/publish-drafts-actions.ts index 3f291a5..db5a0fa 100644 --- a/src/modules/courses/actions/publish-drafts-actions.ts +++ b/src/modules/courses/actions/publish-drafts-actions.ts @@ -240,29 +240,29 @@ export async function publishDraftCourses(courseIds: string[]) { } // IDs, die die DB-Abfrage gar nicht zurückgegeben hat (gelöscht, fremd oder - // inzwischen nicht mehr "draft"), sind ebenfalls übersprungen. + // inzwischen nicht mehr "draft"), sind übersprungen. const foundIds = new Set(courses.map((c) => c.id)); - const vanishedCount = courseIds.filter((id) => !foundIds.has(id)).length; + let vanishedCount = courseIds.filter((id) => !foundIds.has(id)).length; let publishedIds: string[] = []; if (publishableIds.length > 0) { - // In einer Transaktion, damit das nachgelagerte findMany nur die Zeilen - // sieht, die dieses updateMany erfasst hat. status erneut im where: - // zwischen findMany und updateMany kann der Kurs in einem parallelen - // Request veröffentlicht oder abgesagt worden sein. - publishedIds = await prisma.$transaction(async (tx) => { - await tx.course.updateMany({ - where: { id: { in: publishableIds }, createdBy: auth.managerId, status: "draft" }, - data: { status: "published" }, - }); - - const updated = await tx.course.findMany({ - where: { id: { in: publishableIds }, createdBy: auth.managerId, status: "published" }, - select: { id: true }, - }); - - return updated.map((c) => c.id); + // updateManyAndReturn liefert genau die Zeilen zurück, die DIESES Statement + // geändert hat. Ein nachgelagertes findMany auf status "published" würde + // dagegen auch Kurse zurückgeben, die ein paralleler Request zwischen dem + // findMany oben und hier veröffentlicht hat — die würden wir uns fälschlich + // zuschreiben und an den Client melden. status: "draft" im where sorgt dafür, + // dass ein zwischenzeitlich abgesagter Kurs nicht doch noch live geht. + const updated = await prisma.course.updateManyAndReturn({ + where: { id: { in: publishableIds }, createdBy: auth.managerId, status: "draft" }, + data: { status: "published" }, + select: { id: true }, }); + + publishedIds = updated.map((c) => c.id); + + // Differenz = Kurse, die zwischen Prüfung und Update von woanders + // verändert wurden. Sie zählen als übersprungen, nicht als veröffentlicht. + vanishedCount += publishableIds.length - publishedIds.length; } revalidatePath("/courses"); diff --git a/src/modules/courses/ui/components/PublishDraftsDialog.tsx b/src/modules/courses/ui/components/PublishDraftsDialog.tsx index 91da530..93a8416 100644 --- a/src/modules/courses/ui/components/PublishDraftsDialog.tsx +++ b/src/modules/courses/ui/components/PublishDraftsDialog.tsx @@ -97,6 +97,16 @@ function PublishDraftsDialogBody({ useEffect(() => { let cancelled = false; + // Schlägt das Laden fehl, muss der alte Stand weg: publishDraftCourses + // bekommt den Zeitraum nicht mit und würde einen noch gültigen Entwurf aus + // dem VORHERIGEN Zeitraum anstandslos veröffentlichen, wenn er nach dem + // Fehler ausgewählt im Dialog stehen bleibt. + const clearDrafts = () => { + setDrafts([]); + setSelectedIds(new Set()); + setPage(0); + }; + (async () => { setIsLoading(true); try { @@ -115,10 +125,14 @@ function PublishDraftsDialogBody({ ); setPage(0); } else { + clearDrafts(); toast.error(result.error); } } catch { - if (!cancelled) toast.error("Entwürfe konnten nicht geladen werden."); + if (!cancelled) { + clearDrafts(); + toast.error("Entwürfe konnten nicht geladen werden."); + } } finally { if (!cancelled) setIsLoading(false); } diff --git a/src/modules/courses/ui/views/course-list-view.tsx b/src/modules/courses/ui/views/course-list-view.tsx index bdd2eef..b937e85 100644 --- a/src/modules/courses/ui/views/course-list-view.tsx +++ b/src/modules/courses/ui/views/course-list-view.tsx @@ -289,11 +289,6 @@ const CourseListView = ({ ].join("-"); }; - // Steuert nur, OB der Veröffentlichen-Button erscheint. Bewusst ohne Zahl am - // Button: hier sind nur die geladenen Kurse bekannt, der Dialog zeigt aber - // alle kommenden Entwürfe — eine Zahl hier würde der Zahl dort widersprechen. - const hasDrafts = courses.some((c) => c.status === "draft" && !isPastCourse(c.date)); - // Der Dialog übernimmt den Datumsfilter des Nutzers, falls gesetzt. Ohne // Filter bleibt "bis" leer = keine obere Grenze, es werden also alle // kommenden Entwürfe gezeigt. Bewusst NICHT loadedDateTo: das ist der @@ -438,7 +433,13 @@ const CourseListView = ({ )} - {canEdit && hasDrafts && ( + {/* Bewusst nicht an die geladenen Kurse gekoppelt: die Liste zeigt nur + ein Datumsfenster (initial heute ±14 Tage), der Dialog dagegen alle + kommenden Entwürfe. Ein Entwurf außerhalb des Fensters würde den + Button sonst verstecken — und bei leerem Fenster fehlt auch + "Mehr laden", der Nutzer käme gar nicht mehr an seine Entwürfe. + Der Dialog hat für "nichts gefunden" einen eigenen Leerzustand. */} + {canEdit && (