Add project members management UI in ProjectSettings
This commit is contained in:
parent
66c14d74c0
commit
cd0f9a9d62
@ -1,8 +1,14 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
import { useState } from "react";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import type { Project } from "@/lib/api";
|
import type { Project, ProjectMember, Member } from "@/lib/api";
|
||||||
import { updateProject, deleteProject } from "@/lib/api";
|
import {
|
||||||
|
updateProject,
|
||||||
|
deleteProject,
|
||||||
|
getProjectMembers,
|
||||||
|
addProjectMember,
|
||||||
|
removeProjectMember,
|
||||||
|
getMembers
|
||||||
|
} from "@/lib/api";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
project: Project;
|
project: Project;
|
||||||
@ -19,6 +25,65 @@ export default function ProjectSettings({ project, onUpdated }: Props) {
|
|||||||
const [saved, setSaved] = useState(false);
|
const [saved, setSaved] = useState(false);
|
||||||
const [confirmDelete, setConfirmDelete] = useState(false);
|
const [confirmDelete, setConfirmDelete] = useState(false);
|
||||||
|
|
||||||
|
// Members state
|
||||||
|
const [members, setMembers] = useState<ProjectMember[]>([]);
|
||||||
|
const [allMembers, setAllMembers] = useState<Member[]>([]);
|
||||||
|
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 () => {
|
const handleSave = async () => {
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
try {
|
try {
|
||||||
@ -107,6 +172,70 @@ export default function ProjectSettings({ project, onUpdated }: Props) {
|
|||||||
{saved && <span className="text-sm text-green-400">Сохранено ✓</span>}
|
{saved && <span className="text-sm text-green-400">Сохранено ✓</span>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Members Section */}
|
||||||
|
<div className="pt-6 border-t border-[var(--border)]">
|
||||||
|
<div className="text-sm font-medium mb-3">Участники</div>
|
||||||
|
|
||||||
|
{loadingMembers ? (
|
||||||
|
<div className="text-sm text-[var(--muted)]">Загрузка...</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{/* Current Members */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
{members.map((member) => (
|
||||||
|
<div key={member.slug} className="flex items-center justify-between p-2 bg-[var(--bg)] border border-[var(--border)] rounded-lg">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className={`w-6 h-6 rounded-full flex items-center justify-center text-xs font-medium ${
|
||||||
|
member.type === "agent" ? "bg-blue-500/20 text-blue-400" : "bg-green-500/20 text-green-400"
|
||||||
|
}`}>
|
||||||
|
{member.type === "agent" ? "A" : "H"}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-medium">{member.name}</div>
|
||||||
|
<div className="text-xs text-[var(--muted)]">{member.slug} • {member.role}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{member.role !== "owner" && (
|
||||||
|
<button
|
||||||
|
onClick={() => handleRemoveMember(member.slug)}
|
||||||
|
className="px-2 py-1 text-xs text-red-400 hover:bg-red-500/10 rounded"
|
||||||
|
>
|
||||||
|
Убрать
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Add Member */}
|
||||||
|
{availableMembers.length > 0 && (
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<select
|
||||||
|
value={selectedMember}
|
||||||
|
onChange={(e) => setSelectedMember(e.target.value)}
|
||||||
|
className="flex-1 px-3 py-2 bg-[var(--bg)] border border-[var(--border)] rounded-lg outline-none focus:border-[var(--accent)] text-sm"
|
||||||
|
>
|
||||||
|
<option value="">Добавить участника...</option>
|
||||||
|
{availableMembers.map((member) => (
|
||||||
|
<option key={member.slug} value={member.slug}>
|
||||||
|
{member.name} ({member.slug}) - {member.type}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<button
|
||||||
|
onClick={handleAddMember}
|
||||||
|
disabled={!selectedMember}
|
||||||
|
className="px-3 py-2 bg-[var(--accent)] text-white rounded-lg text-sm hover:opacity-90 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
Добавить
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Danger Zone */}
|
||||||
<div className="pt-6 border-t border-[var(--border)]">
|
<div className="pt-6 border-t border-[var(--border)]">
|
||||||
<div className="text-sm text-[var(--muted)] mb-2">Зона опасности</div>
|
<div className="text-sm text-[var(--muted)] mb-2">Зона опасности</div>
|
||||||
{confirmDelete ? (
|
{confirmDelete ? (
|
||||||
@ -136,4 +265,4 @@ export default function ProjectSettings({ project, onUpdated }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -79,6 +79,14 @@ export interface Project {
|
|||||||
chat_id: string | null;
|
chat_id: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ProjectMember {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
type: "human" | "agent";
|
||||||
|
role: string; // "owner" | "member"
|
||||||
|
}
|
||||||
|
|
||||||
export interface Step {
|
export interface Step {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
@ -107,6 +115,14 @@ export interface Task {
|
|||||||
steps: Step[];
|
steps: Step[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ProjectMember {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
type: "human" | "agent";
|
||||||
|
role: "owner" | "member";
|
||||||
|
}
|
||||||
|
|
||||||
export interface Attachment {
|
export interface Attachment {
|
||||||
id: string;
|
id: string;
|
||||||
filename: string;
|
filename: string;
|
||||||
@ -288,4 +304,38 @@ export async function regenerateToken(slug: string): Promise<{ token: string }>
|
|||||||
|
|
||||||
export async function revokeToken(slug: string): Promise<void> {
|
export async function revokeToken(slug: string): Promise<void> {
|
||||||
await request(`/api/v1/members/${slug}/revoke-token`, { method: "POST" });
|
await request(`/api/v1/members/${slug}/revoke-token`, { method: "POST" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Project Members ---
|
||||||
|
|
||||||
|
export async function getProjectMembers(slug: string): Promise<ProjectMember[]> {
|
||||||
|
return request(`/api/v1/projects/${slug}/members`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function addProjectMember(slug: string, memberSlug: string): Promise<void> {
|
||||||
|
await request(`/api/v1/projects/${slug}/members`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ slug: memberSlug }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function removeProjectMember(slug: string, memberSlug: string): Promise<void> {
|
||||||
|
await request(`/api/v1/projects/${slug}/members/${memberSlug}`, { method: "DELETE" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Project Members ---
|
||||||
|
|
||||||
|
export async function getProjectMembers(slug: string): Promise<ProjectMember[]> {
|
||||||
|
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" });
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue
Block a user