feat(FN-5589): merge fusion/fn-5589
This commit is contained in:
@@ -360,8 +360,20 @@
|
||||
margin-top: var(--space-lg);
|
||||
}
|
||||
|
||||
.detail-pr-tab {
|
||||
display: grid;
|
||||
gap: var(--space-lg);
|
||||
}
|
||||
|
||||
.detail-pr-tab .detail-section,
|
||||
.detail-pr-section {
|
||||
margin-top: var(--space-lg);
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.detail-pr-tab {
|
||||
gap: var(--space-md);
|
||||
}
|
||||
}
|
||||
|
||||
.detail-provider-icons {
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -305,7 +305,7 @@ describe("TaskDetailModal", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("wraps in-review PR content in a spaced detail section after dependencies", () => {
|
||||
it("renders in-review PR content only in the Pull Request tab body", () => {
|
||||
const { container } = render(
|
||||
<TaskDetailModal
|
||||
task={makeTask({ column: "in-review", status: "creating-pr", dependencies: ["FN-001"] })}
|
||||
@@ -318,20 +318,19 @@ describe("TaskDetailModal", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const depsSection = container.querySelector(".detail-deps");
|
||||
const blockingSection = container.querySelector(".detail-deps.detail-blocking");
|
||||
const prSection = container.querySelector(".detail-pr-section");
|
||||
expect(container.querySelector(".detail-pr-section")).toBeNull();
|
||||
|
||||
expect(depsSection).toBeTruthy();
|
||||
expect(blockingSection).toBeTruthy();
|
||||
fireEvent.click(screen.getByRole("button", { name: "Pull Request" }));
|
||||
|
||||
const prSection = container.querySelector(".detail-pr-section");
|
||||
expect(prSection).toBeTruthy();
|
||||
expect(blockingSection?.nextElementSibling).toBe(prSection);
|
||||
expect(prSection?.querySelector(".pr-section")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("defines tokenized margin on detail-pr-section spacing contract", () => {
|
||||
it("defines tokenized spacing rules for detail-pr-tab layout", () => {
|
||||
const css = readDashboardStylesSource();
|
||||
expectBaseRule(css, ".detail-pr-section", "margin-top: var(--space-lg);");
|
||||
expectBaseRule(css, ".detail-pr-tab", "gap: var(--space-lg);");
|
||||
expectBaseRule(css, ".detail-pr-tab .detail-section,\n.detail-pr-section", "margin-top: 0;");
|
||||
});
|
||||
|
||||
it("activity list does not have nested scroll constraints", () => {
|
||||
|
||||
@@ -181,6 +181,7 @@ describe("TaskDetailModal create-PR integration wiring", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Pull Request" }));
|
||||
const panelCreateButton = await screen.findByTestId("pr-panel-create-pr");
|
||||
expect(screen.queryByRole("dialog", { name: "Create Pull Request" })).toBeNull();
|
||||
|
||||
|
||||
@@ -111,6 +111,7 @@ describe("TaskDetailModal create-PR wiring", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Pull Request" }));
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("button", { name: "Create PR" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("../PrPanel", () => ({
|
||||
PrPanel: () => <div data-testid="pr-panel-stub">PR Panel</div>,
|
||||
}));
|
||||
|
||||
import {
|
||||
makeTask,
|
||||
noop,
|
||||
noopDelete,
|
||||
noopMerge,
|
||||
noopMove,
|
||||
noopOpenDetail,
|
||||
setupTaskDetailModalHooks,
|
||||
} from "./TaskDetailModal.test-helpers";
|
||||
import { TaskDetailModal } from "../TaskDetailModal";
|
||||
|
||||
setupTaskDetailModalHooks();
|
||||
|
||||
describe("TaskDetailModal Pull Request tab", () => {
|
||||
it("shows Pull Request tab only for in-review tasks", () => {
|
||||
const { rerender } = render(
|
||||
<TaskDetailModal
|
||||
task={makeTask({ column: "todo" })}
|
||||
onClose={noop}
|
||||
onMoveTask={noopMove}
|
||||
onDeleteTask={noopDelete}
|
||||
onMergeTask={noopMerge}
|
||||
onOpenDetail={noopOpenDetail}
|
||||
addToast={noop}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByRole("button", { name: "Pull Request" })).toBeNull();
|
||||
|
||||
rerender(
|
||||
<TaskDetailModal
|
||||
task={makeTask({ column: "in-review" })}
|
||||
onClose={noop}
|
||||
onMoveTask={noopMove}
|
||||
onDeleteTask={noopDelete}
|
||||
onMergeTask={noopMerge}
|
||||
onOpenDetail={noopOpenDetail}
|
||||
addToast={noop}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByRole("button", { name: "Pull Request" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders PrPanel and in-review stall badge in Pull Request tab, not Definition tab", () => {
|
||||
const inReviewStall = {
|
||||
code: "merge-failed" as const,
|
||||
reason: "merge failed",
|
||||
observedAt: "2026-01-01T00:00:00Z",
|
||||
consecutiveFailures: 2,
|
||||
mergeRetries: 2,
|
||||
maxAutoMergeRetries: 3,
|
||||
};
|
||||
|
||||
const { container } = render(
|
||||
<TaskDetailModal
|
||||
task={makeTask({ column: "in-review", inReviewStall })}
|
||||
onClose={noop}
|
||||
onMoveTask={noopMove}
|
||||
onDeleteTask={noopDelete}
|
||||
onMergeTask={noopMerge}
|
||||
onOpenDetail={noopOpenDetail}
|
||||
addToast={noop}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId("pr-panel-stub")).toBeNull();
|
||||
expect(container.querySelector(".detail-in-review-stall")).toBeNull();
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Pull Request" }));
|
||||
|
||||
expect(screen.getByTestId("pr-panel-stub")).toBeInTheDocument();
|
||||
expect(container.querySelector(".detail-in-review-stall")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -146,6 +146,8 @@ describe("TaskDetailModal in-review stall diagnostics", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Pull Request" }));
|
||||
|
||||
expect(screen.getByText("Merge blocked by a pre-merge check")).toBeInTheDocument();
|
||||
expect(screen.getByText("Workflow pre-merge check failed")).toBeInTheDocument();
|
||||
expect(screen.getByText("Open the Review tab to see which step is blocking, then fix the failure or override the step.")).toBeInTheDocument();
|
||||
@@ -157,7 +159,8 @@ describe("TaskDetailModal in-review stall diagnostics", () => {
|
||||
expect(highlighted?.textContent).toContain("In-review stall surfaced [merge-blocker]");
|
||||
});
|
||||
|
||||
it("renders retry-exhausted badge label with counter", () => {
|
||||
it("renders retry-exhausted badge label with counter", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<TaskDetailModal
|
||||
task={makeTask({
|
||||
@@ -178,10 +181,12 @@ describe("TaskDetailModal in-review stall diagnostics", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Pull Request" }));
|
||||
expect(screen.getByText("Retries exhausted 3/3")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows no-log copy when no matching stall entry exists", () => {
|
||||
it("shows no-log copy when no matching stall entry exists", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<TaskDetailModal
|
||||
task={makeTask({
|
||||
@@ -202,6 +207,7 @@ describe("TaskDetailModal in-review stall diagnostics", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Pull Request" }));
|
||||
expect(screen.getByText("No log entry yet")).toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "View activity log" })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ export type DetailTaskTab =
|
||||
| "comments"
|
||||
| "model"
|
||||
| "workflow"
|
||||
| "pr"
|
||||
| "retries";
|
||||
|
||||
export type DetailTaskOrigin = "list-mobile";
|
||||
|
||||
@@ -12,7 +12,7 @@ import type { ReactNode } from "react";
|
||||
import type { Task, TaskDetail, WorkflowStep } from "@fusion/core";
|
||||
|
||||
/** Tab identifiers for the task detail modal. Mirrors the dashboard's local enum. */
|
||||
export type DetailTaskTab = "definition" | "logs" | "changes" | "comments" | "model" | "workflow" | "retries";
|
||||
export type DetailTaskTab = "definition" | "logs" | "changes" | "comments" | "model" | "workflow" | "pr" | "retries";
|
||||
|
||||
export type PluginToastType = "success" | "error" | "warning" | "info";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user