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:
parent
be3007f40a
commit
f3eeed5eb9
@ -111,6 +111,15 @@ export default function KanbanBoard({ projectId, projectSlug }: Props) {
|
||||
)}
|
||||
</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 && (
|
||||
<div className="flex gap-1 mt-2 ml-3.5">
|
||||
{colIndex > 0 && (
|
||||
|
||||
@ -1,13 +1,16 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import type { Project, ProjectMember, Member } from "@/lib/api";
|
||||
import type { Project, ProjectMember, Member, Label } from "@/lib/api";
|
||||
import {
|
||||
updateProject,
|
||||
deleteProject,
|
||||
getProjectMembers,
|
||||
addProjectMember,
|
||||
removeProjectMember,
|
||||
getMembers
|
||||
getMembers,
|
||||
getLabels,
|
||||
createLabel,
|
||||
deleteLabel,
|
||||
} from "@/lib/api";
|
||||
|
||||
interface Props {
|
||||
@ -31,6 +34,11 @@ export default function ProjectSettings({ project, onUpdated }: Props) {
|
||||
const [loadingMembers, setLoadingMembers] = useState(true);
|
||||
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
|
||||
useEffect(() => {
|
||||
const loadMembers = async () => {
|
||||
@ -41,6 +49,8 @@ export default function ProjectSettings({ project, onUpdated }: Props) {
|
||||
]);
|
||||
setMembers(projectMembers);
|
||||
setAllMembers(allMembersList);
|
||||
const projectLabels = await getLabels(project.id);
|
||||
setLabels(projectLabels);
|
||||
} catch (e) {
|
||||
console.error("Failed to load members:", e);
|
||||
} finally {
|
||||
@ -165,6 +175,51 @@ export default function ProjectSettings({ project, onUpdated }: Props) {
|
||||
{saved && <span className="text-sm text-green-400">Сохранено ✓</span>}
|
||||
</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 */}
|
||||
<div className="pt-6 border-t border-[var(--border)]">
|
||||
<div className="text-sm font-medium mb-3">Участники</div>
|
||||
|
||||
@ -448,3 +448,31 @@ export async function createTaskLink(taskId: string, targetId: string, linkType:
|
||||
export async function deleteTaskLink(taskId: string, linkId: string): Promise<void> {
|
||||
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" });
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user