FN-9196: Move archived chat toggle into filter row
Restyle the chat archive control as a compact, responsive filter-line toggle. - Place the Archived toggle beside the tag filter with stable pressed-state semantics. - Add token-based responsive styling for desktop, narrow, and mobile sidebars. - Cover toggle behavior, selected-tag coexistence, and filter-row layout. - Add a patch changeset for the published Fusion package. Files changed: .changeset/chat-archived-filter-row.md | 7 ++ packages/dashboard/app/components/ChatView.css | 17 +++- packages/dashboard/app/components/ChatView.tsx | 44 +++++++--- .../ChatView.archived-toggle-row.test.tsx | 99 ++++++++++++++++++++++ 4 files changed, 151 insertions(+), 16 deletions(-) Fusion-Task-Id: FN-9196 Fusion-Task-Lineage: 2bd3d000-84be-4798-9b73-de1feac40536 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/chat-archived-filter-row.md
Normal file
7
.changeset/chat-archived-filter-row.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Chat sidebar shows a compact Archived toggle on the tag filter line.
|
||||
category: fix
|
||||
dev: ChatView sidebar filter row (.chat-sidebar-filter-row); .chat-archived-toggle restyled, testid unchanged.
|
||||
@@ -706,6 +706,10 @@ A narrow floating host keeps the one-pane list/detail flow. FN-9193 adds a docke
|
||||
min-height: calc(var(--space-2xl) + var(--space-xs));
|
||||
}
|
||||
|
||||
.chat-view--narrow .chat-sidebar-filter-row {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.chat-view--narrow .chat-sidebar-list {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
@@ -2498,9 +2502,17 @@ Shared ChatView hosts expose Direct-only tag filters and compact chips without i
|
||||
|
||||
FNXC:ChatTags 2026-08-23-15:51:
|
||||
The tag filter select must use the sibling sidebar search input's comfortable token-based inner padding so "All tags" is not flush against its border across desktop, narrow, and mobile hosts.
|
||||
|
||||
FNXC:ChatArchived 2026-08-23-16:27:
|
||||
The compact Archived toggle shares the narrowed tag-filter line, with tokenized row padding separating the search input and session list. Narrow and mobile sidebars may wrap the controls rather than clipping or horizontally overflowing them.
|
||||
*/
|
||||
.chat-tag-filter { display: flex; align-items: center; gap: var(--space-xs); color: var(--text-muted); }
|
||||
.chat-tag-filter select { flex: 1; min-width: 0; border: 1px solid var(--border); border-radius: var(--radius-md); background: var(--bg); color: var(--text); padding: var(--space-sm) var(--space-md); }
|
||||
.chat-sidebar-filter-row { display: flex; align-items: center; gap: var(--space-xs); padding: var(--space-xs) 0; min-width: 0; }
|
||||
.chat-tag-filter { display: flex; flex: 1 1 auto; align-items: center; gap: var(--space-xs); min-width: 0; overflow: hidden; color: var(--text-muted); }
|
||||
.chat-tag-filter > svg,
|
||||
.chat-tag-filter > .btn-icon { flex: 0 0 auto; }
|
||||
.chat-tag-filter select { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; border: 1px solid var(--border); border-radius: var(--radius-md); background: var(--bg); color: var(--text); padding: var(--space-sm) var(--space-md); }
|
||||
.chat-archived-toggle { flex: 0 0 auto; padding-inline: var(--space-sm); white-space: nowrap; }
|
||||
.chat-archived-toggle--active { background: color-mix(in srgb, var(--accent) 16%, var(--surface-2)); color: var(--accent-text); border-color: color-mix(in srgb, var(--accent) 45%, var(--border)); }
|
||||
.chat-session-tags { display: flex; flex-wrap: wrap; gap: var(--space-xs); overflow: hidden; }
|
||||
.chat-session-tag { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; padding: 0 var(--space-xs); border-radius: var(--radius-sm); background: var(--surface-2); color: var(--text-muted); font-size: var(--font-size-xs); }
|
||||
/* FNXC:ChatTags 2026-08-05-12:15: direct-conversation tag controls keep assignment, rename, and deletion in the existing context menu so shared Chat hosts do not grow another sidebar action row. */
|
||||
@@ -2511,6 +2523,7 @@ The tag filter select must use the sibling sidebar search input's comfortable to
|
||||
.chat-tag-create-row { display: flex; gap: var(--space-xs); }
|
||||
.chat-tag-create-row .input { min-width: 0; }
|
||||
@media (max-width: 768px) {
|
||||
.chat-sidebar-filter-row { flex-wrap: wrap; }
|
||||
.chat-session-tags { max-height: calc(var(--space-lg) * 2); }
|
||||
/*
|
||||
FNXC:ChatTags 2026-07-24-23:05:
|
||||
|
||||
@@ -3508,23 +3508,39 @@ export function ChatView({ projectId, addToast, floating = false, compactLayout
|
||||
data-testid="chat-search-input"
|
||||
/>
|
||||
</div>
|
||||
<label className="chat-tag-filter" htmlFor="chat-tag-filter">
|
||||
<Tag size={14} aria-hidden="true" />
|
||||
<select
|
||||
id="chat-tag-filter"
|
||||
value={selectedTagId ?? ""}
|
||||
onChange={(event) => setSelectedTagId(event.target.value || null)}
|
||||
data-testid="chat-tag-filter"
|
||||
aria-label={t("chat.filterByTag", "Filter conversations by tag")}
|
||||
{/*
|
||||
FNXC:ChatArchived 2026-08-23-16:27:
|
||||
The archived affordance is a compact Archived toggle sharing the tag-filter line, so the sidebar does not spend a full row on it. aria-pressed and active styling convey state instead of changing the visible label.
|
||||
*/}
|
||||
<div className="chat-sidebar-filter-row">
|
||||
<label className="chat-tag-filter" htmlFor="chat-tag-filter">
|
||||
<Tag size={14} aria-hidden="true" />
|
||||
<select
|
||||
id="chat-tag-filter"
|
||||
value={selectedTagId ?? ""}
|
||||
onChange={(event) => setSelectedTagId(event.target.value || null)}
|
||||
data-testid="chat-tag-filter"
|
||||
aria-label={t("chat.filterByTag", "Filter conversations by tag")}
|
||||
>
|
||||
<option value="">{t("chat.allTags", "All tags")}</option>
|
||||
{tags.map((tag) => <option key={tag.id} value={tag.id}>{tag.name}</option>)}
|
||||
</select>
|
||||
{selectedTagId ? <button type="button" className="btn-icon" aria-label={t("chat.clearTagFilter", "Clear tag filter")} onClick={() => setSelectedTagId(null)}><X size={14} /></button> : null}
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
className={`btn btn-sm chat-archived-toggle${showArchivedSessions ? " chat-archived-toggle--active" : ""}`}
|
||||
data-testid="chat-archived-toggle"
|
||||
aria-pressed={showArchivedSessions}
|
||||
title={t("chat.showArchivedConversations", "Show archived conversations")}
|
||||
aria-label={t("chat.showArchivedConversations", "Show archived conversations")}
|
||||
onClick={() => { const next = !showArchivedSessions; setShowArchivedSessions(next); if (next) void refreshArchivedSessions(); }}
|
||||
>
|
||||
<option value="">{t("chat.allTags", "All tags")}</option>
|
||||
{tags.map((tag) => <option key={tag.id} value={tag.id}>{tag.name}</option>)}
|
||||
</select>
|
||||
{selectedTagId ? <button type="button" className="btn-icon" aria-label={t("chat.clearTagFilter", "Clear tag filter")} onClick={() => setSelectedTagId(null)}><X size={14} /></button> : null}
|
||||
</label>
|
||||
{t("chat.archived", "Archived")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/* Session list section */}
|
||||
<div className="chat-archived-toggle"><button type="button" className="btn btn-sm btn-secondary" data-testid="chat-archived-toggle" onClick={() => { const next = !showArchivedSessions; setShowArchivedSessions(next); if (next) void refreshArchivedSessions(); }}>{showArchivedSessions ? "Active conversations" : "Archived conversations"}</button></div>
|
||||
<div className="chat-session-list chat-sidebar-list">
|
||||
{sessionsLoading ? (
|
||||
<div className="chat-empty-state chat-empty-state--padded">{t("chat.loadingConversations", "Loading...")}</div>
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { screen, within } from "@testing-library/react";
|
||||
import { userEvent } from "@testing-library/user-event";
|
||||
import { ChatView } from "../ChatView";
|
||||
import { installChatViewEnv, renderWithAct, setupMockChat } from "./ChatView.test-harness";
|
||||
|
||||
vi.mock("../../hooks/useChat");
|
||||
vi.mock("../../hooks/useChatRooms");
|
||||
vi.mock("../../hooks/useNavigationHistory", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("../../hooks/useNavigationHistory")>()),
|
||||
useNavigationHistoryContext: () => ({ pushNav: vi.fn(), replaceCurrent: vi.fn() }),
|
||||
}));
|
||||
vi.mock("../../api", () => ({
|
||||
fetchSettings: vi.fn().mockResolvedValue({}),
|
||||
fetchModels: vi.fn().mockResolvedValue({ models: [], favoriteProviders: [], favoriteModels: [], defaultProvider: "anthropic", defaultModelId: "claude-sonnet-4-5" }),
|
||||
fetchAgents: vi.fn().mockResolvedValue([]),
|
||||
fetchDiscoveredSkills: vi.fn().mockResolvedValue([]),
|
||||
fetchTasks: vi.fn().mockResolvedValue([]),
|
||||
searchFiles: vi.fn().mockResolvedValue({ files: [] }),
|
||||
}));
|
||||
|
||||
installChatViewEnv();
|
||||
|
||||
const chatViewCss = readFileSync(resolve(__dirname, "../ChatView.css"), "utf8");
|
||||
const archivedSession = {
|
||||
id: "session-archived",
|
||||
agentId: "agent-002",
|
||||
status: "archived" as const,
|
||||
title: "Archived conversation",
|
||||
createdAt: "2026-08-23T00:00:00.000Z",
|
||||
updatedAt: "2026-08-23T00:00:00.000Z",
|
||||
};
|
||||
|
||||
function getFilterRow() {
|
||||
const toggle = screen.getByTestId("chat-archived-toggle");
|
||||
const row = toggle.closest(".chat-sidebar-filter-row");
|
||||
expect(row).toContainElement(screen.getByTestId("chat-tag-filter"));
|
||||
expect(row?.parentElement).toHaveClass("chat-sidebar-search-container");
|
||||
return row!;
|
||||
}
|
||||
|
||||
describe("ChatView archived toggle filter row", () => {
|
||||
it("keeps the Archived label and archive behavior in the shared filter row", async () => {
|
||||
const refreshArchivedSessions = vi.fn().mockResolvedValue(undefined);
|
||||
setupMockChat({ archivedSessions: [archivedSession], refreshArchivedSessions });
|
||||
const user = userEvent.setup();
|
||||
|
||||
await renderWithAct(<ChatView projectId="proj-123" addToast={vi.fn()} />);
|
||||
|
||||
const toggle = screen.getByTestId("chat-archived-toggle");
|
||||
expect(toggle).toHaveTextContent(/^Archived$/);
|
||||
expect(toggle).toHaveAttribute("aria-pressed", "false");
|
||||
expect(screen.queryByText("Archived conversations")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Active conversations")).not.toBeInTheDocument();
|
||||
getFilterRow();
|
||||
expect(document.querySelectorAll("[data-testid=\"chat-archived-toggle\"]")).toHaveLength(1);
|
||||
expect(document.querySelector(".chat-sidebar-search-container")?.nextElementSibling).toHaveClass("chat-session-list");
|
||||
|
||||
await user.click(toggle);
|
||||
expect(toggle).toHaveTextContent(/^Archived$/);
|
||||
expect(toggle).toHaveAttribute("aria-pressed", "true");
|
||||
expect(refreshArchivedSessions).toHaveBeenCalledOnce();
|
||||
expect(screen.getByTestId("chat-archived-session-session-archived")).toBeInTheDocument();
|
||||
|
||||
await user.click(toggle);
|
||||
expect(toggle).toHaveTextContent(/^Archived$/);
|
||||
expect(toggle).toHaveAttribute("aria-pressed", "false");
|
||||
expect(refreshArchivedSessions).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("keeps populated selected-tag controls beside Archived", async () => {
|
||||
const tag = {
|
||||
id: "tag-long",
|
||||
projectId: "proj-123",
|
||||
name: "A very long tag name that must not displace the archive toggle",
|
||||
createdAt: "2026-08-23T00:00:00.000Z",
|
||||
updatedAt: "2026-08-23T00:00:00.000Z",
|
||||
};
|
||||
setupMockChat({ tags: [tag], selectedTagId: tag.id, setSelectedTagId: vi.fn() });
|
||||
|
||||
await renderWithAct(<ChatView projectId="proj-123" addToast={vi.fn()} />);
|
||||
|
||||
const row = getFilterRow();
|
||||
expect(within(row).getByLabelText("Clear tag filter")).toBeInTheDocument();
|
||||
expect(within(row).getByTestId("chat-archived-toggle")).toHaveTextContent(/^Archived$/);
|
||||
});
|
||||
|
||||
it("declares a token-padded flex filter row without a full-row tag filter", () => {
|
||||
const rowRule = chatViewCss.match(/(?:^|\n)\.chat-sidebar-filter-row\s*\{([^{}]*)\}/)?.[1];
|
||||
const tagFilterRule = chatViewCss.match(/(?:^|\n)\.chat-tag-filter\s*\{([^{}]*)\}/)?.[1];
|
||||
|
||||
expect(rowRule).toMatch(/display:\s*flex/);
|
||||
expect(rowRule).toMatch(/padding:\s*var\(--space-/);
|
||||
expect(tagFilterRule).toContain("flex: 1 1 auto");
|
||||
expect(tagFilterRule).not.toMatch(/flex:\s*1\s*;/);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user