Compare commits
1 Commits
feat/ms21-
...
feat/ms21-
| Author | SHA1 | Date | |
|---|---|---|---|
| de6aa9c768 |
@@ -109,6 +109,7 @@ export default function WorkspaceDetailPage({
|
|||||||
// TODO: Replace with actual role check when API is implemented
|
// TODO: Replace with actual role check when API is implemented
|
||||||
// Currently hardcoded to OWNER in mock data (line 89)
|
// Currently hardcoded to OWNER in mock data (line 89)
|
||||||
const canInvite =
|
const canInvite =
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||||
currentUserRole === WorkspaceMemberRole.OWNER || currentUserRole === WorkspaceMemberRole.ADMIN;
|
currentUserRole === WorkspaceMemberRole.OWNER || currentUserRole === WorkspaceMemberRole.ADMIN;
|
||||||
|
|
||||||
const handleUpdateWorkspace = async (name: string): Promise<void> => {
|
const handleUpdateWorkspace = async (name: string): Promise<void> => {
|
||||||
|
|||||||
@@ -1,333 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useState, type ReactElement, type SyntheticEvent } from "react";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/components/ui/select";
|
|
||||||
import type { InviteUserDto, WorkspaceMemberRole } from "@/lib/api/admin";
|
|
||||||
|
|
||||||
/* ---------------------------------------------------------------------------
|
|
||||||
Types
|
|
||||||
--------------------------------------------------------------------------- */
|
|
||||||
|
|
||||||
interface WorkspaceOption {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface InviteUserDialogProps {
|
|
||||||
open: boolean;
|
|
||||||
onOpenChange: (open: boolean) => void;
|
|
||||||
onSubmit: (data: InviteUserDto) => Promise<void>;
|
|
||||||
isSubmitting: boolean;
|
|
||||||
workspaces: WorkspaceOption[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------------------------------------------------------------------------
|
|
||||||
Validation
|
|
||||||
--------------------------------------------------------------------------- */
|
|
||||||
|
|
||||||
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
||||||
|
|
||||||
function validateEmail(value: string): string | null {
|
|
||||||
const trimmed = value.trim();
|
|
||||||
if (!trimmed) return "Email is required.";
|
|
||||||
if (!EMAIL_REGEX.test(trimmed)) return "Please enter a valid email address.";
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------------------------------------------------------------------------
|
|
||||||
Component
|
|
||||||
--------------------------------------------------------------------------- */
|
|
||||||
|
|
||||||
export function InviteUserDialog({
|
|
||||||
open,
|
|
||||||
onOpenChange,
|
|
||||||
onSubmit,
|
|
||||||
isSubmitting,
|
|
||||||
workspaces,
|
|
||||||
}: InviteUserDialogProps): ReactElement | null {
|
|
||||||
const [email, setEmail] = useState("");
|
|
||||||
const [name, setName] = useState("");
|
|
||||||
const [workspaceId, setWorkspaceId] = useState("");
|
|
||||||
const [role, setRole] = useState<WorkspaceMemberRole>("MEMBER");
|
|
||||||
const [formError, setFormError] = useState<string | null>(null);
|
|
||||||
|
|
||||||
function resetForm(): void {
|
|
||||||
setEmail("");
|
|
||||||
setName("");
|
|
||||||
setWorkspaceId("");
|
|
||||||
setRole("MEMBER");
|
|
||||||
setFormError(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleSubmit(e: SyntheticEvent): Promise<void> {
|
|
||||||
e.preventDefault();
|
|
||||||
setFormError(null);
|
|
||||||
|
|
||||||
const emailError = validateEmail(email);
|
|
||||||
if (emailError) {
|
|
||||||
setFormError(emailError);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const dto: InviteUserDto = {
|
|
||||||
email: email.trim(),
|
|
||||||
role,
|
|
||||||
};
|
|
||||||
const trimmedName = name.trim();
|
|
||||||
if (trimmedName) dto.name = trimmedName;
|
|
||||||
if (workspaceId) dto.workspaceId = workspaceId;
|
|
||||||
|
|
||||||
await onSubmit(dto);
|
|
||||||
resetForm();
|
|
||||||
} catch (err: unknown) {
|
|
||||||
setFormError(err instanceof Error ? err.message : "Failed to send invitation.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!open) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
role="dialog"
|
|
||||||
aria-modal="true"
|
|
||||||
aria-labelledby="invite-user-title"
|
|
||||||
style={{
|
|
||||||
position: "fixed",
|
|
||||||
inset: 0,
|
|
||||||
zIndex: 50,
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* Backdrop */}
|
|
||||||
<div
|
|
||||||
data-testid="invite-dialog-backdrop"
|
|
||||||
style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,0.5)" }}
|
|
||||||
onClick={() => {
|
|
||||||
if (!isSubmitting) {
|
|
||||||
resetForm();
|
|
||||||
onOpenChange(false);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Dialog */}
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: "relative",
|
|
||||||
background: "var(--surface, #fff)",
|
|
||||||
borderRadius: "8px",
|
|
||||||
border: "1px solid var(--border, #e5e7eb)",
|
|
||||||
padding: 24,
|
|
||||||
width: "100%",
|
|
||||||
maxWidth: 480,
|
|
||||||
zIndex: 1,
|
|
||||||
maxHeight: "90vh",
|
|
||||||
overflowY: "auto",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<h2
|
|
||||||
id="invite-user-title"
|
|
||||||
style={{
|
|
||||||
fontSize: "1.125rem",
|
|
||||||
fontWeight: 600,
|
|
||||||
color: "var(--text, #111)",
|
|
||||||
margin: "0 0 8px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Invite User
|
|
||||||
</h2>
|
|
||||||
<p style={{ color: "var(--muted, #6b7280)", fontSize: "0.875rem", margin: "0 0 16px" }}>
|
|
||||||
Send an invitation to join the platform.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<form
|
|
||||||
onSubmit={(e) => {
|
|
||||||
void handleSubmit(e);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* Email */}
|
|
||||||
<div style={{ marginBottom: 16 }}>
|
|
||||||
<label
|
|
||||||
htmlFor="invite-email"
|
|
||||||
style={{
|
|
||||||
display: "block",
|
|
||||||
marginBottom: 6,
|
|
||||||
fontSize: "0.85rem",
|
|
||||||
fontWeight: 500,
|
|
||||||
color: "var(--text-2, #374151)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Email <span style={{ color: "var(--danger, #ef4444)" }}>*</span>
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
id="invite-email"
|
|
||||||
type="email"
|
|
||||||
value={email}
|
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
setEmail(e.target.value);
|
|
||||||
}}
|
|
||||||
placeholder="user@example.com"
|
|
||||||
maxLength={254}
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Name */}
|
|
||||||
<div style={{ marginBottom: 16 }}>
|
|
||||||
<label
|
|
||||||
htmlFor="invite-name"
|
|
||||||
style={{
|
|
||||||
display: "block",
|
|
||||||
marginBottom: 6,
|
|
||||||
fontSize: "0.85rem",
|
|
||||||
fontWeight: 500,
|
|
||||||
color: "var(--text-2, #374151)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Name
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
id="invite-name"
|
|
||||||
type="text"
|
|
||||||
value={name}
|
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
setName(e.target.value);
|
|
||||||
}}
|
|
||||||
placeholder="Full name (optional)"
|
|
||||||
maxLength={255}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Workspace */}
|
|
||||||
{workspaces.length > 0 && (
|
|
||||||
<div style={{ marginBottom: 16 }}>
|
|
||||||
<label
|
|
||||||
htmlFor="invite-workspace"
|
|
||||||
style={{
|
|
||||||
display: "block",
|
|
||||||
marginBottom: 6,
|
|
||||||
fontSize: "0.85rem",
|
|
||||||
fontWeight: 500,
|
|
||||||
color: "var(--text-2, #374151)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Workspace
|
|
||||||
</label>
|
|
||||||
<Select
|
|
||||||
value={workspaceId}
|
|
||||||
onValueChange={(v) => {
|
|
||||||
setWorkspaceId(v);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SelectTrigger id="invite-workspace">
|
|
||||||
<SelectValue placeholder="Select a workspace (optional)" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{workspaces.map((ws) => (
|
|
||||||
<SelectItem key={ws.id} value={ws.id}>
|
|
||||||
{ws.name}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Role */}
|
|
||||||
<div style={{ marginBottom: 16 }}>
|
|
||||||
<label
|
|
||||||
htmlFor="invite-role"
|
|
||||||
style={{
|
|
||||||
display: "block",
|
|
||||||
marginBottom: 6,
|
|
||||||
fontSize: "0.85rem",
|
|
||||||
fontWeight: 500,
|
|
||||||
color: "var(--text-2, #374151)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Role
|
|
||||||
</label>
|
|
||||||
<Select
|
|
||||||
value={role}
|
|
||||||
onValueChange={(v) => {
|
|
||||||
setRole(v as WorkspaceMemberRole);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SelectTrigger id="invite-role">
|
|
||||||
<SelectValue placeholder="Select role" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="MEMBER">Member</SelectItem>
|
|
||||||
<SelectItem value="ADMIN">Admin</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Form error */}
|
|
||||||
{formError !== null && (
|
|
||||||
<p
|
|
||||||
role="alert"
|
|
||||||
style={{
|
|
||||||
color: "var(--danger, #ef4444)",
|
|
||||||
fontSize: "0.85rem",
|
|
||||||
margin: "0 0 12px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{formError}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Buttons */}
|
|
||||||
<div style={{ display: "flex", justifyContent: "flex-end", gap: 8, marginTop: 8 }}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
resetForm();
|
|
||||||
onOpenChange(false);
|
|
||||||
}}
|
|
||||||
disabled={isSubmitting}
|
|
||||||
style={{
|
|
||||||
padding: "8px 16px",
|
|
||||||
background: "transparent",
|
|
||||||
border: "1px solid var(--border, #d1d5db)",
|
|
||||||
borderRadius: "6px",
|
|
||||||
color: "var(--text-2, #374151)",
|
|
||||||
fontSize: "0.85rem",
|
|
||||||
cursor: "pointer",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={isSubmitting || !email.trim()}
|
|
||||||
style={{
|
|
||||||
padding: "8px 16px",
|
|
||||||
background: "var(--primary, #111827)",
|
|
||||||
border: "none",
|
|
||||||
borderRadius: "6px",
|
|
||||||
color: "#fff",
|
|
||||||
fontSize: "0.85rem",
|
|
||||||
fontWeight: 500,
|
|
||||||
cursor: isSubmitting || !email.trim() ? "not-allowed" : "pointer",
|
|
||||||
opacity: isSubmitting || !email.trim() ? 0.6 : 1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{isSubmitting ? "Sending..." : "Send Invitation"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-enum-comparison */
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import type { KnowledgeEntryWithTags } from "@mosaic/shared";
|
import type { KnowledgeEntryWithTags } from "@mosaic/shared";
|
||||||
import { EntryStatus } from "@mosaic/shared";
|
import { EntryStatus } from "@mosaic/shared";
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-enum-comparison */
|
||||||
"use client";
|
"use client";
|
||||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||||
|
|
||||||
|
|||||||
@@ -137,7 +137,8 @@ describe("TaskItem", (): void => {
|
|||||||
dueDate: null,
|
dueDate: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
render(<TaskItem task={taskWithoutDueDate} />);
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
render(<TaskItem task={taskWithoutDueDate as any} />);
|
||||||
expect(screen.getByText("Test task")).toBeInTheDocument();
|
expect(screen.getByText("Test task")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import type { Task } from "@mosaic/shared";
|
import type { Task } from "@mosaic/shared";
|
||||||
import { TaskStatus, TaskPriority } from "@mosaic/shared";
|
import { TaskStatus, TaskPriority } from "@mosaic/shared";
|
||||||
|
|||||||
@@ -1,97 +0,0 @@
|
|||||||
/**
|
|
||||||
* Admin API Client
|
|
||||||
* Handles admin-scoped user and workspace management requests.
|
|
||||||
* These endpoints require AdminGuard (OWNER/ADMIN role).
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { apiGet, apiPost, apiPatch, apiDelete } from "./client";
|
|
||||||
|
|
||||||
/* ---------------------------------------------------------------------------
|
|
||||||
Response Types (mirrors apps/api/src/admin/types/admin.types.ts)
|
|
||||||
--------------------------------------------------------------------------- */
|
|
||||||
|
|
||||||
export type WorkspaceMemberRole = "OWNER" | "ADMIN" | "MEMBER" | "GUEST";
|
|
||||||
|
|
||||||
export interface WorkspaceMembershipResponse {
|
|
||||||
workspaceId: string;
|
|
||||||
workspaceName: string;
|
|
||||||
role: WorkspaceMemberRole;
|
|
||||||
joinedAt: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AdminUserResponse {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
email: string;
|
|
||||||
emailVerified: boolean;
|
|
||||||
image: string | null;
|
|
||||||
createdAt: string;
|
|
||||||
deactivatedAt: string | null;
|
|
||||||
isLocalAuth: boolean;
|
|
||||||
invitedAt: string | null;
|
|
||||||
invitedBy: string | null;
|
|
||||||
workspaceMemberships: WorkspaceMembershipResponse[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PaginatedAdminResponse<T> {
|
|
||||||
data: T[];
|
|
||||||
meta: {
|
|
||||||
total: number;
|
|
||||||
page: number;
|
|
||||||
limit: number;
|
|
||||||
totalPages: number;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface InvitationResponse {
|
|
||||||
userId: string;
|
|
||||||
invitationToken: string;
|
|
||||||
email: string;
|
|
||||||
invitedAt: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------------------------------------------------------------------------
|
|
||||||
Request DTOs
|
|
||||||
--------------------------------------------------------------------------- */
|
|
||||||
|
|
||||||
export interface InviteUserDto {
|
|
||||||
email: string;
|
|
||||||
name?: string;
|
|
||||||
workspaceId?: string;
|
|
||||||
role?: WorkspaceMemberRole;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UpdateUserDto {
|
|
||||||
name?: string;
|
|
||||||
deactivatedAt?: string | null;
|
|
||||||
emailVerified?: boolean;
|
|
||||||
preferences?: Record<string, unknown>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------------------------------------------------------------------------
|
|
||||||
API Functions
|
|
||||||
--------------------------------------------------------------------------- */
|
|
||||||
|
|
||||||
export async function fetchAdminUsers(
|
|
||||||
page = 1,
|
|
||||||
limit = 50
|
|
||||||
): Promise<PaginatedAdminResponse<AdminUserResponse>> {
|
|
||||||
return apiGet<PaginatedAdminResponse<AdminUserResponse>>(
|
|
||||||
`/api/admin/users?page=${String(page)}&limit=${String(limit)}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function inviteUser(dto: InviteUserDto): Promise<InvitationResponse> {
|
|
||||||
return apiPost<InvitationResponse>("/api/admin/users/invite", dto);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function updateUser(
|
|
||||||
userId: string,
|
|
||||||
dto: UpdateUserDto
|
|
||||||
): Promise<AdminUserResponse> {
|
|
||||||
return apiPatch<AdminUserResponse>(`/api/admin/users/${userId}`, dto);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deactivateUser(userId: string): Promise<void> {
|
|
||||||
await apiDelete(`/api/admin/users/${userId}`);
|
|
||||||
}
|
|
||||||
@@ -1,14 +1,29 @@
|
|||||||
/**
|
import type {
|
||||||
* Teams API Client
|
Team,
|
||||||
* Handles team-related API requests
|
TeamMember,
|
||||||
*/
|
User,
|
||||||
|
WorkspaceMemberRole,
|
||||||
import type { Team, TeamMember, User } from "@mosaic/shared";
|
} from "@mosaic/shared";
|
||||||
import { TeamMemberRole } from "@mosaic/shared";
|
import { TeamMemberRole } from "@mosaic/shared";
|
||||||
import { apiGet, apiPost, apiPatch, apiDelete, type ApiResponse } from "./client";
|
import { apiDelete, apiGet, apiPost, type ApiResponse } from "./client";
|
||||||
|
|
||||||
|
export interface TeamMemberWithUser extends TeamMember {
|
||||||
|
user: Pick<User, "id" | "name" | "email" | "image">;
|
||||||
|
}
|
||||||
|
|
||||||
export interface TeamWithMembers extends Team {
|
export interface TeamWithMembers extends Team {
|
||||||
members: (TeamMember & { user: User })[];
|
members?: TeamMemberWithUser[];
|
||||||
|
_count?: {
|
||||||
|
members: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WorkspaceMemberWithUser {
|
||||||
|
workspaceId: string;
|
||||||
|
userId: string;
|
||||||
|
role: WorkspaceMemberRole;
|
||||||
|
joinedAt: string | Date;
|
||||||
|
user: Pick<User, "id" | "name" | "email" | "image">;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateTeamDto {
|
export interface CreateTeamDto {
|
||||||
@@ -16,108 +31,81 @@ export interface CreateTeamDto {
|
|||||||
description?: string;
|
description?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateTeamDto {
|
|
||||||
name?: string;
|
|
||||||
description?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AddTeamMemberDto {
|
export interface AddTeamMemberDto {
|
||||||
userId: string;
|
userId: string;
|
||||||
role?: TeamMemberRole;
|
role?: TeamMemberRole;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
type ApiPayload<T> = T | ApiResponse<T>;
|
||||||
* Fetch all teams for a workspace
|
|
||||||
*/
|
function isApiResponse<T>(payload: ApiPayload<T>): payload is ApiResponse<T> {
|
||||||
export async function fetchTeams(workspaceId: string): Promise<Team[]> {
|
return typeof payload === "object" && payload !== null && "data" in payload;
|
||||||
const response = await apiGet<ApiResponse<Team[]>>(`/api/workspaces/${workspaceId}/teams`);
|
|
||||||
return response.data;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function unwrapPayload<T>(payload: ApiPayload<T>): T {
|
||||||
* Fetch a single team with members
|
return isApiResponse(payload) ? payload.data : payload;
|
||||||
*/
|
}
|
||||||
export async function fetchTeam(workspaceId: string, teamId: string): Promise<TeamWithMembers> {
|
|
||||||
const response = await apiGet<ApiResponse<TeamWithMembers>>(
|
export function getTeamMemberCount(team: TeamWithMembers): number {
|
||||||
`/api/workspaces/${workspaceId}/teams/${teamId}`
|
if (Array.isArray(team.members)) {
|
||||||
|
return team.members.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return team._count?.members ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchTeams(workspaceId: string): Promise<TeamWithMembers[]> {
|
||||||
|
const payload = await apiGet<ApiPayload<TeamWithMembers[]>>(
|
||||||
|
`/api/workspaces/${workspaceId}/teams`,
|
||||||
|
workspaceId
|
||||||
);
|
);
|
||||||
return response.data;
|
return unwrapPayload(payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export async function createTeam(workspaceId: string, data: CreateTeamDto): Promise<TeamWithMembers> {
|
||||||
* Create a new team
|
const payload = await apiPost<ApiPayload<TeamWithMembers>>(
|
||||||
*/
|
`/api/workspaces/${workspaceId}/teams`,
|
||||||
export async function createTeam(workspaceId: string, data: CreateTeamDto): Promise<Team> {
|
data,
|
||||||
const response = await apiPost<ApiResponse<Team>>(`/api/workspaces/${workspaceId}/teams`, data);
|
workspaceId
|
||||||
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;
|
return unwrapPayload(payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete a team
|
|
||||||
*/
|
|
||||||
export async function deleteTeam(workspaceId: string, teamId: string): Promise<void> {
|
export async function deleteTeam(workspaceId: string, teamId: string): Promise<void> {
|
||||||
await apiDelete(`/api/workspaces/${workspaceId}/teams/${teamId}`);
|
await apiDelete<void>(`/api/workspaces/${workspaceId}/teams/${teamId}`, workspaceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a member to a team
|
|
||||||
*/
|
|
||||||
export async function addTeamMember(
|
export async function addTeamMember(
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
teamId: string,
|
teamId: string,
|
||||||
data: AddTeamMemberDto
|
data: AddTeamMemberDto
|
||||||
): Promise<TeamMember> {
|
): Promise<TeamMemberWithUser> {
|
||||||
const response = await apiPost<ApiResponse<TeamMember>>(
|
const payload = await apiPost<ApiPayload<TeamMemberWithUser>>(
|
||||||
`/api/workspaces/${workspaceId}/teams/${teamId}/members`,
|
`/api/workspaces/${workspaceId}/teams/${teamId}/members`,
|
||||||
data
|
data,
|
||||||
|
workspaceId
|
||||||
);
|
);
|
||||||
return response.data;
|
return unwrapPayload(payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove a member from a team
|
|
||||||
*/
|
|
||||||
export async function removeTeamMember(
|
export async function removeTeamMember(
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
teamId: string,
|
teamId: string,
|
||||||
userId: string
|
userId: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await apiDelete(`/api/workspaces/${workspaceId}/teams/${teamId}/members/${userId}`);
|
await apiDelete<void>(`/api/workspaces/${workspaceId}/teams/${teamId}/members/${userId}`, workspaceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export async function fetchWorkspaceMembers(workspaceId: string): Promise<WorkspaceMemberWithUser[]> {
|
||||||
* Update a team member's role
|
const payload = await apiGet<ApiPayload<WorkspaceMemberWithUser[]>>(
|
||||||
*/
|
`/api/workspaces/${workspaceId}/members`,
|
||||||
export async function updateTeamMemberRole(
|
workspaceId
|
||||||
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;
|
return unwrapPayload(payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mock teams for development (until backend endpoints are ready)
|
* Mock teams for development in legacy routes under /app/settings.
|
||||||
*/
|
*/
|
||||||
export const mockTeams: Team[] = [
|
export const mockTeams: Team[] = [
|
||||||
{
|
{
|
||||||
@@ -133,7 +121,7 @@ export const mockTeams: Team[] = [
|
|||||||
id: "team-2",
|
id: "team-2",
|
||||||
workspaceId: "workspace-1",
|
workspaceId: "workspace-1",
|
||||||
name: "Design",
|
name: "Design",
|
||||||
description: "UI/UX design team",
|
description: "UI and UX design team",
|
||||||
metadata: {},
|
metadata: {},
|
||||||
createdAt: new Date("2026-01-22"),
|
createdAt: new Date("2026-01-22"),
|
||||||
updatedAt: new Date("2026-01-22"),
|
updatedAt: new Date("2026-01-22"),
|
||||||
@@ -149,24 +137,16 @@ export const mockTeams: Team[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
const [defaultMockTeam] = mockTeams;
|
||||||
* Mock team with members for development
|
if (!defaultMockTeam) {
|
||||||
*/
|
throw new Error("Mock team was not found");
|
||||||
const baseTeam = mockTeams[0];
|
|
||||||
if (!baseTeam) {
|
|
||||||
throw new Error("Mock team not found");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const mockTeamWithMembers: TeamWithMembers = {
|
export const mockTeamWithMembers: TeamWithMembers = {
|
||||||
id: baseTeam.id,
|
...defaultMockTeam,
|
||||||
workspaceId: baseTeam.workspaceId,
|
|
||||||
name: baseTeam.name,
|
|
||||||
description: baseTeam.description,
|
|
||||||
metadata: baseTeam.metadata,
|
|
||||||
createdAt: baseTeam.createdAt,
|
|
||||||
updatedAt: baseTeam.updatedAt,
|
|
||||||
members: [
|
members: [
|
||||||
{
|
{
|
||||||
teamId: "team-1",
|
teamId: defaultMockTeam.id,
|
||||||
userId: "user-1",
|
userId: "user-1",
|
||||||
role: TeamMemberRole.OWNER,
|
role: TeamMemberRole.OWNER,
|
||||||
joinedAt: new Date("2026-01-20"),
|
joinedAt: new Date("2026-01-20"),
|
||||||
@@ -174,22 +154,11 @@ export const mockTeamWithMembers: TeamWithMembers = {
|
|||||||
id: "user-1",
|
id: "user-1",
|
||||||
email: "john@example.com",
|
email: "john@example.com",
|
||||||
name: "John Doe",
|
name: "John Doe",
|
||||||
emailVerified: true,
|
|
||||||
image: null,
|
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",
|
teamId: defaultMockTeam.id,
|
||||||
userId: "user-2",
|
userId: "user-2",
|
||||||
role: TeamMemberRole.MEMBER,
|
role: TeamMemberRole.MEMBER,
|
||||||
joinedAt: new Date("2026-01-21"),
|
joinedAt: new Date("2026-01-21"),
|
||||||
@@ -197,18 +166,7 @@ export const mockTeamWithMembers: TeamWithMembers = {
|
|||||||
id: "user-2",
|
id: "user-2",
|
||||||
email: "jane@example.com",
|
email: "jane@example.com",
|
||||||
name: "Jane Smith",
|
name: "Jane Smith",
|
||||||
emailVerified: true,
|
|
||||||
image: null,
|
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"),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ export function useLayout(): UseLayoutReturn {
|
|||||||
if (stored) {
|
if (stored) {
|
||||||
const emptyFallback: Record<string, LayoutConfig> = {};
|
const emptyFallback: Record<string, LayoutConfig> = {};
|
||||||
const parsed = safeJsonParse(stored, isLayoutConfigRecord, emptyFallback);
|
const parsed = safeJsonParse(stored, isLayoutConfigRecord, emptyFallback);
|
||||||
const parsedLayouts = parsed;
|
const parsedLayouts = parsed as Record<string, LayoutConfig>;
|
||||||
if (Object.keys(parsedLayouts).length > 0) {
|
if (Object.keys(parsedLayouts).length > 0) {
|
||||||
setLayouts(parsedLayouts);
|
setLayouts(parsedLayouts);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user