Global labels: move to app Settings, instant auto-assign toggle

- Labels management in /settings (not project settings)
- getLabels() no longer takes projectId
- Auto-assign toggle saves immediately on click
- Labels removed from ProjectSettings
This commit is contained in:
Markov 2026-02-28 00:42:04 +01:00
parent 34f473c935
commit c314ac46d8
4 changed files with 86 additions and 68 deletions

View File

@ -50,8 +50,8 @@ export default function CreateTaskModal({ projectId, initialStatus, parentId, pa
useEffect(() => { useEffect(() => {
getMembers().then(setMembers).catch(() => {}); getMembers().then(setMembers).catch(() => {});
getLabels(projectId).then(setLabels).catch(() => {}); getLabels().then(setLabels).catch(() => {});
}, [projectId]); }, []);
const handleSubmit = async () => { const handleSubmit = async () => {
if (!title.trim()) return; if (!title.trim()) return;

View File

@ -1,6 +1,6 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import type { Project, ProjectMember, Member, Label } from "@/lib/api"; import type { Project, ProjectMember, Member } from "@/lib/api";
import { import {
updateProject, updateProject,
deleteProject, deleteProject,
@ -8,9 +8,6 @@ import {
addProjectMember, addProjectMember,
removeProjectMember, removeProjectMember,
getMembers, getMembers,
getLabels,
createLabel,
deleteLabel,
} from "@/lib/api"; } from "@/lib/api";
interface Props { interface Props {
@ -35,10 +32,7 @@ export default function ProjectSettings({ project, onUpdated }: Props) {
const [loadingMembers, setLoadingMembers] = useState(true); const [loadingMembers, setLoadingMembers] = useState(true);
const [selectedMember, setSelectedMember] = useState(""); const [selectedMember, setSelectedMember] = useState("");
// Labels state // Labels removed — now in app settings
const [labels, setLabels] = useState<Label[]>([]);
const [newLabelName, setNewLabelName] = useState("");
const [newLabelColor, setNewLabelColor] = useState("#6366f1");
// Load project members and all members // Load project members and all members
useEffect(() => { useEffect(() => {
@ -50,8 +44,7 @@ export default function ProjectSettings({ project, onUpdated }: Props) {
]); ]);
setMembers(projectMembers); setMembers(projectMembers);
setAllMembers(allMembersList); setAllMembers(allMembersList);
const projectLabels = await getLabels(project.id); // Labels now in app settings
setLabels(projectLabels);
} catch (e) { } catch (e) {
console.error("Failed to load members:", e); console.error("Failed to load members:", e);
} finally { } finally {
@ -97,7 +90,6 @@ export default function ProjectSettings({ project, onUpdated }: Props) {
description: description.trim() || null, description: description.trim() || null,
repo_urls: urls, repo_urls: urls,
status, status,
auto_assign: autoAssign,
}); });
onUpdated(updated); onUpdated(updated);
setSaved(true); setSaved(true);
@ -173,7 +165,14 @@ export default function ProjectSettings({ project, onUpdated }: Props) {
<div className="text-xs text-[var(--muted)]">Назначать задачи агентам по лейблам capabilities</div> <div className="text-xs text-[var(--muted)]">Назначать задачи агентам по лейблам capabilities</div>
</div> </div>
<button <button
onClick={() => setAutoAssign(!autoAssign)} onClick={async () => {
const newVal = !autoAssign;
setAutoAssign(newVal);
try {
const updated = await updateProject(project.id, { auto_assign: newVal });
onUpdated(updated);
} catch (e) { setAutoAssign(!newVal); }
}}
className={`relative w-10 h-5 rounded-full transition-colors duration-200 ${ className={`relative w-10 h-5 rounded-full transition-colors duration-200 ${
autoAssign ? "bg-[var(--accent)]" : "bg-[var(--border)]" autoAssign ? "bg-[var(--accent)]" : "bg-[var(--border)]"
}`} }`}
@ -196,50 +195,6 @@ export default function ProjectSettings({ project, onUpdated }: Props) {
</div> </div>
{/* Labels Section */} {/* Labels Section */}
<div className="pt-6 border-t border-[var(--border)]">
<div className="text-sm font-medium mb-3">Лейблы</div>
<div className="space-y-2 mb-3">
{labels.map((label) => (
<div key={label.id} className="flex items-center justify-between p-2 bg-[var(--bg)] border border-[var(--border)] rounded-lg">
<div className="flex items-center gap-2">
<span className="w-3 h-3 rounded-full" style={{ backgroundColor: label.color }} />
<span className="text-sm">{label.name}</span>
</div>
<button
onClick={async () => {
await deleteLabel(label.id);
setLabels(labels.filter(l => l.id !== label.id));
}}
className="text-xs text-red-400 hover:text-red-300"
>×</button>
</div>
))}
</div>
<div className="flex gap-2">
<input
value={newLabelName}
onChange={e => setNewLabelName(e.target.value)}
placeholder="Новый лейбл..."
className="flex-1 px-3 py-1.5 bg-[var(--bg)] border border-[var(--border)] rounded-lg text-sm outline-none"
/>
<input
type="color"
value={newLabelColor}
onChange={e => setNewLabelColor(e.target.value)}
className="w-8 h-8 rounded cursor-pointer"
/>
<button
onClick={async () => {
if (!newLabelName.trim()) return;
const label = await createLabel(project.id, newLabelName.trim(), newLabelColor);
setLabels([...labels, label]);
setNewLabelName("");
}}
className="px-3 py-1.5 bg-[var(--accent)] text-white rounded-lg text-sm"
>+</button>
</div>
</div>
{/* Members Section */} {/* Members Section */}
<div className="pt-6 border-t border-[var(--border)]"> <div className="pt-6 border-t border-[var(--border)]">
<div className="text-sm font-medium mb-3">Участники</div> <div className="text-sm font-medium mb-3">Участники</div>

View File

@ -464,17 +464,16 @@ export async function deleteTaskLink(taskId: string, linkId: string): Promise<vo
// --- Labels --- // --- Labels ---
export interface Label { export interface Label {
id: string; id: string;
project_id: string;
name: string; name: string;
color: string; color: string;
} }
export async function getLabels(projectId: string): Promise<Label[]> { export async function getLabels(): Promise<Label[]> {
return request(`/projects/${projectId}/labels`); return request(`/labels`);
} }
export async function createLabel(projectId: string, name: string, color: string = "#6366f1"): Promise<Label> { export async function createLabel(name: string, color: string = "#6366f1"): Promise<Label> {
return request(`/projects/${projectId}/labels`, { method: "POST", body: JSON.stringify({ name, color }) }); return request(`/labels`, { method: "POST", body: JSON.stringify({ name, color }) });
} }
export async function deleteLabel(labelId: string): Promise<void> { export async function deleteLabel(labelId: string): Promise<void> {

View File

@ -1,10 +1,74 @@
import { useState, useEffect } from "react";
import type { Label } from "@/lib/api";
import { getLabels, createLabel, deleteLabel } from "@/lib/api";
export default function SettingsPage() { export default function SettingsPage() {
const [labels, setLabels] = useState<Label[]>([]);
const [newName, setNewName] = useState("");
const [newColor, setNewColor] = useState("#6366f1");
useEffect(() => {
getLabels().then(setLabels).catch(() => {});
}, []);
return ( return (
<div className="p-6"> <div className="p-6 max-w-2xl">
<h1 className="text-2xl font-bold mb-4"> Общие настройки</h1> <h1 className="text-2xl font-bold mb-6"> Общие настройки</h1>
<div className="text-[var(--muted)] text-sm">
<p>Coming soon...</p> {/* Labels */}
<div>
<h2 className="text-lg font-semibold mb-3">Лейблы</h2>
<p className="text-xs text-[var(--muted)] mb-3">Глобальные лейблы для задач и агентов. Используются для авто-назначения.</p>
<div className="space-y-2 mb-4">
{labels.map((label) => (
<div key={label.id} className="flex items-center justify-between p-2 bg-[var(--bg)] border border-[var(--border)] rounded-lg">
<div className="flex items-center gap-2">
<span className="w-3 h-3 rounded-full" style={{ backgroundColor: label.color }} />
<span className="text-sm">{label.name}</span>
</div>
<button
onClick={async () => {
await deleteLabel(label.id);
setLabels(labels.filter(l => l.id !== label.id));
}}
className="text-xs text-red-400 hover:text-red-300"
>×</button>
</div>
))}
{labels.length === 0 && <p className="text-xs text-[var(--muted)]">Нет лейблов</p>}
</div>
<div className="flex gap-2">
<input
value={newName}
onChange={e => setNewName(e.target.value)}
onKeyDown={e => {
if (e.key === "Enter" && newName.trim()) {
createLabel(newName.trim(), newColor).then(l => {
setLabels([...labels, l]);
setNewName("");
});
}
}}
placeholder="Новый лейбл..."
className="flex-1 px-3 py-1.5 bg-[var(--bg)] border border-[var(--border)] rounded-lg text-sm outline-none"
/>
<input
type="color"
value={newColor}
onChange={e => setNewColor(e.target.value)}
className="w-8 h-8 rounded cursor-pointer"
/>
<button
onClick={async () => {
if (!newName.trim()) return;
const label = await createLabel(newName.trim(), newColor);
setLabels([...labels, label]);
setNewName("");
}}
className="px-3 py-1.5 bg-[var(--accent)] text-white rounded-lg text-sm"
>+</button>
</div>
</div> </div>
</div> </div>
); );
} }