feat(FN-2948): merge fusion/fn-2948
- feat(FN-2948): complete Step 5 — apply review feedback and finalize delivery - test(FN-2948): complete Step 3 — update insights section visibility coverage - feat(FN-2948): complete Step 2 — enhance insight item readability - feat(FN-2948): complete Step 1 — hide empty insight sections - feat(FN-2939): merge fusion/fn-2939 - feat(FN-2946): merge fusion/fn-2946 Fusion-Task-Id: FN-2948
This commit is contained in:
@@ -1002,6 +1002,10 @@
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chat-session-delete-btn {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.chat-message--assistant .chat-message-render-toggle {
|
.chat-message--assistant .chat-message-render-toggle {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -213,14 +213,6 @@
|
|||||||
padding: var(--space-md);
|
padding: var(--space-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Empty section placeholder */
|
|
||||||
.insights-empty-section {
|
|
||||||
padding: var(--space-lg);
|
|
||||||
text-align: center;
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Insights list */
|
/* Insights list */
|
||||||
.insights-list {
|
.insights-list {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
@@ -236,6 +228,7 @@
|
|||||||
padding: var(--space-md);
|
padding: var(--space-md);
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
|
border-left: 3px solid var(--accent);
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
transition: border-color var(--transition-fast);
|
transition: border-color var(--transition-fast);
|
||||||
}
|
}
|
||||||
@@ -272,7 +265,7 @@
|
|||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
-webkit-line-clamp: 3;
|
-webkit-line-clamp: 4;
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
@@ -304,8 +297,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.insight-item-status--stale {
|
.insight-item-status--stale {
|
||||||
background: color-mix(in srgb, var(--warning) 15%, transparent);
|
background: color-mix(in srgb, var(--color-warning) 15%, transparent);
|
||||||
color: var(--warning);
|
color: var(--color-warning);
|
||||||
}
|
}
|
||||||
|
|
||||||
.insight-item-status--dismissed {
|
.insight-item-status--dismissed {
|
||||||
@@ -387,33 +380,6 @@
|
|||||||
min-height: 36px;
|
min-height: 36px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.post-onboarding-recommendations {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: stretch;
|
|
||||||
gap: var(--space-sm);
|
|
||||||
padding: var(--space-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.post-onboarding-recommendations__main {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.post-onboarding-recommendations__item {
|
|
||||||
align-items: flex-start;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.post-onboarding-recommendations .btn.btn-sm {
|
|
||||||
margin-left: auto;
|
|
||||||
min-height: calc(var(--space-lg) * 2 + var(--space-xs));
|
|
||||||
}
|
|
||||||
|
|
||||||
.post-onboarding-recommendations__dismiss {
|
|
||||||
align-self: flex-end;
|
|
||||||
min-width: calc(var(--space-lg) * 2 + var(--space-xs));
|
|
||||||
min-height: calc(var(--space-lg) * 2 + var(--space-xs));
|
|
||||||
}
|
|
||||||
|
|
||||||
/* InsightsView additional mobile refinements */
|
/* InsightsView additional mobile refinements */
|
||||||
.insights-view-actions .btn {
|
.insights-view-actions .btn {
|
||||||
min-height: 36px;
|
min-height: 36px;
|
||||||
@@ -428,9 +394,5 @@
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Chat session delete button - always visible on mobile */
|
|
||||||
.chat-session-delete-btn {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -164,72 +164,66 @@ export function InsightsView({ projectId, addToast, onClose, onCreateTask }: Ins
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="insights-section-content">
|
<div className="insights-section-content">
|
||||||
{section.items.length === 0 ? (
|
<ul className="insights-list">
|
||||||
<div className="insights-empty-section" data-testid={`insights-empty-${section.category}`}>
|
{section.items.map((insight) => {
|
||||||
<p>No insights in this category</p>
|
const dismissState = dismissStates.get(insight.id);
|
||||||
</div>
|
const createState = createTaskStates.get(insight.id);
|
||||||
) : (
|
const isDismissInFlight = dismissState?.running ?? false;
|
||||||
<ul className="insights-list">
|
const isCreateInFlight = createState?.running ?? false;
|
||||||
{section.items.map((insight) => {
|
|
||||||
const dismissState = dismissStates.get(insight.id);
|
|
||||||
const createState = createTaskStates.get(insight.id);
|
|
||||||
const isDismissInFlight = dismissState?.running ?? false;
|
|
||||||
const isCreateInFlight = createState?.running ?? false;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li key={insight.id} className="insight-item" data-insight-id={insight.id}>
|
<li key={insight.id} className="insight-item" data-insight-id={insight.id}>
|
||||||
<div className="insight-item-header">
|
<div className="insight-item-header">
|
||||||
<h4 className="insight-item-title">{insight.title}</h4>
|
<h4 className="insight-item-title">{insight.title}</h4>
|
||||||
<div className="insight-item-actions">
|
<div className="insight-item-actions">
|
||||||
<button
|
<button
|
||||||
className="btn btn-sm btn-icon"
|
className="btn btn-sm btn-icon"
|
||||||
onClick={() => void handleCreateTask(insight.id, insight.title)}
|
onClick={() => void handleCreateTask(insight.id, insight.title)}
|
||||||
disabled={isCreateInFlight || isAnyActionInFlight}
|
disabled={isCreateInFlight || isAnyActionInFlight}
|
||||||
title="Create task from this insight"
|
title="Create task from this insight"
|
||||||
aria-label="Create task from this insight"
|
aria-label="Create task from this insight"
|
||||||
data-testid={`create-task-${insight.id}`}
|
data-testid={`create-task-${insight.id}`}
|
||||||
>
|
>
|
||||||
{isCreateInFlight ? (
|
{isCreateInFlight ? (
|
||||||
<RefreshCw size={14} className="spin" />
|
<RefreshCw size={14} className="spin" />
|
||||||
) : (
|
) : (
|
||||||
<Plus size={14} />
|
<Plus size={14} />
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="btn btn-sm btn-icon"
|
className="btn btn-sm btn-icon"
|
||||||
onClick={() => void handleDismiss(insight.id, insight.title)}
|
onClick={() => void handleDismiss(insight.id, insight.title)}
|
||||||
disabled={isDismissInFlight || isAnyActionInFlight}
|
disabled={isDismissInFlight || isAnyActionInFlight}
|
||||||
title="Dismiss this insight"
|
title="Dismiss this insight"
|
||||||
aria-label="Dismiss this insight"
|
aria-label="Dismiss this insight"
|
||||||
data-testid={`dismiss-${insight.id}`}
|
data-testid={`dismiss-${insight.id}`}
|
||||||
>
|
>
|
||||||
{isDismissInFlight ? (
|
{isDismissInFlight ? (
|
||||||
<RefreshCw size={14} className="spin" />
|
<RefreshCw size={14} className="spin" />
|
||||||
) : (
|
) : (
|
||||||
<X size={14} />
|
<X size={14} />
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{insight.content && (
|
</div>
|
||||||
<p className="insight-item-content">{insight.content}</p>
|
{insight.content && (
|
||||||
)}
|
<p className="insight-item-content">{insight.content}</p>
|
||||||
<div className="insight-item-meta">
|
)}
|
||||||
<span className={`insight-item-status insight-item-status--${insight.status}`}>
|
<div className="insight-item-meta">
|
||||||
{insight.status}
|
<span className={`insight-item-status insight-item-status--${insight.status}`}>
|
||||||
|
{insight.status}
|
||||||
|
</span>
|
||||||
|
{insight.createdAt && (
|
||||||
|
<span className="insight-item-date">
|
||||||
|
<Clock size={12} />
|
||||||
|
{new Date(insight.createdAt).toLocaleDateString()}
|
||||||
</span>
|
</span>
|
||||||
{insight.createdAt && (
|
)}
|
||||||
<span className="insight-item-date">
|
</div>
|
||||||
<Clock size={12} />
|
</li>
|
||||||
{new Date(insight.createdAt).toLocaleDateString()}
|
);
|
||||||
</span>
|
})}
|
||||||
)}
|
</ul>
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
@@ -355,7 +349,7 @@ export function InsightsView({ projectId, addToast, onClose, onCreateTask }: Ins
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="insights-sections">
|
<div className="insights-sections">
|
||||||
{sections.map(renderSection)}
|
{sections.filter((section) => section.items.length > 0).map(renderSection)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -120,8 +120,101 @@ describe("InsightsView", () => {
|
|||||||
|
|
||||||
describe("rendering", () => {
|
describe("rendering", () => {
|
||||||
it("should render all five section headings in expected order", () => {
|
it("should render all five section headings in expected order", () => {
|
||||||
|
const populatedSections = [
|
||||||
|
{
|
||||||
|
...mockSections[0],
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: "INS-100",
|
||||||
|
projectId: "test",
|
||||||
|
title: "Features insight",
|
||||||
|
content: "Features content",
|
||||||
|
category: "features" as const,
|
||||||
|
status: "generated" as const,
|
||||||
|
fingerprint: "fp100",
|
||||||
|
provenance: { trigger: "manual" as const },
|
||||||
|
lastRunId: null,
|
||||||
|
createdAt: "2024-01-01T00:00:00Z",
|
||||||
|
updatedAt: "2024-01-01T00:00:00Z",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...mockSections[1],
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: "INS-101",
|
||||||
|
projectId: "test",
|
||||||
|
title: "Architecture insight",
|
||||||
|
content: "Architecture content",
|
||||||
|
category: "architecture" as const,
|
||||||
|
status: "generated" as const,
|
||||||
|
fingerprint: "fp101",
|
||||||
|
provenance: { trigger: "manual" as const },
|
||||||
|
lastRunId: null,
|
||||||
|
createdAt: "2024-01-01T00:00:00Z",
|
||||||
|
updatedAt: "2024-01-01T00:00:00Z",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...mockSections[2],
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: "INS-102",
|
||||||
|
projectId: "test",
|
||||||
|
title: "Competitive insight",
|
||||||
|
content: "Competitive content",
|
||||||
|
category: "competitive_analysis" as const,
|
||||||
|
status: "generated" as const,
|
||||||
|
fingerprint: "fp102",
|
||||||
|
provenance: { trigger: "manual" as const },
|
||||||
|
lastRunId: null,
|
||||||
|
createdAt: "2024-01-01T00:00:00Z",
|
||||||
|
updatedAt: "2024-01-01T00:00:00Z",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...mockSections[3],
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: "INS-103",
|
||||||
|
projectId: "test",
|
||||||
|
title: "Research insight",
|
||||||
|
content: "Research content",
|
||||||
|
category: "research" as const,
|
||||||
|
status: "generated" as const,
|
||||||
|
fingerprint: "fp103",
|
||||||
|
provenance: { trigger: "manual" as const },
|
||||||
|
lastRunId: null,
|
||||||
|
createdAt: "2024-01-01T00:00:00Z",
|
||||||
|
updatedAt: "2024-01-01T00:00:00Z",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...mockSections[4],
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: "INS-104",
|
||||||
|
projectId: "test",
|
||||||
|
title: "Trends insight",
|
||||||
|
content: "Trends content",
|
||||||
|
category: "trends" as const,
|
||||||
|
status: "generated" as const,
|
||||||
|
fingerprint: "fp104",
|
||||||
|
provenance: { trigger: "manual" as const },
|
||||||
|
lastRunId: null,
|
||||||
|
createdAt: "2024-01-01T00:00:00Z",
|
||||||
|
updatedAt: "2024-01-01T00:00:00Z",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
mockUseInsights.mockReturnValue({
|
mockUseInsights.mockReturnValue({
|
||||||
sections: mockSections,
|
sections: populatedSections,
|
||||||
loading: false,
|
loading: false,
|
||||||
error: null,
|
error: null,
|
||||||
latestRun: null,
|
latestRun: null,
|
||||||
@@ -222,7 +315,7 @@ describe("InsightsView", () => {
|
|||||||
expect(screen.getByText("No insights yet")).toBeInTheDocument();
|
expect(screen.getByText("No insights yet")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should render per-section empty placeholder when specific section has no items", () => {
|
it("should hide empty sections when specific section has no items", () => {
|
||||||
const sectionsWithOne = [
|
const sectionsWithOne = [
|
||||||
{
|
{
|
||||||
category: "features" as const,
|
category: "features" as const,
|
||||||
@@ -267,8 +360,102 @@ describe("InsightsView", () => {
|
|||||||
|
|
||||||
render(<InsightsView {...defaultProps} />);
|
render(<InsightsView {...defaultProps} />);
|
||||||
|
|
||||||
expect(screen.getByTestId("insights-empty-architecture")).toBeInTheDocument();
|
expect(screen.queryByTestId("insights-section-architecture")).not.toBeInTheDocument();
|
||||||
expect(screen.getByTestId("insights-empty-architecture")).toHaveTextContent("No insights in this category");
|
expect(screen.getByTestId("insights-section-features")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should only render sections that have items", () => {
|
||||||
|
const sectionsWithTwo = [
|
||||||
|
{
|
||||||
|
category: "features" as const,
|
||||||
|
label: "Features",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: "INS-11",
|
||||||
|
projectId: "test",
|
||||||
|
title: "Features Insight",
|
||||||
|
content: "Content",
|
||||||
|
category: "features" as const,
|
||||||
|
status: "generated" as const,
|
||||||
|
fingerprint: "fp11",
|
||||||
|
provenance: { trigger: "manual" as const },
|
||||||
|
lastRunId: null,
|
||||||
|
createdAt: "2024-01-01T00:00:00Z",
|
||||||
|
updatedAt: "2024-01-01T00:00:00Z",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: "architecture" as const,
|
||||||
|
label: "Architecture",
|
||||||
|
items: [],
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: "competitive_analysis" as const,
|
||||||
|
label: "Competitive Analysis",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: "INS-12",
|
||||||
|
projectId: "test",
|
||||||
|
title: "Competitive Insight",
|
||||||
|
content: "Content",
|
||||||
|
category: "competitive_analysis" as const,
|
||||||
|
status: "generated" as const,
|
||||||
|
fingerprint: "fp12",
|
||||||
|
provenance: { trigger: "manual" as const },
|
||||||
|
lastRunId: null,
|
||||||
|
createdAt: "2024-01-01T00:00:00Z",
|
||||||
|
updatedAt: "2024-01-01T00:00:00Z",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: "research" as const,
|
||||||
|
label: "Research",
|
||||||
|
items: [],
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: "trends" as const,
|
||||||
|
label: "Trends",
|
||||||
|
items: [],
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
mockUseInsights.mockReturnValue({
|
||||||
|
sections: sectionsWithTwo,
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
latestRun: null,
|
||||||
|
isRunInFlight: false,
|
||||||
|
runError: null,
|
||||||
|
refresh: vi.fn(),
|
||||||
|
runInsights: vi.fn(),
|
||||||
|
dismiss: vi.fn(),
|
||||||
|
createTask: vi.fn(),
|
||||||
|
dismissStates: new Map(),
|
||||||
|
createTaskStates: new Map(),
|
||||||
|
totalCount: 2,
|
||||||
|
dismissedCount: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<InsightsView {...defaultProps} />);
|
||||||
|
|
||||||
|
expect(screen.getAllByTestId(/insights-section-/)).toHaveLength(2);
|
||||||
|
expect(screen.getByTestId("insights-section-features")).toBeInTheDocument();
|
||||||
|
expect(screen.getByTestId("insights-section-competitive_analysis")).toBeInTheDocument();
|
||||||
|
expect(screen.queryByTestId("insights-section-architecture")).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByTestId("insights-section-research")).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByTestId("insights-section-trends")).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should render status badge with correct CSS class for generated status", () => {
|
it("should render status badge with correct CSS class for generated status", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user