From cd0f9a9d623f4efc6f66651578ef7da5d0e52168 Mon Sep 17 00:00:00 2001 From: Markov Date: Tue, 24 Feb 2026 23:19:57 +0100 Subject: [PATCH] Add project members management UI in ProjectSettings --- src/components/ProjectSettings.tsx | 139 +++++++++++++++++++++++++++-- src/lib/api.ts | 50 +++++++++++ 2 files changed, 184 insertions(+), 5 deletions(-) diff --git a/src/components/ProjectSettings.tsx b/src/components/ProjectSettings.tsx index f75899b..d1eb6ff 100644 --- a/src/components/ProjectSettings.tsx +++ b/src/components/ProjectSettings.tsx @@ -1,8 +1,14 @@ - -import { useState } from "react"; +import { useState, useEffect } from "react"; import { useNavigate } from "react-router-dom"; -import type { Project } from "@/lib/api"; -import { updateProject, deleteProject } from "@/lib/api"; +import type { Project, ProjectMember, Member } from "@/lib/api"; +import { + updateProject, + deleteProject, + getProjectMembers, + addProjectMember, + removeProjectMember, + getMembers +} from "@/lib/api"; interface Props { project: Project; @@ -19,6 +25,65 @@ export default function ProjectSettings({ project, onUpdated }: Props) { const [saved, setSaved] = useState(false); const [confirmDelete, setConfirmDelete] = useState(false); + // Members state + const [members, setMembers] = useState([]); + const [allMembers, setAllMembers] = useState([]); + const [loadingMembers, setLoadingMembers] = useState(true); + const [selectedMember, setSelectedMember] = useState(""); + + // Load project members and all members + useEffect(() => { + const loadMembers = async () => { + try { + const [projectMembers, allMembersList] = await Promise.all([ + getProjectMembers(project.slug), + getMembers() + ]); + setMembers(projectMembers); + setAllMembers(allMembersList); + } catch (e) { + console.error("Failed to load members:", e); + } finally { + setLoadingMembers(false); + } + }; + loadMembers(); + }, [project.slug]); + + // Get available members for adding (not already in project) + const availableMembers = allMembers.filter( + (member) => !members.some((pm) => pm.slug === member.slug) + ); + + const handleAddMember = async () => { + if (!selectedMember) return; + try { + await addProjectMember(project.slug, selectedMember); + const newMember = allMembers.find((m) => m.slug === selectedMember); + if (newMember) { + setMembers([...members, { + id: newMember.id, + name: newMember.name, + slug: newMember.slug, + type: newMember.type, + role: "member" + }]); + setSelectedMember(""); + } + } catch (e) { + console.error("Failed to add member:", e); + } + }; + + const handleRemoveMember = async (memberSlug: string) => { + try { + await removeProjectMember(project.slug, memberSlug); + setMembers(members.filter((m) => m.slug !== memberSlug)); + } catch (e) { + console.error("Failed to remove member:", e); + } + }; + const handleSave = async () => { setSaving(true); try { @@ -107,6 +172,70 @@ export default function ProjectSettings({ project, onUpdated }: Props) { {saved && Сохранено ✓} + {/* Members Section */} +
+
Участники
+ + {loadingMembers ? ( +
Загрузка...
+ ) : ( +
+ {/* Current Members */} +
+ {members.map((member) => ( +
+
+
+ {member.type === "agent" ? "A" : "H"} +
+
+
{member.name}
+
{member.slug} • {member.role}
+
+
+ {member.role !== "owner" && ( + + )} +
+ ))} +
+ + {/* Add Member */} + {availableMembers.length > 0 && ( +
+ + +
+ )} +
+ )} +
+ + {/* Danger Zone */}
Зона опасности
{confirmDelete ? ( @@ -136,4 +265,4 @@ export default function ProjectSettings({ project, onUpdated }: Props) {
); -} +} \ No newline at end of file diff --git a/src/lib/api.ts b/src/lib/api.ts index 5e82295..520cc90 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -79,6 +79,14 @@ export interface Project { chat_id: string | null; } +export interface ProjectMember { + id: string; + name: string; + slug: string; + type: "human" | "agent"; + role: string; // "owner" | "member" +} + export interface Step { id: string; title: string; @@ -107,6 +115,14 @@ export interface Task { steps: Step[]; } +export interface ProjectMember { + id: string; + name: string; + slug: string; + type: "human" | "agent"; + role: "owner" | "member"; +} + export interface Attachment { id: string; filename: string; @@ -288,4 +304,38 @@ export async function regenerateToken(slug: string): Promise<{ token: string }> export async function revokeToken(slug: string): Promise { await request(`/api/v1/members/${slug}/revoke-token`, { method: "POST" }); +} + +// --- Project Members --- + +export async function getProjectMembers(slug: string): Promise { + return request(`/api/v1/projects/${slug}/members`); +} + +export async function addProjectMember(slug: string, memberSlug: string): Promise { + await request(`/api/v1/projects/${slug}/members`, { + method: "POST", + body: JSON.stringify({ slug: memberSlug }), + }); +} + +export async function removeProjectMember(slug: string, memberSlug: string): Promise { + await request(`/api/v1/projects/${slug}/members/${memberSlug}`, { method: "DELETE" }); +} + +// --- Project Members --- + +export async function getProjectMembers(slug: string): Promise { + return request(`/api/v1/projects/${slug}/members`); +} + +export async function addProjectMember(slug: string, memberSlug: string): Promise<{ ok: boolean }> { + return request(`/api/v1/projects/${slug}/members`, { + method: "POST", + body: JSON.stringify({ slug: memberSlug }) + }); +} + +export async function removeProjectMember(slug: string, memberSlug: string): Promise<{ ok: boolean }> { + return request(`/api/v1/projects/${slug}/members/${memberSlug}`, { method: "DELETE" }); } \ No newline at end of file