FN-5925: polish task review tab layout
Refine the task review tab layout for clearer hierarchy and better mobile behavior. - group the review summary and decision badge into a stacked header layout - reorganize review items with dedicated selection, status, and metadata regions - restyle review cards, status badges, auto-merge controls, and body containers for improved spacing and overflow handling - extend TaskReviewTab coverage for populated layout hooks and mobile CSS regressions Files changed: packages/dashboard/app/components/TaskReviewTab.css | 220 +++++++++++++++------ packages/dashboard/app/components/TaskReviewTab.tsx | 24 ++- packages/dashboard/app/components/__tests__/TaskReviewTab.test.tsx | 32 ++- 3 files changed, 206 insertions(+), 70 deletions(-) Fusion-Task-Id: FN-5925 Fusion-Task-Lineage: fdb9d882-abda-48b0-aed0-a1ff4262af8f
This commit is contained in:
@@ -1,110 +1,188 @@
|
||||
.task-review-tab {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-md);
|
||||
gap: var(--space-lg);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.task-review-tab__header {
|
||||
display: flex;
|
||||
gap: var(--space-md);
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: var(--space-lg);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.task-review-tab__summary-wrap {
|
||||
flex: 1 1 20rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.task-review-tab__summary-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-sm);
|
||||
flex-wrap: wrap;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: var(--space-xs);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.task-review-tab__summary {
|
||||
margin: 0;
|
||||
color: var(--text-muted);
|
||||
color: var(--text);
|
||||
font-family: var(--font-primary);
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.5;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.task-review-tab__decision {
|
||||
.task-review-tab__decision,
|
||||
.task-review-tab__status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
inline-size: fit-content;
|
||||
min-width: 0;
|
||||
padding: 0 var(--space-sm);
|
||||
min-height: calc(var(--space-lg) + var(--space-xs));
|
||||
border-radius: var(--radius-pill);
|
||||
border: var(--btn-border-width) solid transparent;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
line-height: 1.2;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.task-review-tab__decision--APPROVED {
|
||||
.task-review-tab__decision--APPROVED,
|
||||
.task-review-tab__status--addressed {
|
||||
color: var(--color-success);
|
||||
background: color-mix(in srgb, var(--color-success) 14%, transparent);
|
||||
border-color: color-mix(in srgb, var(--color-success) 28%, transparent);
|
||||
}
|
||||
|
||||
.task-review-tab__decision--CHANGES_REQUESTED {
|
||||
.task-review-tab__decision--CHANGES_REQUESTED,
|
||||
.task-review-tab__status--failed {
|
||||
color: var(--color-error);
|
||||
background: color-mix(in srgb, var(--color-error) 14%, transparent);
|
||||
border-color: color-mix(in srgb, var(--color-error) 28%, transparent);
|
||||
}
|
||||
|
||||
.task-review-tab__decision--COMMENTED,
|
||||
.task-review-tab__decision--PENDING,
|
||||
.task-review-tab__decision--REVIEW_REQUIRED {
|
||||
.task-review-tab__decision--REVIEW_REQUIRED,
|
||||
.task-review-tab__status--queued,
|
||||
.task-review-tab__status--in-progress {
|
||||
color: var(--color-warning);
|
||||
background: color-mix(in srgb, var(--color-warning) 16%, transparent);
|
||||
border-color: color-mix(in srgb, var(--color-warning) 30%, transparent);
|
||||
}
|
||||
|
||||
.task-review-tab__actions {
|
||||
display: flex;
|
||||
gap: var(--space-sm);
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-end;
|
||||
gap: var(--space-sm);
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
|
||||
.task-review-tab__actions .btn {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.task-review-tab__auto-merge-control {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2xs);
|
||||
min-inline-size: max-content;
|
||||
gap: var(--space-xs);
|
||||
min-inline-size: 14rem;
|
||||
padding: var(--space-sm);
|
||||
border: var(--btn-border-width) solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
background: color-mix(in srgb, var(--surface) 72%, var(--card));
|
||||
}
|
||||
|
||||
.task-review-tab__list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-sm);
|
||||
gap: var(--space-md);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.task-review-tab__item {
|
||||
padding: var(--space-md);
|
||||
padding: var(--card-padding);
|
||||
}
|
||||
|
||||
.task-review-tab__item-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-sm);
|
||||
gap: var(--space-md);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.task-review-tab__row {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
.task-review-tab__direct-item {
|
||||
display: block;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.task-review-tab__direct-item--selectable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.task-review-tab__item-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-sm);
|
||||
align-items: start;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.task-review-tab__item-selection {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--space-sm);
|
||||
min-width: 0;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.task-review-tab__item-selection input {
|
||||
margin: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.task-review-tab__item-summary {
|
||||
color: var(--text);
|
||||
font-family: var(--font-primary);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.5;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.task-review-tab__direct-item {
|
||||
.task-review-tab__item-meta-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-sm);
|
||||
gap: var(--space-xs);
|
||||
}
|
||||
|
||||
.task-review-tab__body {
|
||||
margin: 0;
|
||||
padding: var(--space-sm);
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
padding: var(--space-md);
|
||||
border: var(--btn-border-width) solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
font-family: var(--font-primary);
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
max-width: 100%;
|
||||
font-family: var(--font-primary);
|
||||
overflow-wrap: anywhere;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.task-review-tab__body.markdown-body {
|
||||
@@ -124,22 +202,12 @@
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.task-review-tab__status {
|
||||
color: var(--text-muted);
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.task-review-tab__status--failed {
|
||||
color: var(--color-error);
|
||||
}
|
||||
|
||||
.task-review-tab__status--queued,
|
||||
.task-review-tab__status--in-progress {
|
||||
color: var(--color-warning);
|
||||
}
|
||||
|
||||
.task-review-tab__status--addressed {
|
||||
color: var(--color-success);
|
||||
.task-review-tab__meta,
|
||||
.task-review-tab__empty,
|
||||
.task-review-tab__error {
|
||||
font-family: var(--font-primary);
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.task-review-tab__meta,
|
||||
@@ -147,18 +215,35 @@
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.task-review-tab__refresh-meta {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
.task-review-tab__refresh-meta,
|
||||
.task-review-tab__empty,
|
||||
.task-review-tab__error {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--space-sm);
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
border: var(--btn-border-width) solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
background: color-mix(in srgb, var(--surface) 72%, var(--card));
|
||||
}
|
||||
|
||||
.task-review-tab__refresh-meta {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.task-review-tab__refresh-meta .status-dot {
|
||||
flex-shrink: 0;
|
||||
margin-top: var(--space-xs);
|
||||
}
|
||||
|
||||
.task-review-tab__empty {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.task-review-tab__error {
|
||||
color: var(--color-error);
|
||||
border-color: color-mix(in srgb, var(--color-error) 26%, var(--border));
|
||||
background: color-mix(in srgb, var(--color-error) 12%, var(--surface));
|
||||
}
|
||||
|
||||
.task-review-tab__reviewers,
|
||||
@@ -173,27 +258,52 @@
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.task-review-tab__row {
|
||||
grid-template-columns: auto 1fr;
|
||||
.task-review-tab {
|
||||
gap: var(--space-md);
|
||||
}
|
||||
|
||||
.task-review-tab__status {
|
||||
grid-column: 2;
|
||||
.task-review-tab__header {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: var(--space-md);
|
||||
}
|
||||
|
||||
.task-review-tab__summary-wrap,
|
||||
.task-review-tab__actions,
|
||||
.task-review-tab__auto-merge-control {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.task-review-tab__actions {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
gap: var(--space-sm);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.task-review-tab__auto-merge-control {
|
||||
.task-review-tab__actions .btn {
|
||||
width: 100%;
|
||||
min-inline-size: 0;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.task-review-tab__auto-merge-control .select {
|
||||
width: 100%;
|
||||
.task-review-tab__item,
|
||||
.task-review-tab__body,
|
||||
.task-review-tab__refresh-meta,
|
||||
.task-review-tab__empty,
|
||||
.task-review-tab__error {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.task-review-tab__item-header {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.task-review-tab__status,
|
||||
.task-review-tab__decision {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.task-review-tab__body {
|
||||
padding: var(--space-sm);
|
||||
}
|
||||
|
||||
.task-review-tab__refresh-meta {
|
||||
|
||||
@@ -314,8 +314,10 @@ export function TaskReviewTab({
|
||||
<div className="task-review-tab">
|
||||
<div className="task-review-tab__header">
|
||||
<div className="task-review-tab__summary-wrap">
|
||||
<p className="task-review-tab__summary">{summaryText}</p>
|
||||
{decisionLabel ? <span className={`task-review-tab__decision task-review-tab__decision--${decisionLabel}`}>{decisionLabel}</span> : null}
|
||||
<div className="task-review-tab__summary-group">
|
||||
<p className="task-review-tab__summary">{summaryText}</p>
|
||||
{decisionLabel ? <span className={`task-review-tab__decision task-review-tab__decision--${decisionLabel}`}>{decisionLabel}</span> : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className="task-review-tab__actions">
|
||||
<div className="task-review-tab__auto-merge-control">
|
||||
@@ -373,16 +375,20 @@ export function TaskReviewTab({
|
||||
<li key={item.id} className="task-review-tab__item card">
|
||||
<div className="task-review-tab__item-inner">
|
||||
<label htmlFor={checkboxId} className="task-review-tab__direct-item task-review-tab__direct-item--selectable">
|
||||
<div className="task-review-tab__summary-wrap">
|
||||
<input id={checkboxId} type="checkbox" checked={selected.includes(item.id)} onChange={() => toggleSelected(item.id)} />
|
||||
<span className="task-review-tab__item-summary">{item.path ? `${item.path}: ` : ""}{item.summary}</span>
|
||||
<div className="task-review-tab__item-header">
|
||||
<div className="task-review-tab__item-selection">
|
||||
<input id={checkboxId} type="checkbox" checked={selected.includes(item.id)} onChange={() => toggleSelected(item.id)} />
|
||||
<span className="task-review-tab__item-summary">{item.path ? `${item.path}: ` : ""}{item.summary}</span>
|
||||
</div>
|
||||
<span className={`task-review-tab__status task-review-tab__status--${item.status}`}>{item.status}</span>
|
||||
</div>
|
||||
</label>
|
||||
<div className="task-review-tab__meta">{formatTimestamp(item.createdAt)}</div>
|
||||
{item.addressing ? (
|
||||
<div className="task-review-tab__meta">Selected: {formatTimestamp(item.addressing.selectedAt)}{item.addressing.startedAt ? ` · Started: ${formatTimestamp(item.addressing.startedAt)}` : ""}{item.addressing.completedAt ? ` · Completed: ${formatTimestamp(item.addressing.completedAt)}` : ""}{item.addressing.error ? ` · Error: ${item.addressing.error}` : ""}</div>
|
||||
) : null}
|
||||
<div className="task-review-tab__item-meta-list">
|
||||
<div className="task-review-tab__meta">{formatTimestamp(item.createdAt)}</div>
|
||||
{item.addressing ? (
|
||||
<div className="task-review-tab__meta">Selected: {formatTimestamp(item.addressing.selectedAt)}{item.addressing.startedAt ? ` · Started: ${formatTimestamp(item.addressing.startedAt)}` : ""}{item.addressing.completedAt ? ` · Completed: ${formatTimestamp(item.addressing.completedAt)}` : ""}{item.addressing.error ? ` · Error: ${item.addressing.error}` : ""}</div>
|
||||
) : null}
|
||||
</div>
|
||||
{renderMarkdown ? (
|
||||
<div className="task-review-tab__body markdown-body">
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
|
||||
|
||||
@@ -125,7 +125,7 @@ describe("TaskReviewTab", () => {
|
||||
expect(await screen.findByText("Failed to load review data.")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders PR decision and status modifiers", async () => {
|
||||
it("renders PR decision, status modifiers, and populated layout hooks", async () => {
|
||||
const task = makeTask({
|
||||
reviewState: {
|
||||
source: "pull-request",
|
||||
@@ -136,6 +136,8 @@ describe("TaskReviewTab", () => {
|
||||
body: "Fix null handling",
|
||||
author: { login: "reviewer" },
|
||||
createdAt: new Date().toISOString(),
|
||||
path: "src/parser.ts",
|
||||
summary: "Parser guard is missing",
|
||||
},
|
||||
],
|
||||
addressing: [{ itemId: "ri-1", status: "failed", selectedAt: new Date().toISOString() }],
|
||||
@@ -143,9 +145,19 @@ describe("TaskReviewTab", () => {
|
||||
});
|
||||
|
||||
apiMocks.fetchTaskReview.mockResolvedValue({ reviewState: task.reviewState, automationStatus: null, emptyMessage: null });
|
||||
render(<TaskReviewTab task={task} addToast={vi.fn()} />);
|
||||
const { container } = render(<TaskReviewTab task={task} addToast={vi.fn()} />);
|
||||
|
||||
await screen.findByText("CHANGES_REQUESTED");
|
||||
expect(screen.getByText("failed").className).toContain("task-review-tab__status--failed");
|
||||
expect(container.querySelector(".task-review-tab__header")).not.toBeNull();
|
||||
expect(container.querySelector(".task-review-tab__summary-group")).not.toBeNull();
|
||||
expect(container.querySelector(".task-review-tab__actions")).not.toBeNull();
|
||||
expect(container.querySelector(".task-review-tab__refresh-meta")).not.toBeNull();
|
||||
expect(container.querySelector(".task-review-tab__list")).not.toBeNull();
|
||||
expect(container.querySelector(".task-review-tab__item-header")).not.toBeNull();
|
||||
expect(container.querySelector(".task-review-tab__item-selection")).not.toBeNull();
|
||||
expect(container.querySelector(".task-review-tab__item-meta-list")).not.toBeNull();
|
||||
expect(container.querySelector(".task-review-tab__body")).not.toBeNull();
|
||||
});
|
||||
|
||||
it("keeps review body outside the checkbox label and preserves selection on body clicks", async () => {
|
||||
@@ -369,7 +381,7 @@ describe("TaskReviewTab", () => {
|
||||
expect(addToast).toHaveBeenCalledWith("Review refreshed", "success");
|
||||
});
|
||||
|
||||
it("renders reviewer-agent entries in direct mode", async () => {
|
||||
it("renders reviewer-agent entries in direct mode with populated layout hooks", async () => {
|
||||
const task = makeTask();
|
||||
apiMocks.fetchTaskReview.mockResolvedValue({
|
||||
reviewState: {
|
||||
@@ -393,9 +405,11 @@ describe("TaskReviewTab", () => {
|
||||
emptyMessage: null,
|
||||
});
|
||||
|
||||
render(<TaskReviewTab task={task} addToast={vi.fn()} />);
|
||||
const { container } = render(<TaskReviewTab task={task} addToast={vi.fn()} />);
|
||||
expect(await screen.findByText("code review Step 2: REVISE")).toBeInTheDocument();
|
||||
expect(screen.getAllByText("REVISE").length).toBeGreaterThan(0);
|
||||
expect(container.querySelector(".task-review-tab__item-header")).not.toBeNull();
|
||||
expect(container.querySelector(".task-review-tab__item-meta-list")).not.toBeNull();
|
||||
});
|
||||
|
||||
it("renders all persisted addressing progress states from snapshots", async () => {
|
||||
@@ -485,14 +499,20 @@ describe("TaskReviewTab", () => {
|
||||
expect(screen.getByText(/Error: Patch failed/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("keeps mobile actions wrapping contract and removes equal-width flex buttons", async () => {
|
||||
it("keeps mobile actions wrapping contract, stacks header groups, and prevents body overflow regressions", async () => {
|
||||
const css = await loadAllAppCss();
|
||||
const mobileMediaStart = css.indexOf("@media (max-width: 768px)");
|
||||
expect(mobileMediaStart).toBeGreaterThanOrEqual(0);
|
||||
const mobileCss = css.slice(mobileMediaStart);
|
||||
|
||||
expect(mobileCss).toMatch(/\.task-review-tab__actions\s*\{[^}]*flex-wrap\s*:\s*wrap\s*;[^}]*\}/);
|
||||
expect(mobileCss).toMatch(/\.task-review-tab__header\s*\{[^}]*flex-direction\s*:\s*column\s*;[^}]*\}/);
|
||||
expect(mobileCss).toMatch(/\.task-review-tab__actions\s*\{[^}]*justify-content\s*:\s*flex-start\s*;[^}]*\}/);
|
||||
expect(mobileCss).toMatch(/\.task-review-tab__actions\s+\.btn\s*\{[^}]*width\s*:\s*100%\s*;[^}]*\}/);
|
||||
expect(mobileCss).toMatch(/\.task-review-tab__body\s*\{[^}]*padding\s*:\s*var\(--space-sm\)\s*;[^}]*\}/);
|
||||
expect(mobileCss).not.toMatch(/\.task-review-tab__actions\s+\.btn\s*\{[^}]*flex\s*:\s*1\s*;[^}]*\}/);
|
||||
|
||||
expect(css).toMatch(/\.task-review-tab__body\s*\{[^}]*overflow-x\s*:\s*auto\s*;[^}]*overflow-wrap\s*:\s*anywhere\s*;[^}]*\}/);
|
||||
expect(css).toMatch(/\.task-review-tab__item\s*\{[^}]*padding\s*:\s*var\(--card-padding\)\s*;[^}]*\}/);
|
||||
});
|
||||
|
||||
it("shows create PR action when in-review without prInfo and auth is available", async () => {
|
||||
|
||||
Reference in New Issue
Block a user