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));
|
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 {
|
.chat-view--narrow .chat-sidebar-list {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
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:
|
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.
|
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-sidebar-filter-row { display: flex; align-items: center; gap: var(--space-xs); padding: var(--space-xs) 0; min-width: 0; }
|
||||||
.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-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-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); }
|
.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. */
|
/* 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 { display: flex; gap: var(--space-xs); }
|
||||||
.chat-tag-create-row .input { min-width: 0; }
|
.chat-tag-create-row .input { min-width: 0; }
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
|
.chat-sidebar-filter-row { flex-wrap: wrap; }
|
||||||
.chat-session-tags { max-height: calc(var(--space-lg) * 2); }
|
.chat-session-tags { max-height: calc(var(--space-lg) * 2); }
|
||||||
/*
|
/*
|
||||||
FNXC:ChatTags 2026-07-24-23:05:
|
FNXC:ChatTags 2026-07-24-23:05:
|
||||||
|
|||||||
@@ -3508,23 +3508,39 @@ export function ChatView({ projectId, addToast, floating = false, compactLayout
|
|||||||
data-testid="chat-search-input"
|
data-testid="chat-search-input"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<label className="chat-tag-filter" htmlFor="chat-tag-filter">
|
{/*
|
||||||
<Tag size={14} aria-hidden="true" />
|
FNXC:ChatArchived 2026-08-23-16:27:
|
||||||
<select
|
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.
|
||||||
id="chat-tag-filter"
|
*/}
|
||||||
value={selectedTagId ?? ""}
|
<div className="chat-sidebar-filter-row">
|
||||||
onChange={(event) => setSelectedTagId(event.target.value || null)}
|
<label className="chat-tag-filter" htmlFor="chat-tag-filter">
|
||||||
data-testid="chat-tag-filter"
|
<Tag size={14} aria-hidden="true" />
|
||||||
aria-label={t("chat.filterByTag", "Filter conversations by tag")}
|
<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>
|
{t("chat.archived", "Archived")}
|
||||||
{tags.map((tag) => <option key={tag.id} value={tag.id}>{tag.name}</option>)}
|
</button>
|
||||||
</select>
|
</div>
|
||||||
{selectedTagId ? <button type="button" className="btn-icon" aria-label={t("chat.clearTagFilter", "Clear tag filter")} onClick={() => setSelectedTagId(null)}><X size={14} /></button> : null}
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
{/* Session list section */}
|
{/* 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">
|
<div className="chat-session-list chat-sidebar-list">
|
||||||
{sessionsLoading ? (
|
{sessionsLoading ? (
|
||||||
<div className="chat-empty-state chat-empty-state--padded">{t("chat.loadingConversations", "Loading...")}</div>
|
<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