feat(FN-3885): add resizable split pane to mailbox view
Adds a resizable split-pane to the MailboxView component (FN-3885) and introduces a report archive feature to fusion-plugin-reports with a full store, schema, and types layer (FN-3784). Also adds tests for the notification dispatcher pipeline, updates the MemoryView test allowlist, and clarifies the Fusion-Task-Id: FN-3885
This commit is contained in:
5
.changeset/fn-3885-mailbox-resizable-pane.md
Normal file
5
.changeset/fn-3885-mailbox-resizable-pane.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Mailbox view now has a draggable resize handle between the list and detail panes (desktop only), with keyboard support and per-project persisted width.
|
||||||
@@ -649,12 +649,30 @@
|
|||||||
|
|
||||||
.mailbox-view .mailbox-split-layout {
|
.mailbox-view .mailbox-split-layout {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(17.5rem, 38%) minmax(0, 1fr);
|
grid-template-columns: auto auto minmax(0, 1fr);
|
||||||
gap: var(--space-lg);
|
gap: 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mailbox-view .mailbox-split-resize-handle {
|
||||||
|
width: var(--space-sm);
|
||||||
|
cursor: col-resize;
|
||||||
|
background: color-mix(in srgb, var(--border) 70%, transparent);
|
||||||
|
transition: background var(--transition-fast);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mailbox-view .mailbox-split-resize-handle:hover,
|
||||||
|
.mailbox-view .mailbox-split-resize-handle:focus-visible {
|
||||||
|
background: color-mix(in srgb, var(--todo) 35%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mailbox-view .mailbox-split-resize-handle:focus-visible {
|
||||||
|
box-shadow: var(--focus-ring-strong);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
.mailbox-view .mailbox-split-list-pane,
|
.mailbox-view .mailbox-split-list-pane,
|
||||||
.mailbox-view .mailbox-split-detail-pane {
|
.mailbox-view .mailbox-split-detail-pane {
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
@@ -857,6 +875,10 @@
|
|||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mailbox-view .mailbox-split-resize-handle {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.mailbox-view .mailbox-split-list-pane,
|
.mailbox-view .mailbox-split-list-pane,
|
||||||
.mailbox-view .mailbox-split-detail-pane {
|
.mailbox-view .mailbox-split-detail-pane {
|
||||||
border: none;
|
border: none;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import "./MailboxModal.css";
|
import "./MailboxModal.css";
|
||||||
import { useState, useEffect, useCallback, useMemo, type CSSProperties } from "react";
|
import { useState, useEffect, useCallback, useMemo, useRef, type CSSProperties } from "react";
|
||||||
import {
|
import {
|
||||||
Mail,
|
Mail,
|
||||||
Send,
|
Send,
|
||||||
@@ -38,6 +38,7 @@ import { MessageComposer } from "./MessageComposer";
|
|||||||
import { subscribeSse } from "../sse-bus";
|
import { subscribeSse } from "../sse-bus";
|
||||||
import { useViewportMode } from "../hooks/useViewportMode";
|
import { useViewportMode } from "../hooks/useViewportMode";
|
||||||
import { useMobileKeyboard } from "../hooks/useMobileKeyboard";
|
import { useMobileKeyboard } from "../hooks/useMobileKeyboard";
|
||||||
|
import { getScopedItem, setScopedItem } from "../utils/projectStorage";
|
||||||
|
|
||||||
// ── Types ─────────────────────────────────────────────────────────────────
|
// ── Types ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -50,6 +51,34 @@ interface MailboxViewProps {
|
|||||||
onUnreadCountChange?: (count: number) => void;
|
onUnreadCountChange?: (count: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MAILBOX_SIDEBAR_MIN_WIDTH = 280;
|
||||||
|
const MAILBOX_SIDEBAR_MAX_RATIO = 0.65;
|
||||||
|
const MAILBOX_SIDEBAR_KEYBOARD_STEP = 16;
|
||||||
|
const MAILBOX_SIDEBAR_DEFAULT_WIDTH = 320;
|
||||||
|
|
||||||
|
function getMailboxSidebarMaxWidth(containerWidth: number): number {
|
||||||
|
return Math.max(MAILBOX_SIDEBAR_MIN_WIDTH, containerWidth * MAILBOX_SIDEBAR_MAX_RATIO);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clampMailboxSidebarWidth(width: number, containerWidth: number): number {
|
||||||
|
const maxWidth = getMailboxSidebarMaxWidth(containerWidth);
|
||||||
|
return Math.min(Math.max(width, MAILBOX_SIDEBAR_MIN_WIDTH), maxWidth);
|
||||||
|
}
|
||||||
|
|
||||||
|
function readMailboxSidebarWidth(projectId?: string): number {
|
||||||
|
try {
|
||||||
|
const saved = getScopedItem("kb-dashboard-mailbox-sidebar-width", projectId);
|
||||||
|
if (!saved) return MAILBOX_SIDEBAR_DEFAULT_WIDTH;
|
||||||
|
const parsed = Number(saved);
|
||||||
|
if (Number.isFinite(parsed) && parsed > 0) {
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Invalid localStorage data - fall through to default
|
||||||
|
}
|
||||||
|
|
||||||
|
return MAILBOX_SIDEBAR_DEFAULT_WIDTH;
|
||||||
|
}
|
||||||
|
|
||||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -191,6 +220,8 @@ export function MailboxView({
|
|||||||
const viewportMode = useViewportMode();
|
const viewportMode = useViewportMode();
|
||||||
const isMobile = viewportMode === "mobile";
|
const isMobile = viewportMode === "mobile";
|
||||||
const isSplitPane = !isMobile;
|
const isSplitPane = !isMobile;
|
||||||
|
const [sidebarWidth, setSidebarWidth] = useState<number>(() => readMailboxSidebarWidth(projectId));
|
||||||
|
const splitLayoutRef = useRef<HTMLDivElement>(null);
|
||||||
const { keyboardOverlap, viewportHeight, viewportOffsetTop, keyboardOpen } = useMobileKeyboard({ enabled: isMobile });
|
const { keyboardOverlap, viewportHeight, viewportOffsetTop, keyboardOpen } = useMobileKeyboard({ enabled: isMobile });
|
||||||
const containerKeyboardStyle = useMemo<CSSProperties | undefined>(() => {
|
const containerKeyboardStyle = useMemo<CSSProperties | undefined>(() => {
|
||||||
if (!keyboardOpen) {
|
if (!keyboardOpen) {
|
||||||
@@ -204,6 +235,75 @@ export function MailboxView({
|
|||||||
} as CSSProperties;
|
} as CSSProperties;
|
||||||
}, [keyboardOpen, keyboardOverlap, viewportHeight, viewportOffsetTop]);
|
}, [keyboardOpen, keyboardOverlap, viewportHeight, viewportOffsetTop]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setSidebarWidth(readMailboxSidebarWidth(projectId));
|
||||||
|
}, [projectId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isSplitPane) return;
|
||||||
|
const containerWidth = splitLayoutRef.current?.clientWidth;
|
||||||
|
if (!containerWidth) return;
|
||||||
|
|
||||||
|
setSidebarWidth((current) => clampMailboxSidebarWidth(current, containerWidth));
|
||||||
|
}, [isSplitPane]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isSplitPane) return;
|
||||||
|
try {
|
||||||
|
setScopedItem("kb-dashboard-mailbox-sidebar-width", String(sidebarWidth), projectId);
|
||||||
|
} catch {
|
||||||
|
// localStorage persistence is best-effort.
|
||||||
|
}
|
||||||
|
}, [isSplitPane, projectId, sidebarWidth]);
|
||||||
|
|
||||||
|
const handleSplitResizeStart = useCallback((event: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
if (!isSplitPane) return;
|
||||||
|
event.preventDefault();
|
||||||
|
const container = splitLayoutRef.current;
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const rect = container.getBoundingClientRect();
|
||||||
|
const onMouseMove = (moveEvent: MouseEvent) => {
|
||||||
|
const proposedWidth = moveEvent.clientX - rect.left;
|
||||||
|
setSidebarWidth(clampMailboxSidebarWidth(proposedWidth, rect.width));
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMouseUp = () => {
|
||||||
|
window.removeEventListener("mousemove", onMouseMove);
|
||||||
|
window.removeEventListener("mouseup", onMouseUp);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("mousemove", onMouseMove);
|
||||||
|
window.addEventListener("mouseup", onMouseUp);
|
||||||
|
}, [isSplitPane]);
|
||||||
|
|
||||||
|
const handleSplitResizeKeyDown = useCallback((event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||||
|
if (!isSplitPane) return;
|
||||||
|
const measuredWidth = splitLayoutRef.current?.clientWidth ?? 0;
|
||||||
|
const fallbackWidth = sidebarWidth / MAILBOX_SIDEBAR_MAX_RATIO + MAILBOX_SIDEBAR_KEYBOARD_STEP;
|
||||||
|
const containerWidth = Math.max(measuredWidth, fallbackWidth);
|
||||||
|
|
||||||
|
const maxWidth = getMailboxSidebarMaxWidth(containerWidth);
|
||||||
|
|
||||||
|
if (event.key === "ArrowLeft" || event.key === "ArrowRight") {
|
||||||
|
event.preventDefault();
|
||||||
|
const delta = event.key === "ArrowLeft" ? -MAILBOX_SIDEBAR_KEYBOARD_STEP : MAILBOX_SIDEBAR_KEYBOARD_STEP;
|
||||||
|
setSidebarWidth((current) => clampMailboxSidebarWidth(current + delta, containerWidth));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === "Home") {
|
||||||
|
event.preventDefault();
|
||||||
|
setSidebarWidth(MAILBOX_SIDEBAR_MIN_WIDTH);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === "End") {
|
||||||
|
event.preventDefault();
|
||||||
|
setSidebarWidth(maxWidth);
|
||||||
|
}
|
||||||
|
}, [isSplitPane, sidebarWidth]);
|
||||||
|
|
||||||
// ── Data fetching ─────────────────────────────────────────────────────
|
// ── Data fetching ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
const loadInbox = useCallback(async () => {
|
const loadInbox = useCallback(async () => {
|
||||||
@@ -1062,10 +1162,27 @@ export function MailboxView({
|
|||||||
|
|
||||||
<div className="mailbox-content" data-testid="mailbox-content">
|
<div className="mailbox-content" data-testid="mailbox-content">
|
||||||
{isSplitPane ? (
|
{isSplitPane ? (
|
||||||
<div className="mailbox-split-layout" data-testid="mailbox-split-layout">
|
<div className="mailbox-split-layout" data-testid="mailbox-split-layout" ref={splitLayoutRef}>
|
||||||
<div className="mailbox-split-list-pane" data-testid="mailbox-split-list-pane">
|
<div
|
||||||
|
className="mailbox-split-list-pane"
|
||||||
|
data-testid="mailbox-split-list-pane"
|
||||||
|
style={{ width: `${sidebarWidth}px` }}
|
||||||
|
>
|
||||||
{renderListPane()}
|
{renderListPane()}
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
className="mailbox-split-resize-handle"
|
||||||
|
data-testid="mailbox-split-resize-handle"
|
||||||
|
role="separator"
|
||||||
|
aria-orientation="vertical"
|
||||||
|
aria-label="Resize message list pane"
|
||||||
|
tabIndex={0}
|
||||||
|
aria-valuemin={MAILBOX_SIDEBAR_MIN_WIDTH}
|
||||||
|
aria-valuemax={Math.round(getMailboxSidebarMaxWidth(splitLayoutRef.current?.clientWidth ?? sidebarWidth / MAILBOX_SIDEBAR_MAX_RATIO))}
|
||||||
|
aria-valuenow={Math.round(sidebarWidth)}
|
||||||
|
onMouseDown={handleSplitResizeStart}
|
||||||
|
onKeyDown={handleSplitResizeKeyDown}
|
||||||
|
/>
|
||||||
<div className="mailbox-split-detail-pane" data-testid="mailbox-split-detail-pane">
|
<div className="mailbox-split-detail-pane" data-testid="mailbox-split-detail-pane">
|
||||||
{renderDetailPane()}
|
{renderDetailPane()}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -169,6 +169,7 @@ function makeOutboxResponse(messages: Message[]) {
|
|||||||
describe("MailboxView", () => {
|
describe("MailboxView", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
|
window.localStorage.clear();
|
||||||
sseSubscriptions.length = 0;
|
sseSubscriptions.length = 0;
|
||||||
mockUseViewportMode.mockReturnValue("desktop");
|
mockUseViewportMode.mockReturnValue("desktop");
|
||||||
mockUseMobileKeyboard.mockReturnValue({
|
mockUseMobileKeyboard.mockReturnValue({
|
||||||
@@ -1538,6 +1539,80 @@ describe("MailboxView", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("resizable split pane", () => {
|
||||||
|
it("renders a desktop resize handle with separator aria semantics", async () => {
|
||||||
|
mockUseViewportMode.mockReturnValue("desktop");
|
||||||
|
mockFetchInbox.mockResolvedValue(makeInboxResponse([mockMessage], 1));
|
||||||
|
|
||||||
|
render(<MailboxView {...defaultProps} projectId="proj-resize" />);
|
||||||
|
|
||||||
|
const handle = await screen.findByTestId("mailbox-split-resize-handle");
|
||||||
|
expect(handle.getAttribute("role")).toBe("separator");
|
||||||
|
expect(handle.getAttribute("aria-orientation")).toBe("vertical");
|
||||||
|
expect(Number(handle.getAttribute("aria-valuenow"))).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not render the resize handle on mobile", async () => {
|
||||||
|
mockUseViewportMode.mockReturnValue("mobile");
|
||||||
|
mockFetchInbox.mockResolvedValue(makeInboxResponse([mockMessage], 1));
|
||||||
|
|
||||||
|
render(<MailboxView {...defaultProps} />);
|
||||||
|
|
||||||
|
await screen.findByTestId("mailbox-view");
|
||||||
|
expect(screen.queryByTestId("mailbox-split-resize-handle")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("supports keyboard resize and Home/End clamping", async () => {
|
||||||
|
mockUseViewportMode.mockReturnValue("desktop");
|
||||||
|
mockFetchInbox.mockResolvedValue(makeInboxResponse([mockMessage], 1));
|
||||||
|
|
||||||
|
render(<MailboxView {...defaultProps} projectId="proj-keys" />);
|
||||||
|
|
||||||
|
const handle = await screen.findByTestId("mailbox-split-resize-handle");
|
||||||
|
const initial = Number(handle.getAttribute("aria-valuenow"));
|
||||||
|
|
||||||
|
fireEvent.keyDown(handle, { key: "ArrowLeft" });
|
||||||
|
const afterLeft = Number(handle.getAttribute("aria-valuenow"));
|
||||||
|
expect(afterLeft).toBeLessThan(initial);
|
||||||
|
|
||||||
|
fireEvent.keyDown(handle, { key: "ArrowRight" });
|
||||||
|
const afterRight = Number(handle.getAttribute("aria-valuenow"));
|
||||||
|
expect(afterRight).toBeGreaterThanOrEqual(afterLeft);
|
||||||
|
|
||||||
|
fireEvent.keyDown(handle, { key: "Home" });
|
||||||
|
expect(Number(handle.getAttribute("aria-valuenow"))).toBe(280);
|
||||||
|
|
||||||
|
fireEvent.keyDown(handle, { key: "End" });
|
||||||
|
expect(Number(handle.getAttribute("aria-valuenow"))).toBeGreaterThanOrEqual(280);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("persists and restores scoped mailbox sidebar width", async () => {
|
||||||
|
mockUseViewportMode.mockReturnValue("desktop");
|
||||||
|
mockFetchInbox.mockResolvedValue(makeInboxResponse([mockMessage], 1));
|
||||||
|
|
||||||
|
const projectId = "proj-persist";
|
||||||
|
const storageKey = `kb:${projectId}:kb-dashboard-mailbox-sidebar-width`;
|
||||||
|
window.localStorage.setItem(storageKey, "360");
|
||||||
|
|
||||||
|
const { unmount } = render(<MailboxView {...defaultProps} projectId={projectId} />);
|
||||||
|
|
||||||
|
const handle = await screen.findByTestId("mailbox-split-resize-handle");
|
||||||
|
expect(Number(handle.getAttribute("aria-valuenow"))).toBe(360);
|
||||||
|
|
||||||
|
fireEvent.keyDown(handle, { key: "ArrowRight" });
|
||||||
|
await waitFor(() => {
|
||||||
|
const savedWidth = Number(window.localStorage.getItem(storageKey));
|
||||||
|
expect(savedWidth).toBeGreaterThan(360);
|
||||||
|
});
|
||||||
|
|
||||||
|
unmount();
|
||||||
|
render(<MailboxView {...defaultProps} projectId={projectId} />);
|
||||||
|
const remountedHandle = await screen.findByTestId("mailbox-split-resize-handle");
|
||||||
|
const persistedWidth = Number(window.localStorage.getItem(storageKey));
|
||||||
|
expect(Number(remountedHandle.getAttribute("aria-valuenow"))).toBe(Math.round(persistedWidth));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("mobile layout CSS regressions", () => {
|
describe("mobile layout CSS regressions", () => {
|
||||||
it("defines .mailbox-view base flex layout with min-height: 0", async () => {
|
it("defines .mailbox-view base flex layout with min-height: 0", async () => {
|
||||||
const fs = await import("fs");
|
const fs = await import("fs");
|
||||||
@@ -1557,12 +1632,7 @@ describe("MailboxView", () => {
|
|||||||
it("defines desktop/tablet split-pane selectors under .mailbox-view scope", async () => {
|
it("defines desktop/tablet split-pane selectors under .mailbox-view scope", async () => {
|
||||||
const css = loadAllAppCss();
|
const css = loadAllAppCss();
|
||||||
|
|
||||||
const splitLayoutBlockMatch = css.match(/\.mailbox-view\s+\.mailbox-split-layout\s*\{([^}]*)\}/);
|
expect(css).toMatch(/\.mailbox-view\s+\.mailbox-split-layout\s*\{[^}]*display:\s*grid;[^}]*grid-template-columns:\s*auto\s+auto\s+minmax\(0,\s*1fr\);[^}]*gap:\s*0;[^}]*min-height:\s*0;[^}]*\}/);
|
||||||
expect(splitLayoutBlockMatch).toBeTruthy();
|
|
||||||
const splitLayoutBlock = splitLayoutBlockMatch![1];
|
|
||||||
expect(splitLayoutBlock).toContain("display: grid;");
|
|
||||||
expect(splitLayoutBlock).toContain("min-height: 0;");
|
|
||||||
expect(splitLayoutBlock).toContain("grid-template-columns");
|
|
||||||
|
|
||||||
const splitPaneBlockMatch = css.match(/\.mailbox-view\s+\.mailbox-split-list-pane,\s*\n\.mailbox-view\s+\.mailbox-split-detail-pane\s*\{([^}]*)\}/);
|
const splitPaneBlockMatch = css.match(/\.mailbox-view\s+\.mailbox-split-list-pane,\s*\n\.mailbox-view\s+\.mailbox-split-detail-pane\s*\{([^}]*)\}/);
|
||||||
expect(splitPaneBlockMatch).toBeTruthy();
|
expect(splitPaneBlockMatch).toBeTruthy();
|
||||||
@@ -1571,6 +1641,8 @@ describe("MailboxView", () => {
|
|||||||
expect(splitPaneBlock).toContain("border: var(--btn-border-width) solid var(--border);");
|
expect(splitPaneBlock).toContain("border: var(--btn-border-width) solid var(--border);");
|
||||||
expect(splitPaneBlock).toContain("background: var(--surface);");
|
expect(splitPaneBlock).toContain("background: var(--surface);");
|
||||||
|
|
||||||
|
expect(css).toMatch(/\.mailbox-view\s+\.mailbox-split-resize-handle\s*\{[^}]*cursor:\s*col-resize;[^}]*background:\s*color-mix\(in srgb,\s*var\(--border\)\s*70%,\s*transparent\);[^}]*\}/);
|
||||||
|
|
||||||
const splitEmptyBlockMatch = css.match(/\.mailbox-view\s+\.mailbox-split-empty\s*\{([^}]*)\}/);
|
const splitEmptyBlockMatch = css.match(/\.mailbox-view\s+\.mailbox-split-empty\s*\{([^}]*)\}/);
|
||||||
expect(splitEmptyBlockMatch).toBeTruthy();
|
expect(splitEmptyBlockMatch).toBeTruthy();
|
||||||
expect(splitEmptyBlockMatch![1]).toContain("color: var(--text-muted);");
|
expect(splitEmptyBlockMatch![1]).toContain("color: var(--text-muted);");
|
||||||
@@ -1594,14 +1666,16 @@ describe("MailboxView", () => {
|
|||||||
// Verify .mailbox-view selectors are in mobile section
|
// Verify .mailbox-view selectors are in mobile section
|
||||||
expect(mailboxMobileSection).toContain(".mailbox-view .mailbox-header");
|
expect(mailboxMobileSection).toContain(".mailbox-view .mailbox-header");
|
||||||
expect(mailboxMobileSection).toMatch(/\.mailbox-modal \.mailbox-header-actions,\s*\.mailbox-view \.mailbox-header-actions\s*\{[^}]*gap:\s*var\(--space-sm\);[^}]*\}/);
|
expect(mailboxMobileSection).toMatch(/\.mailbox-modal \.mailbox-header-actions,\s*\.mailbox-view \.mailbox-header-actions\s*\{[^}]*gap:\s*var\(--space-sm\);[^}]*\}/);
|
||||||
expect(mailboxMobileSection).toMatch(/\.mailbox-modal \.mailbox-header-actions \.btn,[^}]*\.mailbox-view \.mailbox-header-actions \.btn-icon\s*\{[^}]*min-height:\s*36px;[^}]*\}/);
|
expect(mailboxMobileSection).toMatch(/\.mailbox-modal \.mailbox-header-actions \.btn,[^}]*\.mailbox-view \.mailbox-header-actions \.btn-icon\s*\{[^}]*min-height:\s*2\.25rem;[^}]*\}/);
|
||||||
expect(mailboxMobileSection).toMatch(/\.mailbox-modal \.mailbox-header-actions \.btn-icon,[^}]*\.mailbox-view \.mailbox-header-actions \.btn-icon\s*\{[^}]*min-width:\s*36px;[^}]*display:\s*inline-flex;[^}]*\}/);
|
expect(mailboxMobileSection).toMatch(/\.mailbox-modal \.mailbox-header-actions \.btn-icon,[^}]*\.mailbox-view \.mailbox-header-actions \.btn-icon\s*\{[^}]*min-width:\s*2\.25rem;[^}]*display:\s*inline-flex;[^}]*\}/);
|
||||||
expect(mailboxMobileSection).toContain(".mailbox-view .mailbox-tabs");
|
expect(mailboxMobileSection).toContain(".mailbox-view .mailbox-tabs");
|
||||||
expect(mailboxMobileSection).toContain(".mailbox-view .mailbox-content");
|
expect(mailboxMobileSection).toContain(".mailbox-view .mailbox-content");
|
||||||
expect(mailboxMobileSection).toContain(".mailbox-view .mailbox-split-layout");
|
expect(mailboxMobileSection).toContain(".mailbox-view .mailbox-split-layout");
|
||||||
expect(mailboxMobileSection).toContain(".mailbox-view .mailbox-split-list-pane");
|
expect(mailboxMobileSection).toContain(".mailbox-view .mailbox-split-list-pane");
|
||||||
expect(mailboxMobileSection).toContain(".mailbox-view .mailbox-split-detail-pane");
|
expect(mailboxMobileSection).toContain(".mailbox-view .mailbox-split-detail-pane");
|
||||||
|
expect(mailboxMobileSection).toContain(".mailbox-view .mailbox-split-resize-handle");
|
||||||
expect(mailboxMobileSection).toContain(".mailbox-view .mailbox-empty");
|
expect(mailboxMobileSection).toContain(".mailbox-view .mailbox-empty");
|
||||||
|
expect(mailboxMobileSection).toMatch(/\.mailbox-view\s+\.mailbox-split-resize-handle\s*\{[^}]*display:\s*none;[^}]*\}/);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("uses mobile-specific values for .mailbox-view content and FAB", async () => {
|
it("uses mobile-specific values for .mailbox-view content and FAB", async () => {
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ describe("projectStorage", () => {
|
|||||||
"kb-dashboard-selected-tasks",
|
"kb-dashboard-selected-tasks",
|
||||||
"kb-dashboard-list-selected-task",
|
"kb-dashboard-list-selected-task",
|
||||||
"kb-dashboard-list-sidebar-width",
|
"kb-dashboard-list-sidebar-width",
|
||||||
|
"kb-dashboard-mailbox-sidebar-width",
|
||||||
"kb-quick-entry-text",
|
"kb-quick-entry-text",
|
||||||
"kb-inline-create-text",
|
"kb-inline-create-text",
|
||||||
"fn-agent-view",
|
"fn-agent-view",
|
||||||
@@ -101,7 +102,7 @@ describe("projectStorage", () => {
|
|||||||
"fusion-plugin-dependency-graph:positions",
|
"fusion-plugin-dependency-graph:positions",
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
expect(PROJECT_STORAGE_KEYS).toHaveLength(23);
|
expect(PROJECT_STORAGE_KEYS).toHaveLength(24);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("stores branch filter values as scoped strings per project", () => {
|
it("stores branch filter values as scoped strings per project", () => {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export const PROJECT_STORAGE_KEYS: string[] = [
|
|||||||
"kb-dashboard-selected-tasks",
|
"kb-dashboard-selected-tasks",
|
||||||
"kb-dashboard-list-selected-task",
|
"kb-dashboard-list-selected-task",
|
||||||
"kb-dashboard-list-sidebar-width",
|
"kb-dashboard-list-sidebar-width",
|
||||||
|
"kb-dashboard-mailbox-sidebar-width",
|
||||||
"kb-quick-entry-text",
|
"kb-quick-entry-text",
|
||||||
"kb-inline-create-text",
|
"kb-inline-create-text",
|
||||||
"fn-agent-view",
|
"fn-agent-view",
|
||||||
|
|||||||
Reference in New Issue
Block a user