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:
@@ -1,14 +1,53 @@
|
||||
/**
|
||||
* Teams API Client
|
||||
* Handles team-related API requests
|
||||
* Handles workspace-scoped team API requests.
|
||||
*/
|
||||
|
||||
import type { Team, TeamMember, User } from "@mosaic/shared";
|
||||
import { TeamMemberRole } from "@mosaic/shared";
|
||||
import { apiGet, apiPost, apiPatch, apiDelete, type ApiResponse } from "./client";
|
||||
import type { TeamMemberRole } from "@mosaic/shared";
|
||||
import { apiDelete, apiGet, apiPost } from "./client";
|
||||
|
||||
export interface TeamWithMembers extends Team {
|
||||
members: (TeamMember & { user: User })[];
|
||||
const WORKSPACE_STORAGE_KEY = "mosaic-workspace-id";
|
||||
|
||||
function resolveWorkspaceId(explicitWorkspaceId?: string): string {
|
||||
if (explicitWorkspaceId !== undefined) {
|
||||
return explicitWorkspaceId;
|
||||
}
|
||||
|
||||
if (typeof window === "undefined") {
|
||||
throw new Error("Workspace context is unavailable outside the browser");
|
||||
}
|
||||
|
||||
const workspaceId = window.localStorage.getItem(WORKSPACE_STORAGE_KEY);
|
||||
if (!workspaceId) {
|
||||
throw new Error("No active workspace selected");
|
||||
}
|
||||
|
||||
return workspaceId;
|
||||
}
|
||||
|
||||
export interface TeamRecord {
|
||||
id: string;
|
||||
workspaceId: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
metadata: Record<string, unknown>;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
_count?: {
|
||||
members: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface TeamMemberRecord {
|
||||
teamId: string;
|
||||
userId: string;
|
||||
role: TeamMemberRole;
|
||||
joinedAt: string;
|
||||
user?: {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface CreateTeamDto {
|
||||
@@ -16,200 +55,81 @@ export interface CreateTeamDto {
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface UpdateTeamDto {
|
||||
name?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface AddTeamMemberDto {
|
||||
userId: string;
|
||||
role?: TeamMemberRole;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all teams for a workspace
|
||||
* Fetch all teams in the active workspace.
|
||||
*/
|
||||
export async function fetchTeams(workspaceId: string): Promise<Team[]> {
|
||||
const response = await apiGet<ApiResponse<Team[]>>(`/api/workspaces/${workspaceId}/teams`);
|
||||
return response.data;
|
||||
export async function fetchTeams(workspaceId?: string): Promise<TeamRecord[]> {
|
||||
const resolvedWorkspaceId = resolveWorkspaceId(workspaceId);
|
||||
return apiGet<TeamRecord[]>(`/api/workspaces/${resolvedWorkspaceId}/teams`, resolvedWorkspaceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a single team with members
|
||||
* Create a team in the active workspace.
|
||||
*/
|
||||
export async function fetchTeam(workspaceId: string, teamId: string): Promise<TeamWithMembers> {
|
||||
const response = await apiGet<ApiResponse<TeamWithMembers>>(
|
||||
`/api/workspaces/${workspaceId}/teams/${teamId}`
|
||||
export async function createTeam(dto: CreateTeamDto, workspaceId?: string): Promise<TeamRecord> {
|
||||
const resolvedWorkspaceId = resolveWorkspaceId(workspaceId);
|
||||
return apiPost<TeamRecord>(
|
||||
`/api/workspaces/${resolvedWorkspaceId}/teams`,
|
||||
dto,
|
||||
resolvedWorkspaceId
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new team
|
||||
* Fetch team members for a team in the active workspace.
|
||||
* The current backend route shape is workspace-scoped team membership.
|
||||
*/
|
||||
export async function createTeam(workspaceId: string, data: CreateTeamDto): Promise<Team> {
|
||||
const response = await apiPost<ApiResponse<Team>>(`/api/workspaces/${workspaceId}/teams`, data);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a team
|
||||
*/
|
||||
export async function updateTeam(
|
||||
workspaceId: string,
|
||||
export async function fetchTeamMembers(
|
||||
teamId: string,
|
||||
data: UpdateTeamDto
|
||||
): Promise<Team> {
|
||||
const response = await apiPatch<ApiResponse<Team>>(
|
||||
`/api/workspaces/${workspaceId}/teams/${teamId}`,
|
||||
data
|
||||
workspaceId?: string
|
||||
): Promise<TeamMemberRecord[]> {
|
||||
const resolvedWorkspaceId = resolveWorkspaceId(workspaceId);
|
||||
return apiGet<TeamMemberRecord[]>(
|
||||
`/api/workspaces/${resolvedWorkspaceId}/teams/${teamId}/members`,
|
||||
resolvedWorkspaceId
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a team
|
||||
* Delete a team in the active workspace.
|
||||
*/
|
||||
export async function deleteTeam(workspaceId: string, teamId: string): Promise<void> {
|
||||
await apiDelete(`/api/workspaces/${workspaceId}/teams/${teamId}`);
|
||||
export async function deleteTeam(teamId: string, workspaceId?: string): Promise<void> {
|
||||
const resolvedWorkspaceId = resolveWorkspaceId(workspaceId);
|
||||
await apiDelete(`/api/workspaces/${resolvedWorkspaceId}/teams/${teamId}`, resolvedWorkspaceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a member to a team
|
||||
* Add a member to a team in the active workspace.
|
||||
*/
|
||||
export async function addTeamMember(
|
||||
workspaceId: string,
|
||||
teamId: string,
|
||||
data: AddTeamMemberDto
|
||||
): Promise<TeamMember> {
|
||||
const response = await apiPost<ApiResponse<TeamMember>>(
|
||||
`/api/workspaces/${workspaceId}/teams/${teamId}/members`,
|
||||
data
|
||||
data: AddTeamMemberDto,
|
||||
workspaceId?: string
|
||||
): Promise<TeamMemberRecord> {
|
||||
const resolvedWorkspaceId = resolveWorkspaceId(workspaceId);
|
||||
return apiPost<TeamMemberRecord>(
|
||||
`/api/workspaces/${resolvedWorkspaceId}/teams/${teamId}/members`,
|
||||
data,
|
||||
resolvedWorkspaceId
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a member from a team
|
||||
* Remove a member from a team in the active workspace.
|
||||
*/
|
||||
export async function removeTeamMember(
|
||||
workspaceId: string,
|
||||
teamId: string,
|
||||
userId: string
|
||||
): Promise<void> {
|
||||
await apiDelete(`/api/workspaces/${workspaceId}/teams/${teamId}/members/${userId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a team member's role
|
||||
*/
|
||||
export async function updateTeamMemberRole(
|
||||
workspaceId: string,
|
||||
teamId: string,
|
||||
userId: string,
|
||||
role: TeamMemberRole
|
||||
): Promise<TeamMember> {
|
||||
const response = await apiPatch<ApiResponse<TeamMember>>(
|
||||
`/api/workspaces/${workspaceId}/teams/${teamId}/members/${userId}`,
|
||||
{ role }
|
||||
workspaceId?: string
|
||||
): Promise<void> {
|
||||
const resolvedWorkspaceId = resolveWorkspaceId(workspaceId);
|
||||
await apiDelete(
|
||||
`/api/workspaces/${resolvedWorkspaceId}/teams/${teamId}/members/${userId}`,
|
||||
resolvedWorkspaceId
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock teams for development (until backend endpoints are ready)
|
||||
*/
|
||||
export const mockTeams: Team[] = [
|
||||
{
|
||||
id: "team-1",
|
||||
workspaceId: "workspace-1",
|
||||
name: "Engineering",
|
||||
description: "Product development team",
|
||||
metadata: {},
|
||||
createdAt: new Date("2026-01-20"),
|
||||
updatedAt: new Date("2026-01-20"),
|
||||
},
|
||||
{
|
||||
id: "team-2",
|
||||
workspaceId: "workspace-1",
|
||||
name: "Design",
|
||||
description: "UI/UX design team",
|
||||
metadata: {},
|
||||
createdAt: new Date("2026-01-22"),
|
||||
updatedAt: new Date("2026-01-22"),
|
||||
},
|
||||
{
|
||||
id: "team-3",
|
||||
workspaceId: "workspace-1",
|
||||
name: "Marketing",
|
||||
description: null,
|
||||
metadata: {},
|
||||
createdAt: new Date("2026-01-25"),
|
||||
updatedAt: new Date("2026-01-25"),
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Mock team with members for development
|
||||
*/
|
||||
const baseTeam = mockTeams[0];
|
||||
if (!baseTeam) {
|
||||
throw new Error("Mock team not found");
|
||||
}
|
||||
export const mockTeamWithMembers: TeamWithMembers = {
|
||||
id: baseTeam.id,
|
||||
workspaceId: baseTeam.workspaceId,
|
||||
name: baseTeam.name,
|
||||
description: baseTeam.description,
|
||||
metadata: baseTeam.metadata,
|
||||
createdAt: baseTeam.createdAt,
|
||||
updatedAt: baseTeam.updatedAt,
|
||||
members: [
|
||||
{
|
||||
teamId: "team-1",
|
||||
userId: "user-1",
|
||||
role: TeamMemberRole.OWNER,
|
||||
joinedAt: new Date("2026-01-20"),
|
||||
user: {
|
||||
id: "user-1",
|
||||
email: "john@example.com",
|
||||
name: "John Doe",
|
||||
emailVerified: true,
|
||||
image: null,
|
||||
authProviderId: null,
|
||||
preferences: {},
|
||||
deactivatedAt: null,
|
||||
isLocalAuth: false,
|
||||
passwordHash: null,
|
||||
invitedBy: null,
|
||||
invitationToken: null,
|
||||
invitedAt: null,
|
||||
createdAt: new Date("2026-01-15"),
|
||||
updatedAt: new Date("2026-01-15"),
|
||||
},
|
||||
},
|
||||
{
|
||||
teamId: "team-1",
|
||||
userId: "user-2",
|
||||
role: TeamMemberRole.MEMBER,
|
||||
joinedAt: new Date("2026-01-21"),
|
||||
user: {
|
||||
id: "user-2",
|
||||
email: "jane@example.com",
|
||||
name: "Jane Smith",
|
||||
emailVerified: true,
|
||||
image: null,
|
||||
authProviderId: null,
|
||||
preferences: {},
|
||||
deactivatedAt: null,
|
||||
isLocalAuth: false,
|
||||
passwordHash: null,
|
||||
invitedBy: null,
|
||||
invitationToken: null,
|
||||
invitedAt: null,
|
||||
createdAt: new Date("2026-01-16"),
|
||||
updatedAt: new Date("2026-01-16"),
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user