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>
|
||||||
<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 && (
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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" });
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user