-
Notifications
You must be signed in to change notification settings - Fork 0
Improve onboarding and team settings on dashboard branch #109
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
5f4a186
357dbcb
4c4f447
94cf889
56ee51e
e6b8572
9b48b3d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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' }); | ||||||||||||
|
|
||||||||||||
| 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 } | ||||||||||||
| ); | ||||||||||||
|
|
||||||||||||
|
||||||||||||
| if (!updated) { | |
| return res.status(404).json({ message: 'Team not found' }); | |
| } |
Copilot
AI
Apr 20, 2026
There was a problem hiding this comment.
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).
| 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
|
||
| - 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. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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); | ||
|
|
||
|
|
@@ -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
|
||
|
|
||
| useEffect(() => { | ||
| return () => { | ||
| if (renameSavedTimerRef.current) { | ||
| window.clearTimeout(renameSavedTimerRef.current); | ||
| } | ||
| }; | ||
| }, []); | ||
|
|
||
|
|
||
| useEffect(() => { | ||
| const fetchAllTeams = async () => { | ||
|
|
@@ -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." }); | ||
|
|
@@ -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> | ||
|
|
||
|
|
||
There was a problem hiding this comment.
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 becauseTeam.findById(teamId)will throw a CastError and the catch returns 500. Since this is client input, consider validatingteamIdup front and returning 400 (e.g., viamongoose.Types.ObjectId.isValid(teamId)) to avoid misclassifying bad requests as server failures.