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:
parent
34f473c935
commit
c314ac46d8
@ -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;
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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> {
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user