Labels UI + task labels on kanban + API functions

- Labels management in ProjectSettings (CRUD, color picker)
- Task labels displayed as colored pills on kanban cards
- API: getLabels, createLabel, deleteLabel, addTaskLabel, removeTaskLabel
- Task links API functions
This commit is contained in:
Markov 2026-02-27 23:46:59 +01:00
parent be3007f40a
commit f3eeed5eb9
3 changed files with 94 additions and 2 deletions

View File

@ -111,6 +111,15 @@ export default function KanbanBoard({ projectId, projectSlug }: Props) {
)} )}
</div> </div>
<div className="text-sm ml-3.5">{task.title}</div> <div className="text-sm ml-3.5">{task.title}</div>
{task.labels && task.labels.length > 0 && (
<div className="flex flex-wrap gap-1 ml-3.5 mt-1">
{task.labels.map((label: string) => (
<span key={label} className="text-[10px] px-1.5 py-0.5 rounded-full bg-[var(--accent)]/20 text-[var(--accent)]">
{label}
</span>
))}
</div>
)}
{showMoveButtons && colIndex !== undefined && ( {showMoveButtons && colIndex !== undefined && (
<div className="flex gap-1 mt-2 ml-3.5"> <div className="flex gap-1 mt-2 ml-3.5">
{colIndex > 0 && ( {colIndex > 0 && (

View File

@ -1,13 +1,16 @@
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 } from "@/lib/api"; import type { Project, ProjectMember, Member, Label } from "@/lib/api";
import { import {
updateProject, updateProject,
deleteProject, deleteProject,
getProjectMembers, getProjectMembers,
addProjectMember, addProjectMember,
removeProjectMember, removeProjectMember,
getMembers getMembers,
getLabels,
createLabel,
deleteLabel,
} from "@/lib/api"; } from "@/lib/api";
interface Props { interface Props {
@ -31,6 +34,11 @@ 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
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(() => {
const loadMembers = async () => { const loadMembers = async () => {
@ -41,6 +49,8 @@ export default function ProjectSettings({ project, onUpdated }: Props) {
]); ]);
setMembers(projectMembers); setMembers(projectMembers);
setAllMembers(allMembersList); setAllMembers(allMembersList);
const projectLabels = await getLabels(project.id);
setLabels(projectLabels);
} catch (e) { } catch (e) {
console.error("Failed to load members:", e); console.error("Failed to load members:", e);
} finally { } finally {
@ -165,6 +175,51 @@ 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>
{/* 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

@ -448,3 +448,31 @@ export async function createTaskLink(taskId: string, targetId: string, linkType:
export async function deleteTaskLink(taskId: string, linkId: string): Promise<void> { export async function deleteTaskLink(taskId: string, linkId: string): Promise<void> {
return request(`/tasks/${taskId}/links/${linkId}`, { method: "DELETE" }); return request(`/tasks/${taskId}/links/${linkId}`, { method: "DELETE" });
} }
// --- Labels ---
export interface Label {
id: string;
project_id: string;
name: string;
color: string;
}
export async function getLabels(projectId: string): Promise<Label[]> {
return request(`/projects/${projectId}/labels`);
}
export async function createLabel(projectId: string, name: string, color: string = "#6366f1"): Promise<Label> {
return request(`/projects/${projectId}/labels`, { method: "POST", body: JSON.stringify({ name, color }) });
}
export async function deleteLabel(labelId: string): Promise<void> {
return request(`/labels/${labelId}`, { method: "DELETE" });
}
export async function addTaskLabel(taskId: string, labelId: string): Promise<void> {
return request(`/tasks/${taskId}/labels/${labelId}`, { method: "POST" });
}
export async function removeTaskLabel(taskId: string, labelId: string): Promise<void> {
return request(`/tasks/${taskId}/labels/${labelId}`, { method: "DELETE" });
}