feat(frontend): tickets/sprints surface for the web workspace (#86 lot 2)
Adds a Live/Tickets/Sprints tab navigation to WebWorkspace after a project is opened — a mobile-first adaptation, not a port of the desktop docks/ floating windows, per carnet #86. The web-server transport (17 ticket_*/ sprint_* commands) and the HttpTicketGateway were already wired (lot 1 + pre-existing gateway code); this lot is UI only. - Tickets tab: search/filters, list grouped by sprint, create screen, detail view with stacked accordion sections (Résumé/Statut et priorité/ Carnet open by default; Agents assignés/Liens/Zone dangereuse collapsed), delete confirmation, optimistic-concurrency conflict banner. - Sprints tab: create/rename/reorder (Monter/Descendre)/delete with confirmation, add tickets via a mobile full-screen ticket picker (never a small desktop modal), "Voir tickets" filters the Tickets tab to one sprint. - Reuses the transport-neutral hooks as-is (useTickets, useTicketDetail, useTicketSearch) — only presentation and copy are web-specific, in French per decision #78 (new local label module, not a reuse of the English desktop ticketMeta labels). - Workaround: `list_agents` isn't in the web-server allowlist, so agent names/assignment options are derived from `get_project_work_state` (already used by the Live tab) instead of the desktop's useProjectAgents. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
141
frontend/src/features/web/tickets/WebTicketCreate.tsx
Normal file
141
frontend/src/features/web/tickets/WebTicketCreate.tsx
Normal file
@ -0,0 +1,141 @@
|
||||
/**
|
||||
* Ticket creation screen for the web workspace (ticket #86) — replaces the list
|
||||
* in the column (no inline permanent form, no desktop popup), per carnet #86.
|
||||
*
|
||||
* Short form: Titre (required) / Priorité / Sprint / Description (optional).
|
||||
* On success the caller opens the created ticket's detail so the user can
|
||||
* complete carnet/assignments/links right away.
|
||||
*/
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
import type { CreateTicketInput } from "@/ports";
|
||||
import type { Sprint, TicketPriority } from "@/domain";
|
||||
import { Button, Input, Panel, cn } from "@/shared";
|
||||
import { TICKET_PRIORITIES } from "@/features/tickets";
|
||||
import { webPriorityLabel } from "./webTicketLabels";
|
||||
|
||||
const selectClass = cn(
|
||||
"h-9 rounded-md bg-raised px-3 text-sm text-content",
|
||||
"border border-border outline-none transition-colors",
|
||||
"focus:border-primary disabled:cursor-not-allowed disabled:opacity-50",
|
||||
);
|
||||
|
||||
const textareaClass = cn(
|
||||
"w-full rounded-md bg-raised p-3 text-sm text-content",
|
||||
"border border-border outline-none transition-colors",
|
||||
"focus:border-primary disabled:cursor-not-allowed disabled:opacity-50",
|
||||
);
|
||||
|
||||
export interface WebTicketCreateProps {
|
||||
sprints: Sprint[];
|
||||
busy: boolean;
|
||||
error: string | null;
|
||||
onCancel: () => void;
|
||||
onCreate: (input: CreateTicketInput, sprintId: string | null) => Promise<void>;
|
||||
}
|
||||
|
||||
export function WebTicketCreate({ sprints, busy, error, onCancel, onCreate }: WebTicketCreateProps) {
|
||||
const [title, setTitle] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [priority, setPriority] = useState<TicketPriority>("medium");
|
||||
const [sprintId, setSprintId] = useState<string>("");
|
||||
|
||||
async function submit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
const trimmed = title.trim();
|
||||
if (!trimmed) return;
|
||||
await onCreate(
|
||||
{
|
||||
title: trimmed,
|
||||
priority,
|
||||
...(description.trim() ? { description: description.trim() } : {}),
|
||||
},
|
||||
sprintId || null,
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Panel
|
||||
title="Nouveau ticket"
|
||||
actions={
|
||||
<Button size="sm" variant="ghost" onClick={onCancel}>
|
||||
Annuler
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
{error && (
|
||||
<p role="alert" className="mb-3 rounded-md border border-danger/40 bg-danger/10 px-3 py-2 text-sm text-danger">
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
<form onSubmit={submit} className="flex flex-col gap-3">
|
||||
<label className="flex flex-col gap-1">
|
||||
<span className="text-xs font-medium text-muted">Titre</span>
|
||||
<Input
|
||||
aria-label="Titre"
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
disabled={busy}
|
||||
autoFocus
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="flex flex-col gap-1">
|
||||
<span className="text-xs font-medium text-muted">Priorité</span>
|
||||
<select
|
||||
aria-label="Priorité"
|
||||
className={selectClass}
|
||||
value={priority}
|
||||
disabled={busy}
|
||||
onChange={(e) => setPriority(e.target.value as TicketPriority)}
|
||||
>
|
||||
{TICKET_PRIORITIES.map((p) => (
|
||||
<option key={p} value={p}>
|
||||
{webPriorityLabel(p)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label className="flex flex-col gap-1">
|
||||
<span className="text-xs font-medium text-muted">Sprint</span>
|
||||
<select
|
||||
aria-label="Sprint"
|
||||
className={selectClass}
|
||||
value={sprintId}
|
||||
disabled={busy}
|
||||
onChange={(e) => setSprintId(e.target.value)}
|
||||
>
|
||||
<option value="">Sans sprint</option>
|
||||
{sprints.map((s) => (
|
||||
<option key={s.id} value={s.id}>
|
||||
{s.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label className="flex flex-col gap-1">
|
||||
<span className="text-xs font-medium text-muted">Description</span>
|
||||
<textarea
|
||||
aria-label="Description"
|
||||
className={cn(textareaClass, "min-h-20")}
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
disabled={busy}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div className="flex justify-end gap-2 pt-1">
|
||||
<Button type="button" variant="ghost" size="sm" disabled={busy} onClick={onCancel}>
|
||||
Annuler
|
||||
</Button>
|
||||
<Button type="submit" size="sm" disabled={!title.trim() || busy} loading={busy}>
|
||||
Créer
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user