feat(web): add teams settings page (MS21-UI-005) (#576)
All checks were successful
ci/woodpecker/push/web Pipeline was successful
All checks were successful
ci/woodpecker/push/web Pipeline was successful
Co-authored-by: Jason Woltje <jason@diversecanvas.com> Co-committed-by: Jason Woltje <jason@diversecanvas.com>
This commit was merged in pull request #576.
This commit is contained in:
@@ -2,136 +2,25 @@
|
||||
|
||||
import type { ReactElement } from "react";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { TeamSettings } from "@/components/team/TeamSettings";
|
||||
import { TeamMemberList } from "@/components/team/TeamMemberList";
|
||||
import { mockTeamWithMembers } from "@/lib/api/teams";
|
||||
import type { User } from "@mosaic/shared";
|
||||
import type { TeamMemberRole } from "@mosaic/shared";
|
||||
import { useParams } from "next/navigation";
|
||||
import { ComingSoon } from "@/components/ui/ComingSoon";
|
||||
import Link from "next/link";
|
||||
|
||||
// Mock available users for adding to team
|
||||
const mockAvailableUsers: User[] = [
|
||||
{
|
||||
id: "user-3",
|
||||
email: "alice@example.com",
|
||||
name: "Alice Johnson",
|
||||
emailVerified: true,
|
||||
image: null,
|
||||
authProviderId: null,
|
||||
preferences: {},
|
||||
deactivatedAt: null,
|
||||
isLocalAuth: false,
|
||||
passwordHash: null,
|
||||
invitedBy: null,
|
||||
invitationToken: null,
|
||||
invitedAt: null,
|
||||
createdAt: new Date("2026-01-17"),
|
||||
updatedAt: new Date("2026-01-17"),
|
||||
},
|
||||
{
|
||||
id: "user-4",
|
||||
email: "bob@example.com",
|
||||
name: "Bob Wilson",
|
||||
emailVerified: true,
|
||||
image: null,
|
||||
authProviderId: null,
|
||||
preferences: {},
|
||||
deactivatedAt: null,
|
||||
isLocalAuth: false,
|
||||
passwordHash: null,
|
||||
invitedBy: null,
|
||||
invitationToken: null,
|
||||
invitedAt: null,
|
||||
createdAt: new Date("2026-01-18"),
|
||||
updatedAt: new Date("2026-01-18"),
|
||||
},
|
||||
];
|
||||
|
||||
export default function TeamDetailPage(): ReactElement {
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
const workspaceId = params.id as string;
|
||||
// const teamId = params.teamId as string; // Will be used for API calls
|
||||
|
||||
// TODO: Replace with real API call when backend is ready
|
||||
// const { data: team, isLoading } = useQuery({
|
||||
// queryKey: ["team", workspaceId, params.teamId],
|
||||
// queryFn: () => fetchTeam(workspaceId, params.teamId as string),
|
||||
// });
|
||||
|
||||
const [team] = useState(mockTeamWithMembers);
|
||||
const [isLoading] = useState(false);
|
||||
|
||||
const handleUpdateTeam = (data: { name?: string; description?: string }): Promise<void> => {
|
||||
// TODO: Replace with real API call
|
||||
// await updateTeam(workspaceId, teamId, data);
|
||||
console.log("Updating team:", data);
|
||||
// TODO: Refetch team data
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
const handleDeleteTeam = (): Promise<void> => {
|
||||
// TODO: Replace with real API call
|
||||
// await deleteTeam(workspaceId, teamId);
|
||||
console.log("Deleting team");
|
||||
|
||||
// Navigate back to teams list
|
||||
router.push(`/settings/workspaces/${workspaceId}/teams`);
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
const handleAddMember = (userId: string, role?: TeamMemberRole): Promise<void> => {
|
||||
// TODO: Replace with real API call
|
||||
// await addTeamMember(workspaceId, teamId, { userId, role });
|
||||
console.log("Adding member:", { userId, role });
|
||||
// TODO: Refetch team data
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
const handleRemoveMember = (userId: string): Promise<void> => {
|
||||
// TODO: Replace with real API call
|
||||
// await removeTeamMember(workspaceId, teamId, userId);
|
||||
console.log("Removing member:", userId);
|
||||
// TODO: Refetch team data
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<main className="container mx-auto px-4 py-8">
|
||||
<div className="flex justify-center items-center p-8">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900"></div>
|
||||
<span className="ml-3 text-gray-600">Loading team...</span>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="container mx-auto px-4 py-8">
|
||||
<div className="mb-8">
|
||||
<Link
|
||||
href={`/settings/workspaces/${workspaceId}/teams`}
|
||||
className="text-blue-600 hover:text-blue-700 text-sm mb-2 inline-block"
|
||||
>
|
||||
← Back to Teams
|
||||
</Link>
|
||||
<h1 className="text-3xl font-bold text-gray-900">{team.name}</h1>
|
||||
{team.description && <p className="text-gray-600 mt-2">{team.description}</p>}
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<TeamSettings team={team} onUpdate={handleUpdateTeam} onDelete={handleDeleteTeam} />
|
||||
|
||||
<TeamMemberList
|
||||
members={team.members}
|
||||
onAddMember={handleAddMember}
|
||||
onRemoveMember={handleRemoveMember}
|
||||
availableUsers={mockAvailableUsers}
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
<ComingSoon
|
||||
feature="Team Details"
|
||||
description="Team member management is being migrated to live API-backed data."
|
||||
>
|
||||
<Link
|
||||
href={`/settings/workspaces/${workspaceId}/teams`}
|
||||
className="text-sm text-blue-600 hover:text-blue-700"
|
||||
>
|
||||
{"<-"} Back to Teams
|
||||
</Link>
|
||||
</ComingSoon>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,63 +2,90 @@
|
||||
|
||||
import type { ReactElement } from "react";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { TeamCard } from "@/components/team/TeamCard";
|
||||
import { ComingSoon } from "@/components/ui/ComingSoon";
|
||||
import { Button, Input, Modal } from "@mosaic/ui";
|
||||
import { mockTeams } from "@/lib/api/teams";
|
||||
import { createTeam, fetchTeams, type CreateTeamDto, type TeamRecord } from "@/lib/api/teams";
|
||||
import Link from "next/link";
|
||||
|
||||
// Check if we're in development mode
|
||||
const isDevelopment = process.env.NODE_ENV === "development";
|
||||
|
||||
/**
|
||||
* Teams Page Content - Development Only
|
||||
* Shows mock team data for development purposes
|
||||
*/
|
||||
function getErrorMessage(error: unknown, fallback: string): string {
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function TeamsPageContent(): ReactElement {
|
||||
const params = useParams();
|
||||
const workspaceId = params.id as string;
|
||||
|
||||
// TODO: Replace with real API call when backend is ready
|
||||
// const { data: teams, isLoading } = useQuery({
|
||||
// queryKey: ["teams", workspaceId],
|
||||
// queryFn: () => fetchTeams(workspaceId),
|
||||
// });
|
||||
|
||||
const [teams] = useState(mockTeams);
|
||||
const [isLoading] = useState(false);
|
||||
const [teams, setTeams] = useState<TeamRecord[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [loadError, setLoadError] = useState<string | null>(null);
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||
const [newTeamName, setNewTeamName] = useState("");
|
||||
const [newTeamDescription, setNewTeamDescription] = useState("");
|
||||
const [createError, setCreateError] = useState<string | null>(null);
|
||||
|
||||
const handleCreateTeam = (): void => {
|
||||
if (!newTeamName.trim()) return;
|
||||
const loadTeams = useCallback(async (): Promise<void> => {
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const data = await fetchTeams(workspaceId);
|
||||
setTeams(data);
|
||||
setLoadError(null);
|
||||
} catch (error) {
|
||||
setLoadError(getErrorMessage(error, "Failed to load teams"));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [workspaceId]);
|
||||
|
||||
useEffect(() => {
|
||||
void loadTeams();
|
||||
}, [loadTeams]);
|
||||
|
||||
const handleCreateTeam = async (): Promise<void> => {
|
||||
const teamName = newTeamName.trim();
|
||||
if (!teamName) return;
|
||||
|
||||
setIsCreating(true);
|
||||
try {
|
||||
// TODO: Replace with real API call
|
||||
// await createTeam(workspaceId, {
|
||||
// name: newTeamName,
|
||||
// description: newTeamDescription || undefined,
|
||||
// });
|
||||
setCreateError(null);
|
||||
|
||||
try {
|
||||
const description = newTeamDescription.trim();
|
||||
const dto: CreateTeamDto = {
|
||||
name: teamName,
|
||||
};
|
||||
if (description.length > 0) {
|
||||
dto.description = description;
|
||||
}
|
||||
|
||||
await createTeam(dto, workspaceId);
|
||||
|
||||
// Reset form
|
||||
setNewTeamName("");
|
||||
setNewTeamDescription("");
|
||||
setShowCreateModal(false);
|
||||
|
||||
// TODO: Refresh teams list
|
||||
} catch (_error) {
|
||||
console.error("Failed to create team:", _error);
|
||||
alert("Failed to create team. Please try again.");
|
||||
await loadTeams();
|
||||
} catch (error) {
|
||||
setCreateError(getErrorMessage(error, "Failed to create team"));
|
||||
} finally {
|
||||
setIsCreating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const teamsForCards = teams.map((team) => ({
|
||||
...team,
|
||||
createdAt: new Date(team.createdAt),
|
||||
updatedAt: new Date(team.updatedAt),
|
||||
}));
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<main className="container mx-auto px-4 py-8">
|
||||
@@ -80,6 +107,7 @@ function TeamsPageContent(): ReactElement {
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
setCreateError(null);
|
||||
setShowCreateModal(true);
|
||||
}}
|
||||
>
|
||||
@@ -87,7 +115,11 @@ function TeamsPageContent(): ReactElement {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{teams.length === 0 ? (
|
||||
{loadError !== null ? (
|
||||
<div className="rounded-md border border-red-200 bg-red-50 px-4 py-3 text-red-700">
|
||||
{loadError}
|
||||
</div>
|
||||
) : teamsForCards.length === 0 ? (
|
||||
<div className="text-center p-12 bg-gray-50 rounded-lg">
|
||||
<p className="text-lg text-gray-500 mb-4">No teams yet</p>
|
||||
<p className="text-sm text-gray-400 mb-6">
|
||||
@@ -96,6 +128,7 @@ function TeamsPageContent(): ReactElement {
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
setCreateError(null);
|
||||
setShowCreateModal(true);
|
||||
}}
|
||||
>
|
||||
@@ -104,13 +137,12 @@ function TeamsPageContent(): ReactElement {
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{teams.map((team) => (
|
||||
{teamsForCards.map((team) => (
|
||||
<TeamCard key={team.id} team={team} workspaceId={workspaceId} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Create Team Modal */}
|
||||
{showCreateModal && (
|
||||
<Modal
|
||||
isOpen={showCreateModal}
|
||||
@@ -143,6 +175,11 @@ function TeamsPageContent(): ReactElement {
|
||||
fullWidth
|
||||
disabled={isCreating}
|
||||
/>
|
||||
{createError !== null && (
|
||||
<div className="rounded-md border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700">
|
||||
{createError}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex gap-2 justify-end pt-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -155,7 +192,7 @@ function TeamsPageContent(): ReactElement {
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleCreateTeam}
|
||||
onClick={() => void handleCreateTeam()}
|
||||
disabled={!newTeamName.trim() || isCreating}
|
||||
>
|
||||
{isCreating ? "Creating..." : "Create Team"}
|
||||
@@ -168,12 +205,7 @@ function TeamsPageContent(): ReactElement {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Teams Page Entry Point
|
||||
* Shows development content or Coming Soon based on environment
|
||||
*/
|
||||
export default function TeamsPage(): ReactElement {
|
||||
// In production, show Coming Soon placeholder
|
||||
if (!isDevelopment) {
|
||||
return (
|
||||
<ComingSoon
|
||||
@@ -187,6 +219,5 @@ export default function TeamsPage(): ReactElement {
|
||||
);
|
||||
}
|
||||
|
||||
// In development, show the full page with mock data
|
||||
return <TeamsPageContent />;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user