Workflow declutter: auto-advance + hide redundant action buttons
Producer feedback: too many "Start Work" buttons everywhere, unclear
forward path, lots of clicking. This commit collapses the common cases.
Server (stage-service.updateStageStatus):
- Any TERMINAL transition (APPROVED / DELIVERED / SKIPPED) now
auto-opens every downstream stage whose prereqs are now satisfied.
Previously only `isCriticalGate` stages did this, and the unblock
landed them in NOT_STARTED — still requiring a Start Work click.
Now the gate requirement is dropped and unblocked stages flip
straight to IN_PROGRESS with a startDate stamp.
UI (deliverable detail stage rows):
- BLOCKED rows: no transition buttons. The blocked hint + lock icon
explain why nothing's actionable here (path forward is to finish
the upstream prereq).
- Stages with approvalType != NONE: hide the inline transition
buttons entirely — StageReviewPanel below the row owns Approve /
Request changes / Send to client / New revision, so duplicates
just cluttered the view.
- Everywhere else: same buttons as before, with the muted "Skip /
Reopen" outline so the forward action is the obvious primary.
UI (stage-review-panel):
- Approve button relabelled "Approve & advance" with a tooltip
explaining that it auto-opens the next stage.
Net effect: a typical 5-stage pipeline that previously needed
~10 button clicks to walk through (Start, Submit, Approve, Start,
Submit, Approve, ...) now needs ~5 — the first Start Work, then
each terminal transition cascades automatically.
What's NOT yet in this commit (acknowledged as follow-ups):
- Resources page deliverable-level assignment toggle.
- Revision-rework: when a stage requests changes via a configured
rework path, the intermediate stages between rework target and
current should reset. The rework transition itself still works;
just the intermediate cleanup is pending.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
02f18e6eab
commit
f0eb29dd0c
3 changed files with 84 additions and 47 deletions
|
|
@ -439,45 +439,76 @@ export default function DeliverableDetailPage() {
|
|||
/>
|
||||
</div>
|
||||
|
||||
{available.length > 0 && (
|
||||
<div className="ml-auto flex flex-wrap gap-1">
|
||||
{available.map((nextStatus) => {
|
||||
const isSkip = nextStatus === "SKIPPED";
|
||||
const isReopen =
|
||||
(nextStatus === "IN_PROGRESS" &&
|
||||
(stage.status === "APPROVED" ||
|
||||
stage.status === "DELIVERED")) ||
|
||||
(nextStatus === "NOT_STARTED" &&
|
||||
stage.status === "SKIPPED");
|
||||
const label = isReopen
|
||||
? "Reopen"
|
||||
: (TRANSITION_LABELS[nextStatus] ?? nextStatus);
|
||||
{(() => {
|
||||
// Visible action set rules (Phase 5b — declutter):
|
||||
// - BLOCKED rows: no buttons (path forward is the
|
||||
// upstream prereq; we don't want producers to click
|
||||
// "Skip" by accident). The lock icon below still
|
||||
// explains why nothing is actionable.
|
||||
// - Stages with approvalType !== NONE: the
|
||||
// StageReviewPanel owns the Approve / Request changes
|
||||
// / Send to client / New revision affordances. No
|
||||
// duplicates in this row.
|
||||
// - All other transitions are visible — but
|
||||
// destructive / sideways moves (Skip, Reopen) stay
|
||||
// muted outline so the forward path is the obvious
|
||||
// primary action.
|
||||
const stageApprovalType =
|
||||
stage.stageDefinition?.approvalType ??
|
||||
stage.template?.approvalType ??
|
||||
"NONE";
|
||||
const hasReviewPanel = stageApprovalType !== "NONE";
|
||||
const isBlocked = stage.status === "BLOCKED";
|
||||
|
||||
return (
|
||||
<Button
|
||||
key={nextStatus}
|
||||
size="sm"
|
||||
variant={isSkip || isReopen ? "outline" : "default"}
|
||||
className={cn(
|
||||
"h-6 text-[13px] px-2",
|
||||
(isSkip || isReopen) && "text-[var(--muted-foreground)]"
|
||||
)}
|
||||
disabled={updateStage.isPending}
|
||||
onClick={() => handleTransition(stage.id, nextStatus)}
|
||||
>
|
||||
{isSkip ? (
|
||||
<SkipForward className="mr-1 h-3 w-3" />
|
||||
) : isReopen ? (
|
||||
<RotateCcw className="mr-1 h-3 w-3" />
|
||||
) : (
|
||||
<ChevronRight className="mr-1 h-3 w-3" />
|
||||
)}
|
||||
{label}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
if (isBlocked || hasReviewPanel || available.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="ml-auto flex flex-wrap gap-1">
|
||||
{available.map((nextStatus) => {
|
||||
const isSkip = nextStatus === "SKIPPED";
|
||||
const isReopen =
|
||||
(nextStatus === "IN_PROGRESS" &&
|
||||
(stage.status === "APPROVED" ||
|
||||
stage.status === "DELIVERED")) ||
|
||||
(nextStatus === "NOT_STARTED" &&
|
||||
stage.status === "SKIPPED");
|
||||
const label = isReopen
|
||||
? "Reopen"
|
||||
: (TRANSITION_LABELS[nextStatus] ?? nextStatus);
|
||||
|
||||
return (
|
||||
<Button
|
||||
key={nextStatus}
|
||||
size="sm"
|
||||
variant={
|
||||
isSkip || isReopen ? "outline" : "default"
|
||||
}
|
||||
className={cn(
|
||||
"h-6 text-[13px] px-2",
|
||||
(isSkip || isReopen) &&
|
||||
"text-[var(--muted-foreground)]"
|
||||
)}
|
||||
disabled={updateStage.isPending}
|
||||
onClick={() =>
|
||||
handleTransition(stage.id, nextStatus)
|
||||
}
|
||||
>
|
||||
{isSkip ? (
|
||||
<SkipForward className="mr-1 h-3 w-3" />
|
||||
) : isReopen ? (
|
||||
<RotateCcw className="mr-1 h-3 w-3" />
|
||||
) : (
|
||||
<ChevronRight className="mr-1 h-3 w-3" />
|
||||
)}
|
||||
{label}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
|
||||
{/* Sub-status + blocked hint on their own third line only when present */}
|
||||
|
|
|
|||
|
|
@ -193,9 +193,10 @@ export function StageReviewPanel({
|
|||
className="h-6 px-2 text-[12px]"
|
||||
disabled={updateStageStatus.isPending}
|
||||
onClick={handleApprove}
|
||||
title="Approve this stage and auto-open the next stage(s)"
|
||||
>
|
||||
<CheckCircle2 className="mr-1 h-3 w-3" />
|
||||
Approve
|
||||
Approve & advance
|
||||
</Button>
|
||||
)}
|
||||
|
||||
|
|
@ -206,6 +207,7 @@ export function StageReviewPanel({
|
|||
className="h-6 px-2 text-[12px]"
|
||||
disabled={updateStageStatus.isPending}
|
||||
onClick={handleRequestChanges}
|
||||
title="Mark this stage as needing changes; producer will iterate"
|
||||
>
|
||||
<XCircle className="mr-1 h-3 w-3" />
|
||||
Request changes
|
||||
|
|
|
|||
|
|
@ -403,11 +403,12 @@ export async function updateStageStatus(
|
|||
downstreamStageNames: [] as string[],
|
||||
};
|
||||
|
||||
// Check if this will unblock downstream stages
|
||||
if (
|
||||
(newStatus === "APPROVED" || newStatus === "DELIVERED") &&
|
||||
stage.template.isCriticalGate
|
||||
) {
|
||||
// Check if this will auto-advance downstream stages. Phase 5b: ALL
|
||||
// terminal transitions (APPROVED / DELIVERED / SKIPPED) auto-open
|
||||
// every stage whose prerequisites are now satisfied — not just
|
||||
// critical gates. Producers stop hitting "Start Work" on every row.
|
||||
const TERMINAL: StageStatus[] = ["APPROVED", "DELIVERED", "SKIPPED"];
|
||||
if (TERMINAL.includes(newStatus)) {
|
||||
const allStages = stage.deliverable.stages.map((s) => ({
|
||||
id: s.id,
|
||||
status: s.id === stageId ? newStatus : s.status,
|
||||
|
|
@ -462,8 +463,11 @@ export async function updateStageStatus(
|
|||
include: { template: { include: { dependsOn: true } } },
|
||||
});
|
||||
|
||||
// If this stage was just APPROVED/DELIVERED and is a critical gate, unblock downstream
|
||||
if ((newStatus === "APPROVED" || newStatus === "DELIVERED") && stage.template.isCriticalGate) {
|
||||
// Phase 5b: any terminal transition auto-advances every downstream
|
||||
// stage whose prerequisites are now satisfied. Set them IN_PROGRESS
|
||||
// directly (not NOT_STARTED) — producers shouldn't have to click
|
||||
// "Start Work" on every unblocked row.
|
||||
if (TERMINAL.includes(newStatus)) {
|
||||
const allStages = stage.deliverable.stages.map((s) => ({
|
||||
id: s.id,
|
||||
status: s.id === stageId ? newStatus : s.status,
|
||||
|
|
@ -482,7 +486,7 @@ export async function updateStageStatus(
|
|||
if (idsToUnblock.length > 0) {
|
||||
await tx.deliverableStage.updateMany({
|
||||
where: { id: { in: idsToUnblock } },
|
||||
data: { status: "NOT_STARTED" },
|
||||
data: { status: "IN_PROGRESS", startDate: now },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue