From 8f54e0b4fe2efa53d63bc3ef17c5762185f74faf Mon Sep 17 00:00:00 2001
From: Sabine Maennel <5292683+sabinem@users.noreply.github.com>
Date: Fri, 9 Oct 2026 06:04:07 +0200
Subject: [PATCH] feat(frontend): allow adding teams once teams are fixed
Fixed teams (a submission exists or teams are published) only need
protecting from deletion, which would take submissions with them or
undo what participants were told. "+ Add Team" is back in that state, a
team added but not yet saved can be removed again, and the save action
refuses only plans that delete a saved team. An empty new team is not
created.
---
CHANGELOG.md | 6 +--
.../server/hackathon/teamAssignmentLock.ts | 10 ++--
.../[id]/teams/manage/+page.server.ts | 17 +++----
.../hackathon/[id]/teams/manage/+page.svelte | 49 ++++++++++---------
4 files changed, 42 insertions(+), 40 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 066e6266..7d353b9f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -19,9 +19,9 @@ written while it was being built. See [RELEASING.md](RELEASING.md).
### Added
- Manage Teams fixes the teams once a team has a submission or teams are
- published: a notice says why, and uploading, adding and deleting teams are
- disabled. People can still be moved between teams or to Unassigned by hand,
- and teams renamed.
+ published: a notice says why, and uploading and deleting teams are disabled.
+ People can still be moved between teams or to Unassigned by hand, teams
+ renamed, and new teams added.
- Manage Teams can filter the Unassigned column by registration answers: click
an answer (or type into a free-text question's box) to bring the people who
gave it to the top; everyone else stays below, greyed out. Each answer shows
diff --git a/components/frontend/src/lib/server/hackathon/teamAssignmentLock.ts b/components/frontend/src/lib/server/hackathon/teamAssignmentLock.ts
index 595cfb08..7154f8e4 100644
--- a/components/frontend/src/lib/server/hackathon/teamAssignmentLock.ts
+++ b/components/frontend/src/lib/server/hackathon/teamAssignmentLock.ts
@@ -11,9 +11,9 @@ import { enabledCapabilities } from "./phaseForm"
/**
* Why the teams themselves are fixed — empty while they are not.
*
- * Fixed means no team may be added or deleted, and so no upload, which
- * replaces every team. People may still be moved by hand and teams renamed:
- * that is how an organizer handles someone dropping out late. Two things fix
+ * Fixed means no team may be deleted, and so no upload, which replaces every
+ * team. People may still be moved by hand, teams renamed and new teams added:
+ * that is how an organizer handles a late dropout or a late joiner. Two things fix
* the teams, each because rebuilding them would pull the ground from under
* somebody:
*
@@ -25,8 +25,8 @@ import { enabledCapabilities } from "./phaseForm"
* Either one is enough, and the reasons are listed so the page can say which.
*
* TODO(backend: team-assignment-lock): this is a frontend rule only. The page
- * stops offering the controls and its save action refuses a plan that adds or
- * deletes a team, but `TeamService` still accepts that from any other caller.
+ * stops offering the controls and its save action refuses a plan that deletes
+ * a team, but `TeamService` still accepts that from any other caller.
* Planned as a backend check in a follow-up pull request; the frontend can
* then keep this for the explanation and rely on the error for the rule.
*/
diff --git a/components/frontend/src/routes/(app)/my/hackathon/[id]/teams/manage/+page.server.ts b/components/frontend/src/routes/(app)/my/hackathon/[id]/teams/manage/+page.server.ts
index e0b86ab8..18d1ecac 100644
--- a/components/frontend/src/routes/(app)/my/hackathon/[id]/teams/manage/+page.server.ts
+++ b/components/frontend/src/routes/(app)/my/hackathon/[id]/teams/manage/+page.server.ts
@@ -249,19 +249,18 @@ export const actions: Actions = {
const before = all.filter((t) => approved.has(t.projectId))
if (locked.length > 0) {
- // Locked, the teams themselves stand: people may move between them or
- // to Unassigned and a team may be renamed, but none may be added or
- // deleted — deleting one takes its submissions with it, and published
- // teams are what participants have been told. An emptied team is kept.
+ // Locked, no team may be deleted: deleting one takes its submissions
+ // with it, and published teams are what participants have been told.
+ // Everything else stays open — people may move between teams or to
+ // Unassigned, teams may be renamed, and a new team may be added. An
+ // emptied team is kept; a new one nobody was put on is not created.
const planned = new Set(plan.map((t) => t.id))
- if (
- plan.some((t) => t.id === null) ||
- before.some((t) => !planned.has(t.id))
- ) {
+ if (before.some((t) => !planned.has(t.id))) {
return fail(409, {
- message: `Teams cannot be added or deleted now: ${locked.join(" and ")}.`,
+ message: `Teams cannot be deleted now: ${locked.join(" and ")}.`,
})
}
+ plan = plan.filter((t) => t.id !== null || t.memberIds.length > 0)
} else {
// A team with nobody in it is not part of the assignment: dropped, so
// a new one is never created and a saved one is deleted below like any
diff --git a/components/frontend/src/routes/(app)/my/hackathon/[id]/teams/manage/+page.svelte b/components/frontend/src/routes/(app)/my/hackathon/[id]/teams/manage/+page.svelte
index a48e1635..82cd9981 100644
--- a/components/frontend/src/routes/(app)/my/hackathon/[id]/teams/manage/+page.svelte
+++ b/components/frontend/src/routes/(app)/my/hackathon/[id]/teams/manage/+page.svelte
@@ -366,9 +366,12 @@
const base = fromServer(data.projectRows);
const nameBefore = new Map(base.map((t) => [t.key, t.name]));
// Save drops a team with nobody in it, so an empty one counts as
- // deleted, or as not added — the summary says what Save will do. Not
- // while the teams are fixed: then an emptied team is kept.
- const kept = locked ? teams : teams.filter((t) => t.memberIds.length > 0);
+ // deleted, or as not added — the summary says what Save will do. While
+ // the teams are fixed, an emptied saved team is kept; an empty new one
+ // is still not created.
+ const kept = teams.filter(
+ (t) => t.memberIds.length > 0 || (locked && t.id !== null)
+ );
const alive = new Set(kept.map((t) => t.key));
const added = kept.filter((t) => t.id === null).length;
@@ -438,10 +441,10 @@
dropTarget = null;
}
- // Once a submission exists or teams are published, the teams themselves
- // stand: no upload (it replaces every team), no adding, deleting or Clear
- // all. People can still be moved by hand — between teams or to Unassigned —
- // and teams renamed, and the save action enforces the same line. The
+ // Once a submission exists or teams are published, no team may go: no
+ // upload (it replaces every team), no deleting, no Clear all. People can
+ // still be moved by hand — between teams or to Unassigned — teams renamed
+ // and new teams added, and the save action enforces the same line. The
// reason is said once at the top and again on hover over what is off. See
// `assignmentLockReasons`.
const lockReasons = $derived(data.lockReasons);
@@ -692,8 +695,8 @@
Teams are fixed: - {lockReasons.join(' and ')}. Upload and adding or deleting teams are off; you can - still move people between teams or to Unassigned, and rename teams. + {lockReasons.join(' and ')}. Upload and deleting teams are off; you can still + move people between teams or to Unassigned, rename teams and add new ones.
{/if} @@ -819,7 +822,7 @@Once a team has a submission or teams are published, teams are fixed: no - upload, no adding or deleting teams. Moving people and renaming still work. + upload, no deleting teams. Moving people, renaming and adding teams still work.
@@ -1190,7 +1193,9 @@ >