Full CreateTaskModal: type, assignee, reviewer, labels
All available fields at creation time: - Title, Description, Type (task/bug/feature) - Status, Priority - Assignee + Reviewer (dropdowns with all members) - Labels (toggle buttons, project-scoped) - parent_id (when creating subtask) Modal wider (max-w-lg), scrollable
This commit is contained in:
parent
9e24516bd0
commit
7ebff6d381
@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
import { useState } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import type { Task } from "@/lib/api";
|
import type { Task, Member, Label } from "@/lib/api";
|
||||||
import { createTask } from "@/lib/api";
|
import { createTask, getMembers, getLabels } from "@/lib/api";
|
||||||
|
|
||||||
const STATUSES = [
|
const STATUSES = [
|
||||||
{ key: "backlog", label: "Бэклог" },
|
{ key: "backlog", label: "Бэклог" },
|
||||||
@ -18,6 +18,12 @@ const PRIORITIES = [
|
|||||||
{ key: "critical", label: "Critical", color: "#ef4444" },
|
{ key: "critical", label: "Critical", color: "#ef4444" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const TYPES = [
|
||||||
|
{ key: "task", label: "Задача" },
|
||||||
|
{ key: "bug", label: "Баг" },
|
||||||
|
{ key: "feature", label: "Фича" },
|
||||||
|
];
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
initialStatus: string;
|
initialStatus: string;
|
||||||
@ -32,9 +38,21 @@ export default function CreateTaskModal({ projectId, initialStatus, parentId, pa
|
|||||||
const [description, setDescription] = useState("");
|
const [description, setDescription] = useState("");
|
||||||
const [status, setStatus] = useState(initialStatus);
|
const [status, setStatus] = useState(initialStatus);
|
||||||
const [priority, setPriority] = useState("medium");
|
const [priority, setPriority] = useState("medium");
|
||||||
|
const [type, setType] = useState("task");
|
||||||
|
const [assigneeId, setAssigneeId] = useState("");
|
||||||
|
const [reviewerId, setReviewerId] = useState("");
|
||||||
|
const [selectedLabels, setSelectedLabels] = useState<string[]>([]);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
|
|
||||||
|
const [members, setMembers] = useState<Member[]>([]);
|
||||||
|
const [labels, setLabels] = useState<Label[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getMembers().then(setMembers).catch(() => {});
|
||||||
|
getLabels(projectId).then(setLabels).catch(() => {});
|
||||||
|
}, [projectId]);
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!title.trim()) return;
|
if (!title.trim()) return;
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
@ -45,7 +63,11 @@ export default function CreateTaskModal({ projectId, initialStatus, parentId, pa
|
|||||||
description: description.trim() || undefined,
|
description: description.trim() || undefined,
|
||||||
status,
|
status,
|
||||||
priority,
|
priority,
|
||||||
|
type,
|
||||||
parent_id: parentId,
|
parent_id: parentId,
|
||||||
|
assignee_id: assigneeId || undefined,
|
||||||
|
reviewer_id: reviewerId || undefined,
|
||||||
|
labels: selectedLabels,
|
||||||
});
|
});
|
||||||
onCreated(task);
|
onCreated(task);
|
||||||
onClose();
|
onClose();
|
||||||
@ -59,7 +81,7 @@ export default function CreateTaskModal({ projectId, initialStatus, parentId, pa
|
|||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50 px-4" onClick={onClose}>
|
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50 px-4" onClick={onClose}>
|
||||||
<div
|
<div
|
||||||
className="bg-[var(--card)] border border-[var(--border)] rounded-xl w-full max-w-md p-6"
|
className="bg-[var(--card)] border border-[var(--border)] rounded-xl w-full max-w-lg p-6 max-h-[90vh] overflow-y-auto"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<h3 className="text-lg font-bold mb-4">
|
<h3 className="text-lg font-bold mb-4">
|
||||||
@ -67,6 +89,7 @@ export default function CreateTaskModal({ projectId, initialStatus, parentId, pa
|
|||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
{/* Title */}
|
||||||
<div>
|
<div>
|
||||||
<label className="text-xs text-[var(--muted)] mb-1 block">Название</label>
|
<label className="text-xs text-[var(--muted)] mb-1 block">Название</label>
|
||||||
<input
|
<input
|
||||||
@ -79,6 +102,7 @@ export default function CreateTaskModal({ projectId, initialStatus, parentId, pa
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
<div>
|
<div>
|
||||||
<label className="text-xs text-[var(--muted)] mb-1 block">Описание</label>
|
<label className="text-xs text-[var(--muted)] mb-1 block">Описание</label>
|
||||||
<textarea
|
<textarea
|
||||||
@ -90,22 +114,42 @@ export default function CreateTaskModal({ projectId, initialStatus, parentId, pa
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Type */}
|
||||||
<div>
|
<div>
|
||||||
<label className="text-xs text-[var(--muted)] mb-1 block">Статус</label>
|
<label className="text-xs text-[var(--muted)] mb-1 block">Тип</label>
|
||||||
<div className="flex flex-wrap gap-1">
|
<div className="flex gap-1">
|
||||||
{STATUSES.map((s) => (
|
{TYPES.map((t) => (
|
||||||
<button
|
<button
|
||||||
key={s.key}
|
key={t.key}
|
||||||
onClick={() => setStatus(s.key)}
|
onClick={() => setType(t.key)}
|
||||||
className={`px-2 py-1 rounded text-xs transition-colors
|
className={`px-2 py-1 rounded text-xs transition-colors
|
||||||
${status === s.key ? "bg-[var(--accent)] text-white" : "bg-white/5 text-[var(--muted)] hover:bg-white/10"}`}
|
${type === t.key ? "bg-[var(--accent)] text-white" : "bg-white/5 text-[var(--muted)] hover:bg-white/10"}`}
|
||||||
>
|
>
|
||||||
{s.label}
|
{t.label}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Status + Priority row */}
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<div className="flex-1">
|
||||||
|
<label className="text-xs text-[var(--muted)] mb-1 block">Статус</label>
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
{STATUSES.map((s) => (
|
||||||
|
<button
|
||||||
|
key={s.key}
|
||||||
|
onClick={() => setStatus(s.key)}
|
||||||
|
className={`px-2 py-1 rounded text-xs transition-colors
|
||||||
|
${status === s.key ? "bg-[var(--accent)] text-white" : "bg-white/5 text-[var(--muted)] hover:bg-white/10"}`}
|
||||||
|
>
|
||||||
|
{s.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="text-xs text-[var(--muted)] mb-1 block">Приоритет</label>
|
<label className="text-xs text-[var(--muted)] mb-1 block">Приоритет</label>
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
@ -123,6 +167,63 @@ export default function CreateTaskModal({ projectId, initialStatus, parentId, pa
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Assignee + Reviewer */}
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<div className="flex-1">
|
||||||
|
<label className="text-xs text-[var(--muted)] mb-1 block">Исполнитель</label>
|
||||||
|
<select
|
||||||
|
value={assigneeId}
|
||||||
|
onChange={(e) => setAssigneeId(e.target.value)}
|
||||||
|
className="w-full px-2 py-1.5 bg-[var(--bg)] border border-[var(--border)] rounded-lg text-sm outline-none"
|
||||||
|
>
|
||||||
|
<option value="">Не назначен</option>
|
||||||
|
{members.map((m) => (
|
||||||
|
<option key={m.id} value={m.id}>{m.name} ({m.slug})</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<label className="text-xs text-[var(--muted)] mb-1 block">Ревьюер</label>
|
||||||
|
<select
|
||||||
|
value={reviewerId}
|
||||||
|
onChange={(e) => setReviewerId(e.target.value)}
|
||||||
|
className="w-full px-2 py-1.5 bg-[var(--bg)] border border-[var(--border)] rounded-lg text-sm outline-none"
|
||||||
|
>
|
||||||
|
<option value="">Не назначен</option>
|
||||||
|
{members.map((m) => (
|
||||||
|
<option key={m.id} value={m.id}>{m.name} ({m.slug})</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Labels */}
|
||||||
|
{labels.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<label className="text-xs text-[var(--muted)] mb-1 block">Лейблы</label>
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
{labels.map((l) => (
|
||||||
|
<button
|
||||||
|
key={l.id}
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedLabels(prev =>
|
||||||
|
prev.includes(l.name) ? prev.filter(n => n !== l.name) : [...prev, l.name]
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
className={`flex items-center gap-1 px-2 py-1 rounded text-xs transition-colors border ${
|
||||||
|
selectedLabels.includes(l.name)
|
||||||
|
? "border-[var(--accent)] bg-[var(--accent)]/20"
|
||||||
|
: "border-[var(--border)] bg-white/5 text-[var(--muted)] hover:bg-white/10"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className="w-2 h-2 rounded-full" style={{ backgroundColor: l.color }} />
|
||||||
|
{l.name}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{error && <div className="text-xs text-red-400">{error}</div>}
|
{error && <div className="text-xs text-red-400">{error}</div>}
|
||||||
|
|
||||||
<div className="flex gap-2 justify-end pt-2">
|
<div className="flex gap-2 justify-end pt-2">
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user