feat(FN-5589): merge fusion/fn-5589
This commit is contained in:
@@ -275,7 +275,7 @@ function formatDurationCompact(ageMs: number): string {
|
||||
return `${minutes}m`;
|
||||
}
|
||||
|
||||
type TabId = "definition" | "logs" | "changes" | "review" | "comments" | "model" | "workflow" | "documents" | "stats" | "routing" | "retries" | `plugin-${string}`;
|
||||
type TabId = "definition" | "logs" | "changes" | "review" | "pr" | "comments" | "model" | "workflow" | "documents" | "stats" | "routing" | "retries" | `plugin-${string}`;
|
||||
|
||||
export interface TaskDetailModalProps {
|
||||
task: Task | TaskDetail;
|
||||
@@ -556,6 +556,12 @@ export function TaskDetailContent({
|
||||
}
|
||||
}, [initialTab]);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTab === "pr" && task.column !== "in-review") {
|
||||
setActiveTab("definition");
|
||||
}
|
||||
}, [activeTab, task.column]);
|
||||
|
||||
// Reset description expanded state when task changes
|
||||
useEffect(() => {
|
||||
setDescriptionExpanded(false);
|
||||
@@ -2577,6 +2583,14 @@ export function TaskDetailContent({
|
||||
>
|
||||
Review
|
||||
</button>
|
||||
{task.column === "in-review" && (
|
||||
<button
|
||||
className={`detail-tab${activeTab === "pr" ? " detail-tab-active" : ""}`}
|
||||
onClick={() => setActiveTab("pr")}
|
||||
>
|
||||
Pull Request
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className={`detail-tab${activeTab === "comments" ? " detail-tab-active" : ""}`}
|
||||
onClick={() => setActiveTab("comments")}
|
||||
@@ -2730,6 +2744,135 @@ export function TaskDetailContent({
|
||||
autoMergeEnabled={autoMergeEnabled}
|
||||
onRequestCreatePr={() => setPrCreateOpen(true)}
|
||||
/>
|
||||
) : activeTab === "pr" ? (
|
||||
<div className="detail-section detail-pr-tab">
|
||||
{task.column === "in-review" && (
|
||||
<>
|
||||
{shouldShowInReviewStallBadge(workingTask) && workingTask.inReviewStall && (() => {
|
||||
const copy = getInReviewStallCopy(workingTask.inReviewStall, {
|
||||
mergeRetries: workingTask.mergeRetries,
|
||||
maxAutoMergeRetries: MAX_AUTO_MERGE_RETRIES,
|
||||
});
|
||||
const logMatch = findInReviewStallLogEntry(workingTask, workingTask.inReviewStall.code);
|
||||
return (
|
||||
<div
|
||||
className={`detail-section detail-in-review-stall detail-in-review-stall--${copy.code}`}
|
||||
data-stall-code={copy.code}
|
||||
>
|
||||
<div className="detail-in-review-stall-header">
|
||||
<span className="card-status-badge card-status-badge--in-review in-review-stall">
|
||||
{copy.badgeLabel}{copy.counter ? ` ${copy.counter}` : ""}
|
||||
</span>
|
||||
<span className="detail-in-review-stall-headline">{copy.headline}</span>
|
||||
</div>
|
||||
<div className="detail-in-review-stall-reason">{workingTask.inReviewStall.reason}</div>
|
||||
<div className="detail-in-review-stall-description">{copy.description}</div>
|
||||
<div className="detail-in-review-stall-action">{copy.suggestedAction}</div>
|
||||
<div className="detail-in-review-stall-meta">
|
||||
<span>Observed {formatTimestamp(workingTask.inReviewStall.observedAt)}</span>
|
||||
{logMatch ? (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm detail-in-review-stall-jump"
|
||||
onClick={() => {
|
||||
setActiveTab("logs");
|
||||
setLogSubview("activity");
|
||||
setHighlightStallCode(workingTask.inReviewStall?.code ?? null);
|
||||
}}
|
||||
>
|
||||
View activity log
|
||||
</button>
|
||||
) : (
|
||||
<span
|
||||
className="detail-in-review-stall-no-log"
|
||||
title="No 'In-review stall surfaced' entry on this task yet — self-healing may not have logged one within its rate-limit window."
|
||||
>
|
||||
No log entry yet
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
{shouldShowStalePausedReviewBadge(workingTask) && workingTask.stalePausedReview && (() => {
|
||||
const copy = getStalePausedReviewCopy(workingTask.stalePausedReview);
|
||||
const logMatch = [...(workingTask.log ?? [])].reverse().find((entry) => {
|
||||
const match = entry.action.match(STALE_PAUSED_REVIEW_LOG_REGEX);
|
||||
return match?.[1] === workingTask.stalePausedReview?.code;
|
||||
});
|
||||
return (
|
||||
<div
|
||||
className={`detail-section detail-in-review-stall detail-in-review-stall--${copy.code}`}
|
||||
data-stall-code={copy.code}
|
||||
>
|
||||
<div className="detail-in-review-stall-header">
|
||||
<span className="card-status-badge card-status-badge--in-review stale-paused-review">
|
||||
{copy.badgeLabel}
|
||||
</span>
|
||||
<span className="detail-in-review-stall-headline">{copy.headline}</span>
|
||||
</div>
|
||||
<div className="detail-in-review-stall-reason">{workingTask.stalePausedReview.reason}</div>
|
||||
<div className="detail-in-review-stall-description">{copy.description}</div>
|
||||
<div className="detail-in-review-stall-action">{copy.suggestedAction}</div>
|
||||
<div className="detail-in-review-stall-meta">
|
||||
<span>Age {formatDurationCompact(workingTask.stalePausedReview.ageMs)}</span>
|
||||
<span>Threshold {formatDurationCompact(workingTask.stalePausedReview.thresholdMs)}</span>
|
||||
<span>Observed {formatTimestamp(workingTask.stalePausedReview.observedAt)}</span>
|
||||
{logMatch ? (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm detail-in-review-stall-jump"
|
||||
onClick={() => {
|
||||
setActiveTab("logs");
|
||||
setLogSubview("activity");
|
||||
setHighlightStallCode(workingTask.stalePausedReview?.code ?? null);
|
||||
}}
|
||||
>
|
||||
View activity log
|
||||
</button>
|
||||
) : (
|
||||
<span className="detail-in-review-stall-no-log">No log entry yet</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
<div className="detail-section detail-pr-section">
|
||||
<PrPanel
|
||||
taskId={task.id}
|
||||
projectId={projectId}
|
||||
prInfo={task.prInfo}
|
||||
prInfos={task.prInfos}
|
||||
automationStatus={task.status ?? null}
|
||||
taskColumn={task.column}
|
||||
autoMerge={settings?.autoMerge ?? false}
|
||||
isManualPrFlow={isManualPrFlow}
|
||||
directMergeCommitStrategy={settings?.directMergeCommitStrategy}
|
||||
prAuthAvailable={prAuthAvailable ?? false}
|
||||
onRequestCreatePr={() => setPrCreateOpen(true)}
|
||||
onPrUpdated={(prInfo) => {
|
||||
const existing = task.prInfos ?? (task.prInfo ? [task.prInfo] : []);
|
||||
const nextPrInfos = existing.some((entry) => entry.number === prInfo.number)
|
||||
? existing.map((entry) => (entry.number === prInfo.number ? prInfo : entry))
|
||||
: [...existing, prInfo];
|
||||
(task as TaskDetail).prInfos = nextPrInfos;
|
||||
(task as TaskDetail).prInfo = nextPrInfos[0] ?? prInfo;
|
||||
}}
|
||||
onPrsRefreshed={(prInfos) => {
|
||||
(task as TaskDetail).prInfos = prInfos;
|
||||
(task as TaskDetail).prInfo = prInfos[0];
|
||||
}}
|
||||
onPrUnlinked={(prNumber) => {
|
||||
const nextPrInfos = (task.prInfos ?? (task.prInfo ? [task.prInfo] : [])).filter((entry) => entry.number !== prNumber);
|
||||
(task as TaskDetail).prInfos = nextPrInfos;
|
||||
(task as TaskDetail).prInfo = nextPrInfos[0];
|
||||
}}
|
||||
addToast={addToast}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
) : activeTab === "comments" ? (
|
||||
<TaskComments task={task} addToast={addToast} projectId={projectId} onTaskUpdated={onTaskUpdated} />
|
||||
) : activeTab === "documents" ? (
|
||||
@@ -3424,133 +3567,6 @@ export function TaskDetailContent({
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
{/* PR Section - only for in-review tasks */}
|
||||
{task.column === "in-review" && (
|
||||
<>
|
||||
{shouldShowInReviewStallBadge(workingTask) && workingTask.inReviewStall && (() => {
|
||||
const copy = getInReviewStallCopy(workingTask.inReviewStall, {
|
||||
mergeRetries: workingTask.mergeRetries,
|
||||
maxAutoMergeRetries: MAX_AUTO_MERGE_RETRIES,
|
||||
});
|
||||
const logMatch = findInReviewStallLogEntry(workingTask, workingTask.inReviewStall.code);
|
||||
return (
|
||||
<div
|
||||
className={`detail-section detail-in-review-stall detail-in-review-stall--${copy.code}`}
|
||||
data-stall-code={copy.code}
|
||||
>
|
||||
<div className="detail-in-review-stall-header">
|
||||
<span className="card-status-badge card-status-badge--in-review in-review-stall">
|
||||
{copy.badgeLabel}{copy.counter ? ` ${copy.counter}` : ""}
|
||||
</span>
|
||||
<span className="detail-in-review-stall-headline">{copy.headline}</span>
|
||||
</div>
|
||||
<div className="detail-in-review-stall-reason">{workingTask.inReviewStall.reason}</div>
|
||||
<div className="detail-in-review-stall-description">{copy.description}</div>
|
||||
<div className="detail-in-review-stall-action">{copy.suggestedAction}</div>
|
||||
<div className="detail-in-review-stall-meta">
|
||||
<span>Observed {formatTimestamp(workingTask.inReviewStall.observedAt)}</span>
|
||||
{logMatch ? (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm detail-in-review-stall-jump"
|
||||
onClick={() => {
|
||||
setActiveTab("logs");
|
||||
setLogSubview("activity");
|
||||
setHighlightStallCode(workingTask.inReviewStall?.code ?? null);
|
||||
}}
|
||||
>
|
||||
View activity log
|
||||
</button>
|
||||
) : (
|
||||
<span
|
||||
className="detail-in-review-stall-no-log"
|
||||
title="No 'In-review stall surfaced' entry on this task yet — self-healing may not have logged one within its rate-limit window."
|
||||
>
|
||||
No log entry yet
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
{shouldShowStalePausedReviewBadge(workingTask) && workingTask.stalePausedReview && (() => {
|
||||
const copy = getStalePausedReviewCopy(workingTask.stalePausedReview);
|
||||
const logMatch = [...(workingTask.log ?? [])].reverse().find((entry) => {
|
||||
const match = entry.action.match(STALE_PAUSED_REVIEW_LOG_REGEX);
|
||||
return match?.[1] === workingTask.stalePausedReview?.code;
|
||||
});
|
||||
return (
|
||||
<div
|
||||
className={`detail-section detail-in-review-stall detail-in-review-stall--${copy.code}`}
|
||||
data-stall-code={copy.code}
|
||||
>
|
||||
<div className="detail-in-review-stall-header">
|
||||
<span className="card-status-badge card-status-badge--in-review stale-paused-review">
|
||||
{copy.badgeLabel}
|
||||
</span>
|
||||
<span className="detail-in-review-stall-headline">{copy.headline}</span>
|
||||
</div>
|
||||
<div className="detail-in-review-stall-reason">{workingTask.stalePausedReview.reason}</div>
|
||||
<div className="detail-in-review-stall-description">{copy.description}</div>
|
||||
<div className="detail-in-review-stall-action">{copy.suggestedAction}</div>
|
||||
<div className="detail-in-review-stall-meta">
|
||||
<span>Age {formatDurationCompact(workingTask.stalePausedReview.ageMs)}</span>
|
||||
<span>Threshold {formatDurationCompact(workingTask.stalePausedReview.thresholdMs)}</span>
|
||||
<span>Observed {formatTimestamp(workingTask.stalePausedReview.observedAt)}</span>
|
||||
{logMatch ? (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm detail-in-review-stall-jump"
|
||||
onClick={() => {
|
||||
setActiveTab("logs");
|
||||
setLogSubview("activity");
|
||||
setHighlightStallCode(workingTask.stalePausedReview?.code ?? null);
|
||||
}}
|
||||
>
|
||||
View activity log
|
||||
</button>
|
||||
) : (
|
||||
<span className="detail-in-review-stall-no-log">No log entry yet</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
<div className="detail-section detail-pr-section">
|
||||
<PrPanel
|
||||
taskId={task.id}
|
||||
projectId={projectId}
|
||||
prInfo={task.prInfo}
|
||||
prInfos={task.prInfos}
|
||||
automationStatus={task.status ?? null}
|
||||
taskColumn={task.column}
|
||||
autoMerge={settings?.autoMerge ?? false}
|
||||
isManualPrFlow={isManualPrFlow}
|
||||
directMergeCommitStrategy={settings?.directMergeCommitStrategy}
|
||||
prAuthAvailable={prAuthAvailable ?? false}
|
||||
onRequestCreatePr={() => setPrCreateOpen(true)}
|
||||
onPrUpdated={(prInfo) => {
|
||||
const existing = task.prInfos ?? (task.prInfo ? [task.prInfo] : []);
|
||||
const nextPrInfos = existing.some((entry) => entry.number === prInfo.number)
|
||||
? existing.map((entry) => (entry.number === prInfo.number ? prInfo : entry))
|
||||
: [...existing, prInfo];
|
||||
(task as TaskDetail).prInfos = nextPrInfos;
|
||||
(task as TaskDetail).prInfo = nextPrInfos[0] ?? prInfo;
|
||||
}}
|
||||
onPrsRefreshed={(prInfos) => {
|
||||
(task as TaskDetail).prInfos = prInfos;
|
||||
(task as TaskDetail).prInfo = prInfos[0];
|
||||
}}
|
||||
onPrUnlinked={(prNumber) => {
|
||||
const nextPrInfos = (task.prInfos ?? (task.prInfo ? [task.prInfo] : [])).filter((entry) => entry.number !== prNumber);
|
||||
(task as TaskDetail).prInfos = nextPrInfos;
|
||||
(task as TaskDetail).prInfo = nextPrInfos[0];
|
||||
}}
|
||||
addToast={addToast}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user