UUID everywhere: all API calls use ID instead of slug

- Project API: project.id for all CRUD + files + members
- Member API: member.id for get/update/regenerate/revoke
- ProjectSettings: member selection by id
- ChatPanel/MentionInput: projectId prop
- AgentModal: agent.id for API, slug for display only
This commit is contained in:
Markov 2026-02-27 09:37:28 +01:00
parent fd3e454dff
commit 52be59dc9f
7 changed files with 59 additions and 59 deletions

View File

@ -28,7 +28,7 @@ export default function AgentModal({ agent, onClose, onUpdated }: Props) {
setSaving(true); setSaving(true);
try { try {
const caps = capabilities.split(",").map((c) => c.trim()).filter(Boolean); const caps = capabilities.split(",").map((c) => c.trim()).filter(Boolean);
const updated = await updateMember(agent.slug, { const updated = await updateMember(agent.id, {
name: name.trim(), name: name.trim(),
agent_config: { agent_config: {
capabilities: caps, capabilities: caps,
@ -49,7 +49,7 @@ export default function AgentModal({ agent, onClose, onUpdated }: Props) {
const handleRegenerate = async () => { const handleRegenerate = async () => {
try { try {
const { token } = await regenerateToken(agent.slug); const { token } = await regenerateToken(agent.id);
setCurrentToken(token); setCurrentToken(token);
setConfirmRegen(false); setConfirmRegen(false);
} catch (e) { } catch (e) {
@ -59,7 +59,7 @@ export default function AgentModal({ agent, onClose, onUpdated }: Props) {
const handleRevoke = async () => { const handleRevoke = async () => {
try { try {
await revokeToken(agent.slug); await revokeToken(agent.id);
setCurrentToken(null); setCurrentToken(null);
setConfirmRevoke(false); setConfirmRevoke(false);
} catch (e) { } catch (e) {

View File

@ -22,7 +22,7 @@ interface StreamingState {
interface Props { interface Props {
chatId: string; chatId: string;
projectSlug?: string; projectId?: string;
} }
function formatFileSize(bytes: number): string { function formatFileSize(bytes: number): string {
@ -35,7 +35,7 @@ function isImageMime(mime: string | null): boolean {
return !!mime && mime.startsWith("image/"); return !!mime && mime.startsWith("image/");
} }
export default function ChatPanel({ chatId, projectSlug }: Props) { export default function ChatPanel({ chatId, projectId }: Props) {
const [messages, setMessages] = useState<Message[]>([]); const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState(""); const [input, setInput] = useState("");
const [sending, setSending] = useState(false); const [sending, setSending] = useState(false);
@ -399,7 +399,7 @@ export default function ChatPanel({ chatId, projectSlug }: Props) {
{uploading ? "⏳" : "📎"} {uploading ? "⏳" : "📎"}
</button> </button>
<MentionInput <MentionInput
projectSlug={projectSlug || ""} projectId={projectId || ""}
value={input} value={input}
onChange={setInput} onChange={setInput}
onSubmit={handleSend} onSubmit={handleSend}

View File

@ -3,7 +3,7 @@ import type { ProjectMember } from "@/lib/api";
import { getProjectMembers } from "@/lib/api"; import { getProjectMembers } from "@/lib/api";
interface Props { interface Props {
projectSlug: string; projectId: string;
value: string; value: string;
onChange: (value: string) => void; onChange: (value: string) => void;
onSubmit: () => void; onSubmit: () => void;
@ -11,7 +11,7 @@ interface Props {
disabled?: boolean; disabled?: boolean;
} }
export default function MentionInput({ projectSlug, value, onChange, onSubmit, placeholder, disabled }: Props) { export default function MentionInput({ projectId, value, onChange, onSubmit, placeholder, disabled }: Props) {
const [members, setMembers] = useState<ProjectMember[]>([]); const [members, setMembers] = useState<ProjectMember[]>([]);
const [showDropdown, setShowDropdown] = useState(false); const [showDropdown, setShowDropdown] = useState(false);
const [filter, setFilter] = useState(""); const [filter, setFilter] = useState("");
@ -22,9 +22,9 @@ export default function MentionInput({ projectSlug, value, onChange, onSubmit, p
// Load members once // Load members once
useEffect(() => { useEffect(() => {
if (!projectSlug) return; if (!projectId) return;
getProjectMembers(projectSlug).then(setMembers).catch(() => {}); getProjectMembers(projectId).then(setMembers).catch(() => {});
}, [projectSlug]); }, [projectId]);
const filtered = members.filter((m) => const filtered = members.filter((m) =>
m.name.toLowerCase().includes(filter.toLowerCase()) || m.name.toLowerCase().includes(filter.toLowerCase()) ||

View File

@ -47,14 +47,14 @@ export default function ProjectFiles({ project }: Props) {
const loadFiles = useCallback(async () => { const loadFiles = useCallback(async () => {
try { try {
const data = await getProjectFiles(project.slug, search || undefined); const data = await getProjectFiles(project.id, search || undefined);
setFiles(data); setFiles(data);
} catch (e) { } catch (e) {
console.error("Failed to load files:", e); console.error("Failed to load files:", e);
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [project.slug, search]); }, [project.id, search]);
// Debounced search // Debounced search
useEffect(() => { useEffect(() => {
@ -67,7 +67,7 @@ export default function ProjectFiles({ project }: Props) {
setUploading(true); setUploading(true);
try { try {
for (const file of Array.from(fileList)) { for (const file of Array.from(fileList)) {
await uploadProjectFile(project.slug, file, uploadDesc || undefined); await uploadProjectFile(project.id, file, uploadDesc || undefined);
} }
setUploadDesc(""); setUploadDesc("");
setShowUploadDesc(false); setShowUploadDesc(false);
@ -83,7 +83,7 @@ export default function ProjectFiles({ project }: Props) {
const handleDelete = async (f: ProjectFile) => { const handleDelete = async (f: ProjectFile) => {
if (!confirm(`Удалить ${f.filename}?`)) return; if (!confirm(`Удалить ${f.filename}?`)) return;
try { try {
await deleteProjectFile(project.slug, f.id); await deleteProjectFile(project.id, f.id);
setFiles((prev) => prev.filter((x) => x.id !== f.id)); setFiles((prev) => prev.filter((x) => x.id !== f.id));
} catch (e) { } catch (e) {
console.error("Delete failed:", e); console.error("Delete failed:", e);
@ -92,7 +92,7 @@ export default function ProjectFiles({ project }: Props) {
const handleSaveDesc = async (f: ProjectFile) => { const handleSaveDesc = async (f: ProjectFile) => {
try { try {
const updated = await updateProjectFile(project.slug, f.id, { description: editDesc }); const updated = await updateProjectFile(project.id, f.id, { description: editDesc });
setFiles((prev) => prev.map((x) => (x.id === f.id ? updated : x))); setFiles((prev) => prev.map((x) => (x.id === f.id ? updated : x)));
setEditingId(null); setEditingId(null);
} catch (e) { } catch (e) {
@ -171,7 +171,7 @@ export default function ProjectFiles({ project }: Props) {
{!loading && files.length > 0 && ( {!loading && files.length > 0 && (
<div className="space-y-1"> <div className="space-y-1">
{files.map((f) => { {files.map((f) => {
const url = getProjectFileUrl(project.slug, f.id); const url = getProjectFileUrl(project.id, f.id);
const isImage = f.mime_type?.startsWith("image/"); const isImage = f.mime_type?.startsWith("image/");
return ( return (
<div <div

View File

@ -36,7 +36,7 @@ export default function ProjectSettings({ project, onUpdated }: Props) {
const loadMembers = async () => { const loadMembers = async () => {
try { try {
const [projectMembers, allMembersList] = await Promise.all([ const [projectMembers, allMembersList] = await Promise.all([
getProjectMembers(project.slug), getProjectMembers(project.id),
getMembers() getMembers()
]); ]);
setMembers(projectMembers); setMembers(projectMembers);
@ -58,8 +58,8 @@ export default function ProjectSettings({ project, onUpdated }: Props) {
const handleAddMember = async () => { const handleAddMember = async () => {
if (!selectedMember) return; if (!selectedMember) return;
try { try {
await addProjectMember(project.slug, selectedMember); await addProjectMember(project.id, selectedMember);
const newMember = allMembers.find((m) => m.slug === selectedMember); const newMember = allMembers.find((m) => m.id === selectedMember);
if (newMember) { if (newMember) {
setMembers([...members, { setMembers([...members, {
id: newMember.id, id: newMember.id,
@ -75,10 +75,10 @@ export default function ProjectSettings({ project, onUpdated }: Props) {
} }
}; };
const handleRemoveMember = async (memberSlug: string) => { const handleRemoveMember = async (memberId: string) => {
try { try {
await removeProjectMember(project.slug, memberSlug); await removeProjectMember(project.id, memberId);
setMembers(members.filter((m) => m.slug !== memberSlug)); setMembers(members.filter((m) => m.id !== memberId));
} catch (e) { } catch (e) {
console.error("Failed to remove member:", e); console.error("Failed to remove member:", e);
} }
@ -88,7 +88,7 @@ export default function ProjectSettings({ project, onUpdated }: Props) {
setSaving(true); setSaving(true);
try { try {
const urls = repoUrls.split("\n").map((u) => u.trim()).filter(Boolean); const urls = repoUrls.split("\n").map((u) => u.trim()).filter(Boolean);
const updated = await updateProject(project.slug, { const updated = await updateProject(project.id, {
name: name.trim(), name: name.trim(),
description: description.trim() || null, description: description.trim() || null,
repo_urls: urls, repo_urls: urls,
@ -106,7 +106,7 @@ export default function ProjectSettings({ project, onUpdated }: Props) {
const handleDelete = async () => { const handleDelete = async () => {
try { try {
await deleteProject(project.slug); await deleteProject(project.id);
navigate("/"); navigate("/");
} catch (e) { } catch (e) {
console.error(e); console.error(e);
@ -197,7 +197,7 @@ export default function ProjectSettings({ project, onUpdated }: Props) {
</div> </div>
{member.role !== "owner" && ( {member.role !== "owner" && (
<button <button
onClick={() => handleRemoveMember(member.slug)} onClick={() => handleRemoveMember(member.id)}
className="px-2 py-1 text-xs text-red-400 hover:bg-red-500/10 rounded" className="px-2 py-1 text-xs text-red-400 hover:bg-red-500/10 rounded"
> >
Убрать Убрать
@ -217,7 +217,7 @@ export default function ProjectSettings({ project, onUpdated }: Props) {
> >
<option value="">Добавить участника...</option> <option value="">Добавить участника...</option>
{availableMembers.map((member) => ( {availableMembers.map((member) => (
<option key={member.slug} value={member.slug}> <option key={member.id} value={member.id}>
{member.name} ({member.slug}) - {member.type} {member.name} ({member.slug}) - {member.type}
</option> </option>
))} ))}

View File

@ -169,20 +169,20 @@ export async function getProjects(): Promise<Project[]> {
return request("/api/v1/projects"); return request("/api/v1/projects");
} }
export async function getProject(slug: string): Promise<Project> { export async function getProject(projectId: string): Promise<Project> {
return request(`/api/v1/projects/${slug}`); return request(`/api/v1/projects/${projectId}`);
} }
export async function createProject(data: { name: string; slug: string; description?: string }): Promise<Project> { export async function createProject(data: { name: string; slug: string; description?: string }): Promise<Project> {
return request("/api/v1/projects", { method: "POST", body: JSON.stringify(data) }); return request("/api/v1/projects", { method: "POST", body: JSON.stringify(data) });
} }
export async function updateProject(slug: string, data: Partial<Pick<Project, "name" | "description" | "repo_urls" | "status">>): Promise<Project> { export async function updateProject(projectId: string, data: Partial<Pick<Project, "name" | "slug" | "description" | "repo_urls" | "status">>): Promise<Project> {
return request(`/api/v1/projects/${slug}`, { method: "PATCH", body: JSON.stringify(data) }); return request(`/api/v1/projects/${projectId}`, { method: "PATCH", body: JSON.stringify(data) });
} }
export async function deleteProject(slug: string): Promise<void> { export async function deleteProject(projectId: string): Promise<void> {
await request(`/api/v1/projects/${slug}`, { method: "DELETE" }); await request(`/api/v1/projects/${projectId}`, { method: "DELETE" });
} }
// --- Tasks --- // --- Tasks ---
@ -287,8 +287,8 @@ export async function getMembers(): Promise<Member[]> {
return request("/api/v1/members"); return request("/api/v1/members");
} }
export async function getMember(slug: string): Promise<Member> { export async function getMember(memberId: string): Promise<Member> {
return request(`/api/v1/members/${slug}`); return request(`/api/v1/members/${memberId}`);
} }
export async function createMember(data: { export async function createMember(data: {
@ -300,21 +300,21 @@ export async function createMember(data: {
return request("/api/v1/members", { method: "POST", body: JSON.stringify(data) }); return request("/api/v1/members", { method: "POST", body: JSON.stringify(data) });
} }
export async function updateMember(slug: string, data: { export async function updateMember(memberId: string, data: {
name?: string; name?: string;
role?: string; role?: string;
status?: string; status?: string;
agent_config?: Partial<AgentConfig>; agent_config?: Partial<AgentConfig>;
}): Promise<Member> { }): Promise<Member> {
return request(`/api/v1/members/${slug}`, { method: "PATCH", body: JSON.stringify(data) }); return request(`/api/v1/members/${memberId}`, { method: "PATCH", body: JSON.stringify(data) });
} }
export async function regenerateToken(slug: string): Promise<{ token: string }> { export async function regenerateToken(memberId: string): Promise<{ token: string }> {
return request(`/api/v1/members/${slug}/regenerate-token`, { method: "POST" }); return request(`/api/v1/members/${memberId}/regenerate-token`, { method: "POST" });
} }
export async function revokeToken(slug: string): Promise<void> { export async function revokeToken(memberId: string): Promise<void> {
await request(`/api/v1/members/${slug}/revoke-token`, { method: "POST" }); await request(`/api/v1/members/${memberId}/revoke-token`, { method: "POST" });
} }
// --- Project Files --- // --- Project Files ---
@ -330,18 +330,18 @@ export interface ProjectFile {
updated_at: string; updated_at: string;
} }
export async function getProjectFiles(slug: string, search?: string): Promise<ProjectFile[]> { export async function getProjectFiles(projectId: string, search?: string): Promise<ProjectFile[]> {
const qs = search ? `?search=${encodeURIComponent(search)}` : ""; const qs = search ? `?search=${encodeURIComponent(search)}` : "";
return request(`/api/v1/projects/${slug}/files${qs}`); return request(`/api/v1/projects/${projectId}/files${qs}`);
} }
export async function uploadProjectFile(slug: string, file: File, description?: string): Promise<ProjectFile> { export async function uploadProjectFile(projectId: string, file: File, description?: string): Promise<ProjectFile> {
const token = getToken(); const token = getToken();
const formData = new FormData(); const formData = new FormData();
formData.append("file", file); formData.append("file", file);
if (description) formData.append("description", description); if (description) formData.append("description", description);
const res = await fetch(`${API_BASE}/api/v1/projects/${slug}/files`, { const res = await fetch(`${API_BASE}/api/v1/projects/${projectId}/files`, {
method: "POST", method: "POST",
headers: token ? { Authorization: `Bearer ${token}` } : {}, headers: token ? { Authorization: `Bearer ${token}` } : {},
body: formData, body: formData,
@ -358,18 +358,18 @@ export async function uploadProjectFile(slug: string, file: File, description?:
return res.json(); return res.json();
} }
export function getProjectFileUrl(slug: string, fileId: string): string { export function getProjectFileUrl(projectId: string, fileId: string): string {
const token = getToken(); const token = getToken();
const qs = token ? `?token=${encodeURIComponent(token)}` : ""; const qs = token ? `?token=${encodeURIComponent(token)}` : "";
return `${API_BASE}/api/v1/projects/${slug}/files/${fileId}/download${qs}`; return `${API_BASE}/api/v1/projects/${projectId}/files/${fileId}/download${qs}`;
} }
export async function updateProjectFile(slug: string, fileId: string, data: { description?: string }): Promise<ProjectFile> { export async function updateProjectFile(projectId: string, fileId: string, data: { description?: string }): Promise<ProjectFile> {
return request(`/api/v1/projects/${slug}/files/${fileId}`, { method: "PATCH", body: JSON.stringify(data) }); return request(`/api/v1/projects/${projectId}/files/${fileId}`, { method: "PATCH", body: JSON.stringify(data) });
} }
export async function deleteProjectFile(slug: string, fileId: string): Promise<void> { export async function deleteProjectFile(projectId: string, fileId: string): Promise<void> {
await request(`/api/v1/projects/${slug}/files/${fileId}`, { method: "DELETE" }); await request(`/api/v1/projects/${projectId}/files/${fileId}`, { method: "DELETE" });
} }
// --- Chat File Upload --- // --- Chat File Upload ---
@ -412,17 +412,17 @@ export function getAttachmentUrl(attachmentId: string): string {
// --- Project Members --- // --- Project Members ---
export async function getProjectMembers(slug: string): Promise<ProjectMember[]> { export async function getProjectMembers(projectId: string): Promise<ProjectMember[]> {
return request(`/api/v1/projects/${slug}/members`); return request(`/api/v1/projects/${projectId}/members`);
} }
export async function addProjectMember(slug: string, memberSlug: string): Promise<{ ok: boolean }> { export async function addProjectMember(projectId: string, memberId: string): Promise<{ ok: boolean }> {
return request(`/api/v1/projects/${slug}/members`, { return request(`/api/v1/projects/${projectId}/members`, {
method: "POST", method: "POST",
body: JSON.stringify({ slug: memberSlug }) body: JSON.stringify({ member_id: memberId })
}); });
} }
export async function removeProjectMember(slug: string, memberSlug: string): Promise<{ ok: boolean }> { export async function removeProjectMember(projectId: string, memberId: string): Promise<{ ok: boolean }> {
return request(`/api/v1/projects/${slug}/members/${memberSlug}`, { method: "DELETE" }); return request(`/api/v1/projects/${projectId}/members/${memberId}`, { method: "DELETE" });
} }

View File

@ -101,7 +101,7 @@ export default function ProjectPage() {
<KanbanBoard projectId={project.id} projectSlug={project.slug} /> <KanbanBoard projectId={project.id} projectSlug={project.slug} />
)} )}
{activeTab === "chat" && project.chat_id && ( {activeTab === "chat" && project.chat_id && (
<ChatPanel chatId={project.chat_id} projectSlug={project.slug} /> <ChatPanel chatId={project.chat_id} projectId={project.id} />
)} )}
{activeTab === "chat" && !project.chat_id && ( {activeTab === "chat" && !project.chat_id && (
<div className="flex items-center justify-center h-full text-[var(--muted)] text-sm"> <div className="flex items-center justify-center h-full text-[var(--muted)] text-sm">