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>
|
</div>
|
||||||
|
|
||||||
{available.length > 0 && (
|
{(() => {
|
||||||
<div className="ml-auto flex flex-wrap gap-1">
|
// Visible action set rules (Phase 5b — declutter):
|
||||||
{available.map((nextStatus) => {
|
// - BLOCKED rows: no buttons (path forward is the
|
||||||
const isSkip = nextStatus === "SKIPPED";
|
// upstream prereq; we don't want producers to click
|
||||||
const isReopen =
|
// "Skip" by accident). The lock icon below still
|
||||||
(nextStatus === "IN_PROGRESS" &&
|
// explains why nothing is actionable.
|
||||||
(stage.status === "APPROVED" ||
|
// - Stages with approvalType !== NONE: the
|
||||||
stage.status === "DELIVERED")) ||
|
// StageReviewPanel owns the Approve / Request changes
|
||||||
(nextStatus === "NOT_STARTED" &&
|
// / Send to client / New revision affordances. No
|
||||||
stage.status === "SKIPPED");
|
// duplicates in this row.
|
||||||
const label = isReopen
|
// - All other transitions are visible — but
|
||||||
? "Reopen"
|
// destructive / sideways moves (Skip, Reopen) stay
|
||||||
: (TRANSITION_LABELS[nextStatus] ?? nextStatus);
|
// 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 (
|
if (isBlocked || hasReviewPanel || available.length === 0) {
|
||||||
<Button
|
return null;
|
||||||
key={nextStatus}
|
}
|
||||||
size="sm"
|
|
||||||
variant={isSkip || isReopen ? "outline" : "default"}
|
return (
|
||||||
className={cn(
|
<div className="ml-auto flex flex-wrap gap-1">
|
||||||
"h-6 text-[13px] px-2",
|
{available.map((nextStatus) => {
|
||||||
(isSkip || isReopen) && "text-[var(--muted-foreground)]"
|
const isSkip = nextStatus === "SKIPPED";
|
||||||
)}
|
const isReopen =
|
||||||
disabled={updateStage.isPending}
|
(nextStatus === "IN_PROGRESS" &&
|
||||||
onClick={() => handleTransition(stage.id, nextStatus)}
|
(stage.status === "APPROVED" ||
|
||||||
>
|
stage.status === "DELIVERED")) ||
|
||||||
{isSkip ? (
|
(nextStatus === "NOT_STARTED" &&
|
||||||
<SkipForward className="mr-1 h-3 w-3" />
|
stage.status === "SKIPPED");
|
||||||
) : isReopen ? (
|
const label = isReopen
|
||||||
<RotateCcw className="mr-1 h-3 w-3" />
|
? "Reopen"
|
||||||
) : (
|
: (TRANSITION_LABELS[nextStatus] ?? nextStatus);
|
||||||
<ChevronRight className="mr-1 h-3 w-3" />
|
|
||||||
)}
|
return (
|
||||||
{label}
|
<Button
|
||||||
</Button>
|
key={nextStatus}
|
||||||
);
|
size="sm"
|
||||||
})}
|
variant={
|
||||||
</div>
|
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>
|
</div>
|
||||||
|
|
||||||
{/* Sub-status + blocked hint on their own third line only when present */}
|
{/* 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]"
|
className="h-6 px-2 text-[12px]"
|
||||||
disabled={updateStageStatus.isPending}
|
disabled={updateStageStatus.isPending}
|
||||||
onClick={handleApprove}
|
onClick={handleApprove}
|
||||||
|
title="Approve this stage and auto-open the next stage(s)"
|
||||||
>
|
>
|
||||||
<CheckCircle2 className="mr-1 h-3 w-3" />
|
<CheckCircle2 className="mr-1 h-3 w-3" />
|
||||||
Approve
|
Approve & advance
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -206,6 +207,7 @@ export function StageReviewPanel({
|
||||||
className="h-6 px-2 text-[12px]"
|
className="h-6 px-2 text-[12px]"
|
||||||
disabled={updateStageStatus.isPending}
|
disabled={updateStageStatus.isPending}
|
||||||
onClick={handleRequestChanges}
|
onClick={handleRequestChanges}
|
||||||
|
title="Mark this stage as needing changes; producer will iterate"
|
||||||
>
|
>
|
||||||
<XCircle className="mr-1 h-3 w-3" />
|
<XCircle className="mr-1 h-3 w-3" />
|
||||||
Request changes
|
Request changes
|
||||||
|
|
|
||||||
|
|
@ -403,11 +403,12 @@ export async function updateStageStatus(
|
||||||
downstreamStageNames: [] as string[],
|
downstreamStageNames: [] as string[],
|
||||||
};
|
};
|
||||||
|
|
||||||
// Check if this will unblock downstream stages
|
// Check if this will auto-advance downstream stages. Phase 5b: ALL
|
||||||
if (
|
// terminal transitions (APPROVED / DELIVERED / SKIPPED) auto-open
|
||||||
(newStatus === "APPROVED" || newStatus === "DELIVERED") &&
|
// every stage whose prerequisites are now satisfied — not just
|
||||||
stage.template.isCriticalGate
|
// 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) => ({
|
const allStages = stage.deliverable.stages.map((s) => ({
|
||||||
id: s.id,
|
id: s.id,
|
||||||
status: s.id === stageId ? newStatus : s.status,
|
status: s.id === stageId ? newStatus : s.status,
|
||||||
|
|
@ -462,8 +463,11 @@ export async function updateStageStatus(
|
||||||
include: { template: { include: { dependsOn: true } } },
|
include: { template: { include: { dependsOn: true } } },
|
||||||
});
|
});
|
||||||
|
|
||||||
// If this stage was just APPROVED/DELIVERED and is a critical gate, unblock downstream
|
// Phase 5b: any terminal transition auto-advances every downstream
|
||||||
if ((newStatus === "APPROVED" || newStatus === "DELIVERED") && stage.template.isCriticalGate) {
|
// 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) => ({
|
const allStages = stage.deliverable.stages.map((s) => ({
|
||||||
id: s.id,
|
id: s.id,
|
||||||
status: s.id === stageId ? newStatus : s.status,
|
status: s.id === stageId ? newStatus : s.status,
|
||||||
|
|
@ -482,7 +486,7 @@ export async function updateStageStatus(
|
||||||
if (idsToUnblock.length > 0) {
|
if (idsToUnblock.length > 0) {
|
||||||
await tx.deliverableStage.updateMany({
|
await tx.deliverableStage.updateMany({
|
||||||
where: { id: { in: idsToUnblock } },
|
where: { id: { in: idsToUnblock } },
|
||||||
data: { status: "NOT_STARTED" },
|
data: { status: "IN_PROGRESS", startDate: now },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue