feat(web): add workspace management UI (M2 #12)

- Create workspace listing page at /settings/workspaces
  - List all user workspaces with role badges
  - Create new workspace functionality
  - Display member count per workspace

- Create workspace detail page at /settings/workspaces/[id]
  - Workspace settings (name, ID, created date)
  - Member management with role editing
  - Invite member functionality
  - Delete workspace (owner only)

- Add workspace components:
  - WorkspaceCard: Display workspace info with role badge
  - WorkspaceSettings: Edit workspace settings and delete
  - MemberList: Display and manage workspace members
  - InviteMember: Send invitations with role selection

- Add WorkspaceMemberWithUser type to shared package
- Follow existing app patterns for styling and structure
- Use mock data (ready for API integration)
This commit is contained in:
Jason Woltje
2026-01-29 16:59:26 -06:00
parent 287a0e2556
commit 5291fece26
43 changed files with 4152 additions and 99 deletions

View File

@@ -0,0 +1,171 @@
/**
* Knowledge API Client
* Handles knowledge entry-related API requests
*/
import type { KnowledgeEntryWithTags, KnowledgeTag } from "@mosaic/shared";
import { EntryStatus } from "@mosaic/shared";
import { apiGet, type ApiResponse } from "./client";
export interface EntryFilters {
status?: EntryStatus;
tag?: string;
page?: number;
limit?: number;
search?: string;
sortBy?: "updatedAt" | "createdAt" | "title";
sortOrder?: "asc" | "desc";
}
export interface EntriesResponse {
data: KnowledgeEntryWithTags[];
meta?: {
total?: number;
page?: number;
limit?: number;
};
}
/**
* Fetch knowledge entries with optional filters
*/
export async function fetchEntries(filters?: EntryFilters): Promise<EntriesResponse> {
const params = new URLSearchParams();
if (filters?.status) {
params.append("status", filters.status);
}
if (filters?.tag) {
params.append("tag", filters.tag);
}
if (filters?.page) {
params.append("page", filters.page.toString());
}
if (filters?.limit) {
params.append("limit", filters.limit.toString());
}
const queryString = params.toString();
const endpoint = queryString ? `/api/knowledge/entries?${queryString}` : "/api/knowledge/entries";
const response = await apiGet<EntriesResponse>(endpoint);
return response;
}
/**
* Fetch all knowledge tags
*/
export async function fetchTags(): Promise<KnowledgeTag[]> {
const response = await apiGet<ApiResponse<KnowledgeTag[]>>("/api/knowledge/tags");
return response.data;
}
/**
* Mock entries for development (until backend endpoints are ready)
*/
export const mockEntries: KnowledgeEntryWithTags[] = [
{
id: "entry-1",
workspaceId: "workspace-1",
slug: "getting-started",
title: "Getting Started with Mosaic Stack",
content: "# Getting Started\n\nWelcome to Mosaic Stack...",
contentHtml: "<h1>Getting Started</h1><p>Welcome to Mosaic Stack...</p>",
summary: "A comprehensive guide to getting started with the Mosaic Stack platform.",
status: EntryStatus.PUBLISHED,
visibility: "PUBLIC" as const,
createdBy: "user-1",
updatedBy: "user-1",
createdAt: new Date("2026-01-20"),
updatedAt: new Date("2026-01-28"),
tags: [
{ id: "tag-1", workspaceId: "workspace-1", name: "Tutorial", slug: "tutorial", color: "#3B82F6", createdAt: new Date(), updatedAt: new Date() },
{ id: "tag-2", workspaceId: "workspace-1", name: "Onboarding", slug: "onboarding", color: "#10B981", createdAt: new Date(), updatedAt: new Date() },
],
},
{
id: "entry-2",
workspaceId: "workspace-1",
slug: "architecture-overview",
title: "Architecture Overview",
content: "# Architecture\n\nThe Mosaic Stack architecture...",
contentHtml: "<h1>Architecture</h1><p>The Mosaic Stack architecture...</p>",
summary: "Overview of the system architecture and design patterns used in Mosaic Stack.",
status: EntryStatus.PUBLISHED,
visibility: "WORKSPACE" as const,
createdBy: "user-1",
updatedBy: "user-1",
createdAt: new Date("2026-01-15"),
updatedAt: new Date("2026-01-27"),
tags: [
{ id: "tag-3", workspaceId: "workspace-1", name: "Architecture", slug: "architecture", color: "#8B5CF6", createdAt: new Date(), updatedAt: new Date() },
{ id: "tag-4", workspaceId: "workspace-1", name: "Technical", slug: "technical", color: "#F59E0B", createdAt: new Date(), updatedAt: new Date() },
],
},
{
id: "entry-3",
workspaceId: "workspace-1",
slug: "api-documentation-draft",
title: "API Documentation (Draft)",
content: "# API Docs\n\nWork in progress...",
contentHtml: "<h1>API Docs</h1><p>Work in progress...</p>",
summary: "Comprehensive API documentation for developers.",
status: EntryStatus.DRAFT,
visibility: "PRIVATE" as const,
createdBy: "user-1",
updatedBy: "user-1",
createdAt: new Date("2026-01-29"),
updatedAt: new Date("2026-01-29"),
tags: [
{ id: "tag-4", workspaceId: "workspace-1", name: "Technical", slug: "technical", color: "#F59E0B", createdAt: new Date(), updatedAt: new Date() },
{ id: "tag-5", workspaceId: "workspace-1", name: "API", slug: "api", color: "#EF4444", createdAt: new Date(), updatedAt: new Date() },
],
},
{
id: "entry-4",
workspaceId: "workspace-1",
slug: "deployment-guide",
title: "Deployment Guide",
content: "# Deployment\n\nHow to deploy Mosaic Stack...",
contentHtml: "<h1>Deployment</h1><p>How to deploy Mosaic Stack...</p>",
summary: "Step-by-step guide for deploying Mosaic Stack to production.",
status: EntryStatus.PUBLISHED,
visibility: "WORKSPACE" as const,
createdBy: "user-1",
updatedBy: "user-1",
createdAt: new Date("2026-01-18"),
updatedAt: new Date("2026-01-25"),
tags: [
{ id: "tag-6", workspaceId: "workspace-1", name: "DevOps", slug: "devops", color: "#14B8A6", createdAt: new Date(), updatedAt: new Date() },
{ id: "tag-1", workspaceId: "workspace-1", name: "Tutorial", slug: "tutorial", color: "#3B82F6", createdAt: new Date(), updatedAt: new Date() },
],
},
{
id: "entry-5",
workspaceId: "workspace-1",
slug: "old-meeting-notes",
title: "Q4 2025 Meeting Notes",
content: "# Meeting Notes\n\nOld archived notes...",
contentHtml: "<h1>Meeting Notes</h1><p>Old archived notes...</p>",
summary: "Meeting notes from Q4 2025 - archived for reference.",
status: EntryStatus.ARCHIVED,
visibility: "PRIVATE" as const,
createdBy: "user-1",
updatedBy: "user-1",
createdAt: new Date("2025-12-15"),
updatedAt: new Date("2026-01-05"),
tags: [
{ id: "tag-7", workspaceId: "workspace-1", name: "Meetings", slug: "meetings", color: "#6B7280", createdAt: new Date(), updatedAt: new Date() },
],
},
];
export const mockTags: KnowledgeTag[] = [
{ id: "tag-1", workspaceId: "workspace-1", name: "Tutorial", slug: "tutorial", color: "#3B82F6", createdAt: new Date(), updatedAt: new Date() },
{ id: "tag-2", workspaceId: "workspace-1", name: "Onboarding", slug: "onboarding", color: "#10B981", createdAt: new Date(), updatedAt: new Date() },
{ id: "tag-3", workspaceId: "workspace-1", name: "Architecture", slug: "architecture", color: "#8B5CF6", createdAt: new Date(), updatedAt: new Date() },
{ id: "tag-4", workspaceId: "workspace-1", name: "Technical", slug: "technical", color: "#F59E0B", createdAt: new Date(), updatedAt: new Date() },
{ id: "tag-5", workspaceId: "workspace-1", name: "API", slug: "api", color: "#EF4444", createdAt: new Date(), updatedAt: new Date() },
{ id: "tag-6", workspaceId: "workspace-1", name: "DevOps", slug: "devops", color: "#14B8A6", createdAt: new Date(), updatedAt: new Date() },
{ id: "tag-7", workspaceId: "workspace-1", name: "Meetings", slug: "meetings", color: "#6B7280", createdAt: new Date(), updatedAt: new Date() },
];

View File

@@ -0,0 +1,196 @@
/**
* Teams API Client
* Handles team-related API requests
*/
import type { Team, TeamMember, User } from "@mosaic/shared";
import { TeamMemberRole } from "@mosaic/shared";
import { apiGet, apiPost, apiPatch, apiDelete, type ApiResponse } from "./client";
export interface TeamWithMembers extends Team {
members: (TeamMember & { user: User })[];
}
export interface CreateTeamDto {
name: string;
description?: string;
}
export interface UpdateTeamDto {
name?: string;
description?: string;
}
export interface AddTeamMemberDto {
userId: string;
role?: TeamMemberRole;
}
/**
* Fetch all teams for a workspace
*/
export async function fetchTeams(workspaceId: string): Promise<Team[]> {
const response = await apiGet<ApiResponse<Team[]>>(`/api/workspaces/${workspaceId}/teams`);
return response.data;
}
/**
* Fetch a single team with members
*/
export async function fetchTeam(workspaceId: string, teamId: string): Promise<TeamWithMembers> {
const response = await apiGet<ApiResponse<TeamWithMembers>>(
`/api/workspaces/${workspaceId}/teams/${teamId}`
);
return response.data;
}
/**
* Create a new team
*/
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,
teamId: string,
data: UpdateTeamDto
): Promise<Team> {
const response = await apiPatch<ApiResponse<Team>>(
`/api/workspaces/${workspaceId}/teams/${teamId}`,
data
);
return response.data;
}
/**
* Delete a team
*/
export async function deleteTeam(workspaceId: string, teamId: string): Promise<void> {
await apiDelete(`/api/workspaces/${workspaceId}/teams/${teamId}`);
}
/**
* Add a member to a team
*/
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
);
return response.data;
}
/**
* Remove a member from a team
*/
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 }
);
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
*/
export const mockTeamWithMembers: TeamWithMembers = {
...mockTeams[0],
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: {},
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: {},
createdAt: new Date("2026-01-16"),
updatedAt: new Date("2026-01-16"),
},
},
],
};