Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions backend/routes/teamRoutes.js
Original file line number Diff line number Diff line change
Expand Up @@ -347,4 +347,38 @@ router.patch('/:teamId/transfer-ownership', verifyToken, async (req, res) => {
}
});

router.patch('/:teamId/name', verifyToken, async (req, res) => {
const { teamId } = req.params;
const uid = req.user.uid;
const nextName = typeof req.body?.name === 'string' ? req.body.name.trim() : '';

if (!nextName) {
return res.status(400).json({ message: 'Team name is required' });
}

if (nextName.length > 80) {
return res.status(400).json({ message: 'Team name must be 80 characters or fewer' });
}

try {
const team = await Team.findById(teamId).lean();
if (!team) return res.status(404).json({ message: 'Team not found' });

Comment on lines +350 to +366
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This endpoint treats an invalid teamId (non-ObjectId) as a server error because Team.findById(teamId) will throw a CastError and the catch returns 500. Since this is client input, consider validating teamId up front and returning 400 (e.g., via mongoose.Types.ObjectId.isValid(teamId)) to avoid misclassifying bad requests as server failures.

Copilot uses AI. Check for mistakes.
if (team.ownerId !== uid) {
return res.status(403).json({ message: 'Only the team owner can rename the team' });
}

const updated = await Team.findByIdAndUpdate(
teamId,
{ $set: { name: nextName } },
{ returnDocument: 'after', lean: true }
);

Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

findByIdAndUpdate can return null (e.g., if the team is deleted between the initial findById and the update). Right now the route responds 200 with null in that case. Add a guard for !updated and return a 404 (or re-fetch) to avoid returning a successful response with an empty body.

Suggested change
if (!updated) {
return res.status(404).json({ message: 'Team not found' });
}

Copilot uses AI. Check for mistakes.
res.status(200).json(normalizeDoc(updated));
} catch (error) {
console.error('Error renaming team:', error);
res.status(500).json({ message: 'Server error' });
}
});
Comment on lines +350 to +382
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New behavior is introduced here (owner-only team renaming with validation), but there are currently no automated tests covering team routes in backend/tests/. Since the repo already uses Jest + Supertest for route/security coverage (e.g. backend/tests/project_security.test.js), please add tests for this endpoint (owner can rename, non-owner gets 403, empty/too-long name gets 400).

Copilot uses AI. Check for mistakes.

module.exports = router;
10 changes: 10 additions & 0 deletions dashboard-commit-log.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Dashboard Branch Commit Log

This file tracks requested commit sequencing for the `dashboard` branch.

Comment on lines +1 to +4
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description mentions changes to post-login onboarding routing (/welcome vs /dashboard), but the diff shown here only includes team rename UI/API and this commit-log doc. If onboarding routing changes are intended in this PR, they appear to be missing from the actual changes; otherwise, please update the PR description to match the scope.

Copilot uses AI. Check for mistakes.
- 01/23: Initialize dashboard branch commit log.
- 02/23: Add explicit branch execution note.
- 03/23: Document incremental commit sequencing.
- 04/23: Record staged branch progress marker.
- 05/23: Capture commit cadence checkpoint.
- 06/23: Add workflow continuity marker.
104 changes: 104 additions & 0 deletions src/components/views/SettingsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -922,6 +922,10 @@ export default function SettingsView() {
function TeamTabContent({ currentUser, userData, teamsData, setTeamsData, teamLoading, setTeamLoading, setUserData }: any) {
const [actionLoading, setActionLoading] = useState(false);
const [selectedTeamId, setSelectedTeamId] = useState<string | null>(null);
const [teamNameDraft, setTeamNameDraft] = useState("");
const [renameSaved, setRenameSaved] = useState(false);
const teamNameInputRef = useRef<HTMLInputElement>(null);
const renameSavedTimerRef = useRef<number | null>(null);

const selectedTeam = teamsData.find((t: any) => (t.id || t._id) === selectedTeamId);

Expand All @@ -932,6 +936,32 @@ function TeamTabContent({ currentUser, userData, teamsData, setTeamsData, teamLo
}
}, [teamsData, selectedTeamId]);

useEffect(() => {
setTeamNameDraft(selectedTeam?.name || "");
setRenameSaved(false);
}, [selectedTeamId, selectedTeam?.name]);

useEffect(() => {
if (!selectedTeam || selectedTeam.ownerId !== currentUser?.uid) {
return;
}

const focusTimer = window.setTimeout(() => {
teamNameInputRef.current?.focus();
teamNameInputRef.current?.select();
}, 0);

return () => window.clearTimeout(focusTimer);
}, [selectedTeamId, selectedTeam, currentUser?.uid]);
Comment on lines +944 to +955
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The autofocus effect depends on the entire selectedTeam object, so any update to team details (e.g. removing a member, transferring ownership, renaming) will re-run the effect and can steal focus back to the team name input unexpectedly. Consider tightening dependencies so focus only happens when the selected team changes (e.g., selectedTeamId and an isOwner boolean / selectedTeam?.ownerId), rather than on every selectedTeam reference change.

Copilot uses AI. Check for mistakes.

useEffect(() => {
return () => {
if (renameSavedTimerRef.current) {
window.clearTimeout(renameSavedTimerRef.current);
}
};
}, []);


useEffect(() => {
const fetchAllTeams = async () => {
Expand Down Expand Up @@ -1131,6 +1161,59 @@ function TeamTabContent({ currentUser, userData, teamsData, setTeamsData, teamLo
}
};

const handleRenameTeam = async (teamId: string) => {
if (!currentUser) return;
const nextName = teamNameDraft.trim();
if (!nextName) {
toast({ title: "Invalid Name", description: "Team name cannot be empty.", variant: "destructive" });
return;
}
if (nextName === selectedTeam?.name) {
return;
}

if (renameSavedTimerRef.current) {
window.clearTimeout(renameSavedTimerRef.current);
renameSavedTimerRef.current = null;
}

setActionLoading(true);
try {
const token = await currentUser.getIdToken();
const res = await fetch(`${API_BASE_URL}/api/teams/${teamId}/name`, {
method: 'PATCH',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ name: nextName })
});

if (!res.ok) {
const err = await res.json();
throw new Error(err.message || 'Failed to rename team');
}

const updatedTeam = await res.json();
const updatedId = updatedTeam.id || updatedTeam._id;
setTeamsData((prev: any[]) =>
prev.map((team: any) => ((team.id || team._id) === updatedId ? { ...team, name: updatedTeam.name } : team))
);

setRenameSaved(true);
renameSavedTimerRef.current = window.setTimeout(() => {
setRenameSaved(false);
renameSavedTimerRef.current = null;
}, 2000);

toast({ title: "Team Updated", description: "Team name changed successfully." });
} catch (error: any) {
toast({ title: "Error", description: error.message, variant: "destructive" });
} finally {
setActionLoading(false);
}
};

const copyInviteCode = (code: string) => {
navigator.clipboard.writeText(code);
toast({ title: "Copied!", description: "Invite code copied to clipboard." });
Expand Down Expand Up @@ -1243,6 +1326,27 @@ function TeamTabContent({ currentUser, userData, teamsData, setTeamsData, teamLo
</div>
</div>
</div>
{selectedTeam.ownerId === currentUser?.uid && (
<div className="mt-4 space-y-2">
<Label className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">Team Name</Label>
<div className="flex flex-col gap-2 sm:flex-row">
<Input
ref={teamNameInputRef}
value={teamNameDraft}
onChange={(e) => setTeamNameDraft(e.target.value)}
placeholder="Enter team name"
maxLength={80}
/>
<Button
type="button"
onClick={() => handleRenameTeam(selectedTeam.id || selectedTeam._id)}
disabled={actionLoading || !teamNameDraft.trim() || teamNameDraft.trim() === selectedTeam.name}
>
{renameSaved ? "Saved" : "Save Name"}
</Button>
</div>
</div>
)}
</CardContent>
</Card>

Expand Down
Loading