|
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>#1527 + #1528 — AFTER — staff action bar, four viewer states</title>
|
|
<style>
|
|
:root{
|
|
--background:#f8fafc; --surface:#fff; --surface-muted:#f1f5f9;
|
|
--border:#e2e8f0; --border-strong:#cbd5e1;
|
|
--text-primary:#0f172a; --text-secondary:#334155; --text-muted:#475569;
|
|
--primary:#047857; --primary-hover:#059669; --on-primary:#fff;
|
|
--brand-50:#ecfdf5; --brand-500:#10b981; --brand-700:#047857;
|
|
--spec-fg:#6d28d9; --spec-bg:#f3eefe;
|
|
--design-fg:#6d28d9; --design-bg:#f3eefe;
|
|
--building-fg:#b45309; --building-bg:#fef3e2;
|
|
--quality-fg:#0e7490; --quality-bg:#e0f2f5;
|
|
--prod-fg:#15803d; --prod-bg:#e7f6ec;
|
|
--proposed-fg:#475569; --proposed-bg:#f1f5f9;
|
|
--danger-fg:#b91c1c; --danger-bg:#fcebeb;
|
|
--accent-50:#e0f2f5; --accent-700:#0e7490;
|
|
--focus-ring:#047857;
|
|
}
|
|
*{box-sizing:border-box}
|
|
body{margin:0;background:var(--background);color:var(--text-primary);
|
|
font-family:Inter,ui-sans-serif,system-ui,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
|
|
font-size:14px;line-height:1.45;-webkit-font-smoothing:antialiased}
|
|
.page{max-width:840px;margin:0 auto;padding:22px 20px 44px}
|
|
h2.state{margin:30px 0 4px;font-size:15px;font-weight:700;letter-spacing:-.01em}
|
|
h2.state span.num{display:inline-flex;align-items:center;justify-content:center;
|
|
height:22px;width:22px;border-radius:9999px;background:var(--brand-50);color:var(--brand-700);
|
|
font-size:12px;margin-right:8px}
|
|
p.note{margin:0 0 12px;font-size:12.5px;color:var(--text-muted);max-width:64ch}
|
|
p.note b{color:var(--text-primary)}
|
|
code{font-family:ui-monospace,"JetBrains Mono",Menlo,monospace;font-size:.92em;
|
|
background:var(--surface-muted);border-radius:4px;padding:1px 4px}
|
|
.banner{border:1px solid var(--border);background:var(--surface);border-radius:12px;
|
|
padding:12px 14px;margin-bottom:6px;font-size:12.5px;color:var(--text-muted)}
|
|
.banner b{color:var(--text-primary)}
|
|
.card{background:var(--surface);border:1px solid var(--border);border-radius:16px;
|
|
padding:18px;box-shadow:0 1px 2px rgba(15,23,42,.05)}
|
|
.hero{position:relative;overflow:hidden}
|
|
.wash{position:absolute;right:-40px;top:-48px;height:176px;width:176px;border-radius:9999px;
|
|
background:var(--brand-50);filter:blur(28px)}
|
|
.hero>.inner{position:relative;display:flex;flex-direction:column;gap:12px}
|
|
.pillrow{display:flex;flex-wrap:wrap;align-items:center;gap:8px}
|
|
.idchip{display:inline-flex;align-items:center;border:1px solid var(--border);
|
|
background:var(--background);border-radius:8px;padding:2px 8px;
|
|
font-family:ui-monospace,"JetBrains Mono",Menlo,monospace;font-size:12px;color:var(--text-muted)}
|
|
.badge{display:inline-flex;align-items:center;gap:6px;border-radius:9999px;padding:3px 10px;
|
|
font-size:12px;font-weight:600}
|
|
.badge.spec,.badge.design{background:var(--spec-bg);color:var(--spec-fg)}
|
|
.badge.dev{background:var(--building-bg);color:var(--building-fg)}
|
|
.badge.qa{background:var(--quality-bg);color:var(--quality-fg)}
|
|
.badge.backlog{background:var(--proposed-bg);color:var(--proposed-fg)}
|
|
.badge.ship{background:var(--prod-bg);color:var(--prod-fg)}
|
|
.tag{display:inline-flex;align-items:center;gap:5px;border-radius:9999px;padding:3px 9px;
|
|
font-size:11.5px;font-weight:600;background:var(--accent-50);color:var(--accent-700)}
|
|
h1{margin:0;max-width:34rem;font-size:23px;line-height:1.2;font-weight:600;letter-spacing:-.02em}
|
|
.metarow{display:flex;flex-wrap:wrap;align-items:center;gap:10px 18px}
|
|
.meta{font-size:13px;color:var(--text-muted);display:inline-flex;align-items:center;gap:8px}
|
|
.avatar{height:24px;width:24px;border-radius:9999px;background:var(--accent-50);color:var(--accent-700);
|
|
display:inline-flex;align-items:center;justify-content:center;font-size:10px;font-weight:700}
|
|
.avatar.sm{height:20px;width:20px;font-size:9px}
|
|
.actions{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin-left:auto}
|
|
.btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;height:36px;
|
|
padding:0 12px;border-radius:8px;font-size:13px;font-weight:500;white-space:nowrap;
|
|
border:1px solid transparent;cursor:default}
|
|
.btn.secondary{background:var(--surface);border-color:var(--border);color:var(--text-primary)}
|
|
.btn.primary{background:var(--primary);color:var(--on-primary)}
|
|
.btn.square{width:36px;padding:0}
|
|
.btn.muted{background:var(--surface);border-color:var(--border);color:var(--text-muted);opacity:.55}
|
|
.btn.focused{outline:2px solid var(--focus-ring);outline-offset:2px}
|
|
|
|
/* ---- IR.1 staff action bar ---- */
|
|
.staffbar{margin-top:14px;border:1px solid var(--border);background:var(--surface-muted);
|
|
border-radius:12px;padding:12px 14px;display:flex;flex-direction:column;gap:10px}
|
|
.staffbar .head{display:flex;align-items:center;gap:8px;font-size:11px;font-weight:600;
|
|
text-transform:uppercase;letter-spacing:.06em;color:var(--text-muted)}
|
|
.staffbar .row{display:flex;flex-wrap:wrap;align-items:center;gap:10px}
|
|
.select{display:inline-flex;align-items:center;justify-content:space-between;gap:10px;height:36px;
|
|
min-width:15rem;border:1px solid var(--border);background:var(--surface);border-radius:8px;
|
|
padding:0 12px;font-size:13px;font-weight:500;color:var(--text-primary)}
|
|
.select .lead{display:inline-flex;align-items:center;gap:8px}
|
|
.select .caret{color:var(--text-muted);font-size:11px}
|
|
.select.disabled{opacity:.6}
|
|
.menu{position:relative}
|
|
.menupanel{margin-top:6px;border:1px solid var(--border);background:var(--surface);border-radius:8px;
|
|
padding:4px;box-shadow:0 8px 24px rgba(15,23,42,.12);min-width:15rem;display:flex;
|
|
flex-direction:column;gap:2px}
|
|
.menupanel .item{display:flex;align-items:center;gap:8px;border-radius:6px;padding:7px 10px;
|
|
font-size:13px;color:var(--text-secondary)}
|
|
.menupanel .item.hi{background:var(--surface-muted)}
|
|
.menupanel .sep{height:1px;background:var(--border);margin:3px 2px}
|
|
.menupanel .note{padding:7px 10px;font-size:12px;color:var(--text-muted)}
|
|
.tipwrap{position:relative;display:inline-flex}
|
|
.tip{position:absolute;bottom:calc(100% + 8px);left:50%;transform:translateX(-50%);
|
|
width:max-content;max-width:15rem;border:1px solid var(--border);background:var(--surface);
|
|
border-radius:8px;box-shadow:0 8px 24px rgba(15,23,42,.14);padding:8px 12px;font-size:12px;
|
|
color:var(--text-secondary);z-index:5}
|
|
.tip:after{content:"";position:absolute;top:100%;left:50%;transform:translateX(-50%) rotate(45deg);
|
|
margin-top:-5px;height:9px;width:9px;background:var(--surface);border-right:1px solid var(--border);
|
|
border-bottom:1px solid var(--border)}
|
|
.alert{display:flex;align-items:flex-start;gap:8px;border-radius:8px;padding:8px 10px;
|
|
font-size:12.5px;background:var(--danger-bg);color:var(--danger-fg);border:1px solid #f3cccc}
|
|
.hint{font-size:12px;color:var(--text-muted)}
|
|
|
|
.rail{width:19rem}
|
|
.railcard{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:14px;
|
|
box-shadow:0 1px 2px rgba(15,23,42,.05)}
|
|
.railcard h3{margin:0 0 4px;font-size:11px;font-weight:600;text-transform:uppercase;
|
|
letter-spacing:.06em;color:var(--text-muted)}
|
|
dl{margin:0}
|
|
.dlrow{display:flex;align-items:center;justify-content:space-between;gap:12px;
|
|
border-top:1px solid var(--border);padding:9px 0;font-size:13px}
|
|
.dlrow dt{color:var(--text-muted)}
|
|
.dlrow dd{margin:0;font-weight:500;text-align:right;display:inline-flex;align-items:center;gap:7px}
|
|
.mono{font-family:ui-monospace,"JetBrains Mono",Menlo,monospace}
|
|
.two{display:grid;grid-template-columns:minmax(0,1fr) 19rem;gap:18px;align-items:start}
|
|
.callout{border-left:3px solid var(--brand-500);background:var(--brand-50);border-radius:0 8px 8px 0;
|
|
padding:9px 12px;font-size:12.5px;color:var(--text-secondary);margin:10px 0 0}
|
|
.payload{margin:8px 0 0;border:1px solid var(--border);background:var(--surface);border-radius:8px;
|
|
padding:9px 11px;font-family:ui-monospace,"JetBrains Mono",Menlo,monospace;font-size:11.5px;
|
|
color:var(--text-secondary);white-space:pre-wrap}
|
|
.payload b{color:var(--text-primary)}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="page">
|
|
|
|
<div class="banner">
|
|
<b>AFTER — #1527 (Move Forward + Change Status) and #1528 (Assign + assignee) designed together.</b><br>
|
|
Both land in the same hero/action region of <code>TaskDetailPane.tsx</code>, so they get one
|
|
design-system section (§IR) and <b>one mount point</b>: a <code>StaffActionBar</code> band
|
|
directly under the hero card. The hero action row itself is <b>untouched</b> — that is how state 1
|
|
stays byte-for-byte today's pane.<br>
|
|
No new colour: everything below is §2/§3 tokens already in the system.
|
|
</div>
|
|
|
|
<!-- ============ 1. ORDINARY CLIENT ============ -->
|
|
<h2 class="state"><span class="num">1</span>Ordinary client — <code>internal_role: null</code></h2>
|
|
<p class="note">Nothing is added. No staff bar, no Move Forward, no Change Status, no Assign, and
|
|
<b>no assignee name anywhere</b> — the payload does not carry <code>assigned_to_name</code> at all
|
|
for this viewer, so the name is absent from the HTTP response, not merely hidden in the DOM
|
|
(spec decision 7). Identical to the BEFORE capture.</p>
|
|
<div class="two">
|
|
<div>
|
|
<section class="card hero">
|
|
<div class="wash"></div>
|
|
<div class="inner">
|
|
<div class="pillrow"><span class="idchip">#1527</span><span class="badge spec">Spec</span><span class="tag">✦ Feature</span></div>
|
|
<h1>Ticket panel: Move Forward and a role-correct Change Status control</h1>
|
|
<div class="metarow">
|
|
<span class="meta"><span class="avatar">RA</span>Requested by Redmine Admin</span>
|
|
<span class="meta">Updated 2 hours ago</span>
|
|
<span class="actions">
|
|
<span class="btn secondary square">✳</span>
|
|
<span class="btn secondary">✎ Edit</span>
|
|
<span class="btn secondary">⏸ Pause</span>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
<div class="payload">GET …/tasks/1527 → <b>{ status_id: 8, allowed_statuses: [] }</b>
|
|
no <b>can_move_forward</b> · no <b>can_assign</b> · no <b>assigned_to_name</b> key</div>
|
|
</div>
|
|
<div class="rail">
|
|
<div class="railcard">
|
|
<h3>At a glance</h3>
|
|
<dl>
|
|
<div class="dlrow"><dt>Reference</dt><dd class="mono">#1527</dd></div>
|
|
<div class="dlrow"><dt>Stage</dt><dd><span class="badge spec">Spec</span></dd></div>
|
|
<div class="dlrow"><dt>Type</dt><dd>Feature</dd></div>
|
|
</dl>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ============ 2. DEVELOPER, ASSIGNED ============ -->
|
|
<h2 class="state"><span class="num">2</span>Developer, assigned to this ticket</h2>
|
|
<p class="note">One control, not two. The server sends <code>allowed_statuses</code> already narrowed
|
|
to what this viewer may set — for a developer that is exactly <code>[next_status]</code>, so
|
|
<b>length < 2 ⇒ Change Status is not rendered</b> and Move Forward is the only door. A
|
|
one-option dropdown would be a menu offering a choice that does not exist, three clicks for what
|
|
the button does in one. The label <b>names the target stage</b>, from
|
|
<code>next_status_id</code> + <code>next_status_name</code>.</p>
|
|
<div class="two">
|
|
<div>
|
|
<section class="card hero">
|
|
<div class="wash"></div>
|
|
<div class="inner">
|
|
<div class="pillrow"><span class="idchip">#1527</span><span class="badge spec">Spec</span><span class="tag">✦ Feature</span></div>
|
|
<h1>Ticket panel: Move Forward and a role-correct Change Status control</h1>
|
|
<div class="metarow">
|
|
<span class="meta"><span class="avatar">RA</span>Requested by Redmine Admin</span>
|
|
<span class="actions">
|
|
<span class="btn secondary square">✳</span>
|
|
<span class="btn secondary">✎ Edit</span>
|
|
<span class="btn secondary">⏸ Pause</span>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="staffbar">
|
|
<div class="head">⚙ Staff controls</div>
|
|
<div class="row">
|
|
<span class="btn primary">→ Move to Design</span>
|
|
<span class="hint">You are assigned to this ticket.</span>
|
|
</div>
|
|
</section>
|
|
<div class="payload">can_move_forward: <b>true</b> · next_status_id: <b>15</b> ("Design")
|
|
allowed_statuses: <b>[ {id:15, name:"Design"} ]</b> → 1 option ⇒ no dropdown</div>
|
|
</div>
|
|
<div class="rail">
|
|
<div class="railcard">
|
|
<h3>At a glance</h3>
|
|
<dl>
|
|
<div class="dlrow"><dt>Reference</dt><dd class="mono">#1527</dd></div>
|
|
<div class="dlrow"><dt>Stage</dt><dd><span class="badge spec">Spec</span></dd></div>
|
|
<div class="dlrow"><dt>Assigned to</dt><dd><span class="avatar sm">GW</span>Guillaume W.</dd></div>
|
|
</dl>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ============ 3. DEVELOPER, NOT ASSIGNED ============ -->
|
|
<h2 class="state"><span class="num">3</span>Developer, <b>not</b> assigned — disabled + tooltip</h2>
|
|
<p class="note">The button is present and <b>keyboard-focusable</b>: <code>aria-disabled="true"</code>
|
|
with the click handler short-circuited, never a bare <code>disabled</code> (which drops it out of
|
|
the tab order, so the explanation becomes unreachable exactly for the people who need it most).
|
|
The tooltip is the <code>aria-describedby</code> target, shown on hover <i>and</i> on
|
|
<code>:focus-visible</code>. Below: the same button focused by keyboard, with the second reason
|
|
shape — nothing left to advance to.</p>
|
|
<div class="two">
|
|
<div>
|
|
<section class="card hero">
|
|
<div class="wash"></div>
|
|
<div class="inner">
|
|
<div class="pillrow"><span class="idchip">#1531</span><span class="badge dev">In development</span></div>
|
|
<h1>Backend: the progression rule and the move-forward route</h1>
|
|
<div class="metarow">
|
|
<span class="meta"><span class="avatar">RA</span>Requested by Redmine Admin</span>
|
|
<span class="actions"><span class="btn secondary square">✳</span><span class="btn secondary">⏸ Pause</span></span>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="staffbar">
|
|
<div class="head">⚙ Staff controls</div>
|
|
<div class="row" style="padding-top:56px">
|
|
<span class="tipwrap">
|
|
<span class="tip" id="tip-notassignee">Only the developer assigned to this ticket can move it forward.</span>
|
|
<span class="btn muted" aria-describedby="tip-notassignee">→ Move to QA</span>
|
|
</span>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="staffbar">
|
|
<div class="head">⚙ Staff controls — variant: no next stage (ticket is Shipped)</div>
|
|
<div class="row" style="padding-top:56px">
|
|
<span class="tipwrap">
|
|
<span class="tip" id="tip-nonext">This ticket is at the end of the pipeline.</span>
|
|
<span class="btn muted focused" aria-describedby="tip-nonext">→ Move forward</span>
|
|
</span>
|
|
<span class="hint">focus-visible ring — the tooltip opens on keyboard focus too</span>
|
|
</div>
|
|
</section>
|
|
<div class="payload">can_move_forward: <b>false</b>, next_status_id: <b>10</b> → “not assigned to you”
|
|
can_move_forward: <b>false</b>, next_status_id: <b>null</b> → “end of the pipeline”
|
|
allowed_statuses: <b>[]</b> in both cases ⇒ no Change Status control</div>
|
|
</div>
|
|
<div class="rail">
|
|
<div class="railcard">
|
|
<h3>At a glance</h3>
|
|
<dl>
|
|
<div class="dlrow"><dt>Stage</dt><dd><span class="badge dev">In development</span></dd></div>
|
|
<div class="dlrow"><dt>Assigned to</dt><dd><span class="avatar sm">AL</span>Alice L.</dd></div>
|
|
</dl>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ============ 4. CTO / ADMIN ============ -->
|
|
<h2 class="state"><span class="num">4</span>CTO / platform admin — full set, backward included, plus Assign</h2>
|
|
<p class="note">Three controls on one row, in decreasing frequency of use:
|
|
<b>Move Forward</b> (primary — the one verb used twenty times a day) → <b>Change Status</b>
|
|
(the correction path, forward <i>and</i> backward) → <b>Assign</b>. The Change Status menu lists
|
|
<code>allowed_statuses</code> verbatim: the frontend holds no status order, so it cannot decide
|
|
what “forward” means and does not try. Backlog is absent because the server never puts it there.</p>
|
|
<div class="two">
|
|
<div>
|
|
<section class="card hero">
|
|
<div class="wash"></div>
|
|
<div class="inner">
|
|
<div class="pillrow"><span class="idchip">#1527</span><span class="badge spec">Spec</span><span class="tag">✦ Feature</span></div>
|
|
<h1>Ticket panel: Move Forward and a role-correct Change Status control</h1>
|
|
<div class="metarow">
|
|
<span class="meta"><span class="avatar">RA</span>Requested by Redmine Admin</span>
|
|
<span class="actions"><span class="btn secondary square">✳</span><span class="btn secondary">✎ Edit</span><span class="btn secondary">⏸ Pause</span></span>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="staffbar">
|
|
<div class="head">⚙ Staff controls</div>
|
|
<div class="row">
|
|
<span class="btn primary">→ Move to Design</span>
|
|
<span class="select"><span class="lead"><span class="badge spec">Spec</span><span style="color:var(--text-secondary);font-weight:400">Change status</span></span><span class="caret">▾</span></span>
|
|
</div>
|
|
<div class="row">
|
|
<span class="select"><span class="lead"><span class="avatar sm">GW</span>Assigned to Guillaume W.</span><span class="caret">▾</span></span>
|
|
</div>
|
|
<div class="menu">
|
|
<div class="menupanel" style="max-width:16rem">
|
|
<div class="item hi"><span class="avatar sm" style="background:var(--surface-muted);color:var(--text-muted)">—</span>Unassigned</div>
|
|
<div class="sep"></div>
|
|
<div class="item"><span class="avatar sm">GW</span>Guillaume W. <span style="color:var(--text-muted);font-size:11.5px">· current</span></div>
|
|
<div class="item"><span class="avatar sm">AL</span>Alice L.</div>
|
|
<div class="item"><span class="avatar sm">MD</span>Marc D.</div>
|
|
</div>
|
|
</div>
|
|
<p class="hint">Assign menu open — <code>GET /projects/9/developers</code>. “Unassigned” is an
|
|
explicit first option, separated by a hairline from the people.</p>
|
|
</section>
|
|
|
|
<div class="callout"><b>Change Status menu (same anatomy as the existing <code>TransitionControl</code>,
|
|
§TB.6):</b> each target is a <code>TaskStatusPill</code>, selecting one opens the inline
|
|
confirm step before the mutation fires. Backward targets are listed in the server's order,
|
|
unmarked — a “backward” badge would need a status order in <code>src/</code>.</div>
|
|
<div class="menu">
|
|
<div class="menupanel">
|
|
<div class="item"><span class="badge backlog">Submitted</span></div>
|
|
<div class="item hi"><span class="badge design">Design</span></div>
|
|
<div class="item"><span class="badge dev">In development</span></div>
|
|
<div class="item"><span class="badge qa">QA</span></div>
|
|
<div class="item"><span class="badge ship">Ready to ship</span></div>
|
|
<div class="item"><span class="badge backlog">Blocked</span></div>
|
|
<div class="note">No “Backlog” — the server never offers it, in either direction.</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="payload">can_move_forward: <b>true</b> · can_assign: <b>true</b> · next_status_id: <b>15</b>
|
|
allowed_statuses: <b>7, 15, 9, 10, 11, 12, 13, 14</b> → ≥ 2 options ⇒ dropdown renders</div>
|
|
</div>
|
|
<div class="rail">
|
|
<div class="railcard">
|
|
<h3>At a glance</h3>
|
|
<dl>
|
|
<div class="dlrow"><dt>Reference</dt><dd class="mono">#1527</dd></div>
|
|
<div class="dlrow"><dt>Stage</dt><dd><span class="badge spec">Spec</span></dd></div>
|
|
<div class="dlrow"><dt>Assigned to</dt><dd><span class="avatar sm">GW</span>Guillaume W.</dd></div>
|
|
</dl>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ============ 5. BACKLOG ============ -->
|
|
<h2 class="state"><span class="num">5</span>Backlog ticket, viewed by a CTO — the credit gate holds</h2>
|
|
<p class="note"><b>No status control renders for anyone, including CTO and admin.</b> Not a disabled
|
|
Move Forward with an explanation — <i>absent</i>. Submit stays the only way out of the backlog,
|
|
and the pane must not suggest a second door exists. The <b>Assign control survives</b>: assigning
|
|
changes no status and costs no credit, so a CTO can line the work up before the client pays.</p>
|
|
<div class="two">
|
|
<div>
|
|
<section class="card hero">
|
|
<div class="wash"></div>
|
|
<div class="inner">
|
|
<div class="pillrow"><span class="idchip">#1528</span><span class="badge backlog">Backlog</span><span class="tag">✦ Feature</span></div>
|
|
<h1>Ticket panel: Assign to Developer and assignee visibility</h1>
|
|
<div class="metarow">
|
|
<span class="meta">Complexity <b style="color:var(--text-primary)">4/10</b></span>
|
|
<span class="actions">
|
|
<span class="btn primary">➤ Submit to the team</span>
|
|
<span class="btn secondary square">✳</span>
|
|
<span class="btn secondary">✎ Edit</span>
|
|
<span class="btn secondary">⏸ Pause</span>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="staffbar">
|
|
<div class="head">⚙ Staff controls</div>
|
|
<div class="row">
|
|
<span class="select disabled"><span class="lead"><span class="avatar sm" style="background:var(--surface-muted);color:var(--text-muted)">—</span>Unassigned</span><span class="caret">▾</span></span>
|
|
<span class="hint">No developer is configured for this project.</span>
|
|
</div>
|
|
<p class="hint">Empty-state variant of the Assign control: <code>GET …/developers</code> legitimately
|
|
returned <code>[]</code>. The trigger is <code>aria-disabled</code> (focusable, described by
|
|
the note), never an empty menu that opens onto nothing.</p>
|
|
</section>
|
|
<p class="hint" style="margin-top:10px">Filing is free — nothing starts until you submit this ticket.</p>
|
|
<div class="payload">status_id: <b>16</b> → Move Forward and Change Status are <b>not rendered</b>
|
|
can_move_forward: <b>false</b> · next_status_id: <b>null</b> · allowed_statuses: <b>[]</b>
|
|
can_assign: <b>true</b> — assignment is not a status change</div>
|
|
</div>
|
|
<div class="rail">
|
|
<div class="railcard">
|
|
<h3>At a glance</h3>
|
|
<dl>
|
|
<div class="dlrow"><dt>Stage</dt><dd><span class="badge backlog">Backlog</span></dd></div>
|
|
<div class="dlrow"><dt>Assigned to</dt><dd style="color:var(--text-muted);font-weight:400">Unassigned</dd></div>
|
|
</dl>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ============ 6. ERRORS ============ -->
|
|
<h2 class="state"><span class="num">6</span>Server refusals — inline, never blanking the pane</h2>
|
|
<p class="note">Every error is a <code>role="alert"</code> paragraph <b>inside the staff bar</b>, under
|
|
the control that produced it. The ticket, its description and its criteria stay on screen; the
|
|
status pill never showed an unconfirmed stage, because the controls are pessimistic by design —
|
|
there is no optimistic state to roll back. Each failure still invalidates the board and detail
|
|
queries, so a pane made stale by someone else converges on the next paint.</p>
|
|
<section class="staffbar" style="gap:12px">
|
|
<div class="head">⚙ Staff controls</div>
|
|
<div class="row">
|
|
<span class="btn primary">→ Move to QA</span>
|
|
<span class="select"><span class="lead"><span class="badge dev">In development</span><span style="color:var(--text-secondary);font-weight:400">Change status</span></span><span class="caret">▾</span></span>
|
|
</div>
|
|
<div class="alert">⚠ <span><b>403</b> — You're not allowed to make that change. The ticket has been refreshed.</span></div>
|
|
<div class="alert">⚠ <span><b>409</b> — Couldn't move this task. <span style="opacity:.85">(the existing invalid-transition message, reused verbatim)</span></span></div>
|
|
<div class="row">
|
|
<span class="select"><span class="lead"><span class="avatar sm">MD</span>Assigned to Marc D.</span><span class="caret">▾</span></span>
|
|
</div>
|
|
<div class="alert">⚠ <span><b>422</b> — Marc D. has no Redmine account linked yet. An admin can add one in the admin dashboard.</span></div>
|
|
<div class="alert">⚠ <span><b>upstream</b> — Couldn't change the assignee.</span></div>
|
|
</section>
|
|
|
|
</div>
|
|
</body>
|
|
</html>
|