feat(dashboard): Import Tasks list pane small + freely resizable, preview fills remainder
Default list width 256px (min 160, max min(480px,50%)), driven by an inline width var with flex:0 0 so the drag actually wins (the embedded wide layout previously forced a clamp via !important, so the handle did nothing). Preview pane is flex:1 1 auto;min-width:0 so freed space flows to it. Persisted per-project via kb-dashboard-github-import-list-width. Shared workspace covers both Issues and Pull Requests tabs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1144,9 +1144,16 @@ body. Let the content take its full intrinsic height and hand vertical scrolling
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
/* Side-by-side again: bound the list pane to a share of the CONTAINER width, overriding the inline viewport width. */
|
||||
/*
|
||||
FNXC:GitHubImport 2026-06-23-00:30:
|
||||
Wide (two-pane) embedded layout: the list pane must honor the user's inline `flex: 0 0 <listPaneWidth>px` from the resize
|
||||
handle — the prior `flex: ... !important` clamp silently overrode it, which is why the embedded Import Tasks list "couldn't
|
||||
be made smaller" and the preview stayed cramped. Reset only the stacked-mode overrides (width/max-height/padding) here and let
|
||||
the inline flex win. The preview pane is `flex: 1 1 auto; min-width: 0` (below) so the freed width flows to the preview.
|
||||
*/
|
||||
.github-import-modal--embedded .github-import-list-pane {
|
||||
flex: 0 1 clamp(240px, 38cqi, 420px) !important;
|
||||
/* `!important` here only neutralizes the stacked rule's own `!important`; the width comes from the inline CSS var. */
|
||||
flex: 0 0 var(--gh-import-list-width, clamp(160px, 30cqi, 480px)) !important;
|
||||
width: auto;
|
||||
max-height: none;
|
||||
padding-right: var(--space-md);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import "./GitHubImportModal.css";
|
||||
import { useState, useEffect, useCallback, useRef, type KeyboardEvent as ReactKeyboardEvent, type PointerEvent as ReactPointerEvent } from "react";
|
||||
import { useState, useEffect, useCallback, useRef, type CSSProperties, type KeyboardEvent as ReactKeyboardEvent, type PointerEvent as ReactPointerEvent } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { Task } from "@fusion/core";
|
||||
import { getErrorMessage } from "@fusion/core";
|
||||
@@ -20,6 +20,7 @@ import { useModalResizePersist } from "../hooks/useModalResizePersist";
|
||||
import { useMobileScrollLock } from "../hooks/useMobileScrollLock";
|
||||
import { useOverlayDismiss } from "../hooks/useOverlayDismiss";
|
||||
import { useEmbeddedPresentation, type ModalPresentation } from "../hooks/useEmbeddedPresentation";
|
||||
import { getScopedItem, setScopedItem } from "../utils/projectStorage";
|
||||
|
||||
interface GitHubImportModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -38,15 +39,32 @@ interface GitHubImportModalProps {
|
||||
// Mobile and two-pane breakpoints in pixels
|
||||
const MOBILE_BREAKPOINT = 640;
|
||||
const TWO_PANE_BREAKPOINT = 860;
|
||||
const GITHUB_IMPORT_LIST_PANE_MIN_WIDTH = 240;
|
||||
const GITHUB_IMPORT_LIST_PANE_MAX_WIDTH = 640;
|
||||
const GITHUB_IMPORT_LIST_PANE_DEFAULT_WIDTH = 360;
|
||||
const GITHUB_IMPORT_LIST_PANE_STORAGE_KEY = "fusion:github-import-list-pane-width";
|
||||
/*
|
||||
FNXC:GitHubImport 2026-06-23-00:30:
|
||||
The Import Tasks two-pane split (Issues AND Pull Requests share the same workspace/list/preview structure) must let the user
|
||||
shrink the LEFT list far below its old fixed share so the RIGHT preview gets the freed space. Default the list narrow (256px),
|
||||
clamp to [160px, min(480px, 50% of container)] so the preview always keeps at least half. Width is user-resizable via a drag
|
||||
handle and persisted per-project through projectStorage (key `kb-dashboard-github-import-list-width`) so each repo context keeps
|
||||
its own split. The freed width flows to the preview because the preview is `flex: 1 1 auto; min-width: 0` (fills remainder).
|
||||
*/
|
||||
const GITHUB_IMPORT_LIST_PANE_MIN_WIDTH = 160;
|
||||
const GITHUB_IMPORT_LIST_PANE_MAX_WIDTH = 480;
|
||||
const GITHUB_IMPORT_LIST_PANE_MAX_RATIO = 0.5;
|
||||
const GITHUB_IMPORT_LIST_PANE_DEFAULT_WIDTH = 256;
|
||||
const GITHUB_IMPORT_LIST_PANE_KEYBOARD_STEP = 16;
|
||||
const GITHUB_IMPORT_LIST_WIDTH_STORAGE_KEY = "kb-dashboard-github-import-list-width";
|
||||
|
||||
type TabType = "issues" | "pulls";
|
||||
|
||||
function clampListPaneWidth(width: number) {
|
||||
return Math.max(GITHUB_IMPORT_LIST_PANE_MIN_WIDTH, Math.min(GITHUB_IMPORT_LIST_PANE_MAX_WIDTH, width));
|
||||
/**
|
||||
* Clamp the list-pane width to [MIN, min(MAX, container * MAX_RATIO)].
|
||||
* The container-relative cap guarantees the preview pane keeps at least half the workspace even on narrow screens.
|
||||
* `containerWidth <= 0` (e.g. unmeasured/test) falls back to the absolute MAX so the static bound still applies.
|
||||
*/
|
||||
function clampListPaneWidth(width: number, containerWidth = 0) {
|
||||
const ratioMax = containerWidth > 0 ? containerWidth * GITHUB_IMPORT_LIST_PANE_MAX_RATIO : Number.POSITIVE_INFINITY;
|
||||
const maxWidth = Math.min(GITHUB_IMPORT_LIST_PANE_MAX_WIDTH, ratioMax);
|
||||
return Math.max(GITHUB_IMPORT_LIST_PANE_MIN_WIDTH, Math.min(maxWidth, width));
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -95,13 +113,15 @@ export function GitHubImportModal({ isOpen, onClose, onImport, tasks, projectId,
|
||||
const [isMobile, setIsMobile] = useState(false);
|
||||
const [canResizePanes, setCanResizePanes] = useState(false);
|
||||
const [mobileView, setMobileView] = useState<"list" | "preview">("list");
|
||||
// Workspace flex-row container; used to measure available width for the container-relative resize clamp.
|
||||
const workspaceRef = useRef<HTMLDivElement>(null);
|
||||
// Parks the active drag teardown (release capture + remove listeners) so it runs once on pointerup/cancel/unmount.
|
||||
const listResizeTeardownRef = useRef<(() => void) | null>(null);
|
||||
// rAF handle so pointermove width updates are batched to one state write per frame.
|
||||
const listResizeFrameRef = useRef<number | null>(null);
|
||||
const [listPaneWidth, setListPaneWidth] = useState(() => {
|
||||
if (typeof window === "undefined") {
|
||||
return GITHUB_IMPORT_LIST_PANE_DEFAULT_WIDTH;
|
||||
}
|
||||
|
||||
try {
|
||||
const stored = window.localStorage.getItem(GITHUB_IMPORT_LIST_PANE_STORAGE_KEY);
|
||||
const stored = getScopedItem(GITHUB_IMPORT_LIST_WIDTH_STORAGE_KEY, projectId);
|
||||
if (!stored) {
|
||||
return GITHUB_IMPORT_LIST_PANE_DEFAULT_WIDTH;
|
||||
}
|
||||
@@ -328,60 +348,119 @@ export function GitHubImportModal({ isOpen, onClose, onImport, tasks, projectId,
|
||||
return () => window.removeEventListener("resize", checkViewportBands);
|
||||
}, [isOpen]);
|
||||
|
||||
// Persist the (already clamped) width per-project; best-effort, scoped so each repo context keeps its own split.
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
window.localStorage.setItem(GITHUB_IMPORT_LIST_PANE_STORAGE_KEY, String(listPaneWidth));
|
||||
setScopedItem(GITHUB_IMPORT_LIST_WIDTH_STORAGE_KEY, String(listPaneWidth), projectId);
|
||||
} catch {
|
||||
// Ignore storage write failures.
|
||||
}
|
||||
}, [listPaneWidth]);
|
||||
}, [listPaneWidth, projectId]);
|
||||
|
||||
/*
|
||||
FNXC:GitHubImport 2026-06-23-00:30:
|
||||
Mirror the proven MailboxView split drag. Pointer events + setPointerCapture keep the drag tracking even when the cursor
|
||||
leaves the thin handle. Each move maps the pointer X to a list-pane width relative to the workspace's left edge, clamped to
|
||||
[MIN, min(MAX, container * MAX_RATIO)] so the preview keeps at least half. Updates are rAF-batched (one state write per
|
||||
frame). The teardown (release capture + remove listeners + cancel frame) runs once on pointerup/pointercancel and is parked
|
||||
in listResizeTeardownRef for unmount safety. Resize only applies in the wide two-pane band (canResizePanes).
|
||||
*/
|
||||
const handleListPaneResizeStart = useCallback((event: ReactPointerEvent<HTMLDivElement>) => {
|
||||
if (!canResizePanes) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
|
||||
listResizeTeardownRef.current?.();
|
||||
|
||||
const handle = event.currentTarget;
|
||||
const pointerId = event.pointerId;
|
||||
const workspaceRect = workspaceRef.current?.getBoundingClientRect();
|
||||
// Fall back to pointer-relative delta math when the workspace is unmeasured (e.g. jsdom layout-less tests).
|
||||
const startX = event.clientX;
|
||||
const startWidth = listPaneWidth;
|
||||
const target = event.currentTarget;
|
||||
target.setPointerCapture(event.pointerId);
|
||||
|
||||
const handlePointerMove = (moveEvent: globalThis.PointerEvent) => {
|
||||
const deltaX = moveEvent.clientX - startX;
|
||||
setListPaneWidth(clampListPaneWidth(startWidth + deltaX));
|
||||
// Latest pointer X awaiting a frame; flushed on the next rAF or synchronously at teardown so the final drag position is never dropped.
|
||||
let pendingClientX: number | null = null;
|
||||
|
||||
const applyWidth = (clientX: number) => {
|
||||
const containerWidth = workspaceRect?.width ?? 0;
|
||||
const proposed = workspaceRect ? clientX - workspaceRect.left : startWidth + (clientX - startX);
|
||||
setListPaneWidth(clampListPaneWidth(proposed, containerWidth));
|
||||
};
|
||||
|
||||
const handlePointerUp = () => {
|
||||
document.removeEventListener("pointermove", handlePointerMove);
|
||||
document.removeEventListener("pointerup", handlePointerUp);
|
||||
if (target.hasPointerCapture(event.pointerId)) {
|
||||
target.releasePointerCapture(event.pointerId);
|
||||
const flushPending = () => {
|
||||
listResizeFrameRef.current = null;
|
||||
if (pendingClientX !== null) {
|
||||
const clientX = pendingClientX;
|
||||
pendingClientX = null;
|
||||
applyWidth(clientX);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("pointermove", handlePointerMove);
|
||||
document.addEventListener("pointerup", handlePointerUp);
|
||||
const onPointerMove = (moveEvent: globalThis.PointerEvent) => {
|
||||
if (moveEvent.pointerId !== pointerId) return;
|
||||
pendingClientX = moveEvent.clientX;
|
||||
if (listResizeFrameRef.current !== null) return;
|
||||
const schedule = typeof window !== "undefined" && typeof window.requestAnimationFrame === "function"
|
||||
? window.requestAnimationFrame
|
||||
: (cb: FrameRequestCallback) => { cb(0); return 0; };
|
||||
listResizeFrameRef.current = schedule(flushPending);
|
||||
};
|
||||
|
||||
const teardown = () => {
|
||||
document.removeEventListener("pointermove", onPointerMove);
|
||||
document.removeEventListener("pointerup", teardown);
|
||||
document.removeEventListener("pointercancel", teardown);
|
||||
if (listResizeFrameRef.current !== null && typeof window !== "undefined" && typeof window.cancelAnimationFrame === "function") {
|
||||
window.cancelAnimationFrame(listResizeFrameRef.current);
|
||||
listResizeFrameRef.current = null;
|
||||
}
|
||||
// Apply any width queued for a frame that never fired so the final drag position sticks (and tests stay deterministic).
|
||||
flushPending();
|
||||
try {
|
||||
handle.releasePointerCapture(pointerId);
|
||||
} catch {
|
||||
// Pointer capture may already be released; ignore.
|
||||
}
|
||||
listResizeTeardownRef.current = null;
|
||||
};
|
||||
|
||||
listResizeTeardownRef.current = teardown;
|
||||
|
||||
try {
|
||||
handle.setPointerCapture(pointerId);
|
||||
} catch {
|
||||
// setPointerCapture can throw in non-DOM test environments; drag still works via listeners.
|
||||
}
|
||||
// Listen on document so the drag keeps tracking even when the pointer leaves the thin handle.
|
||||
document.addEventListener("pointermove", onPointerMove);
|
||||
document.addEventListener("pointerup", teardown);
|
||||
document.addEventListener("pointercancel", teardown);
|
||||
}, [canResizePanes, listPaneWidth]);
|
||||
|
||||
// Detach any in-flight drag on unmount.
|
||||
useEffect(() => () => {
|
||||
listResizeTeardownRef.current?.();
|
||||
}, []);
|
||||
|
||||
const handleListPaneResizeKeyDown = useCallback((event: ReactKeyboardEvent<HTMLDivElement>) => {
|
||||
if (!canResizePanes) {
|
||||
return;
|
||||
}
|
||||
|
||||
const step = event.shiftKey ? 50 : 10;
|
||||
const containerWidth = workspaceRef.current?.clientWidth ?? 0;
|
||||
const step = event.shiftKey ? GITHUB_IMPORT_LIST_PANE_KEYBOARD_STEP * 4 : GITHUB_IMPORT_LIST_PANE_KEYBOARD_STEP;
|
||||
|
||||
if (event.key === "ArrowLeft") {
|
||||
event.preventDefault();
|
||||
setListPaneWidth((current) => clampListPaneWidth(current - step));
|
||||
setListPaneWidth((current) => clampListPaneWidth(current - step, containerWidth));
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === "ArrowRight") {
|
||||
event.preventDefault();
|
||||
setListPaneWidth((current) => clampListPaneWidth(current + step));
|
||||
setListPaneWidth((current) => clampListPaneWidth(current + step, containerWidth));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -393,7 +472,7 @@ export function GitHubImportModal({ isOpen, onClose, onImport, tasks, projectId,
|
||||
|
||||
if (event.key === "End") {
|
||||
event.preventDefault();
|
||||
setListPaneWidth(GITHUB_IMPORT_LIST_PANE_MAX_WIDTH);
|
||||
setListPaneWidth(clampListPaneWidth(GITHUB_IMPORT_LIST_PANE_MAX_WIDTH, containerWidth));
|
||||
}
|
||||
}, [canResizePanes]);
|
||||
|
||||
@@ -662,11 +741,20 @@ export function GitHubImportModal({ isOpen, onClose, onImport, tasks, projectId,
|
||||
)}
|
||||
|
||||
{/* Two-pane workspace */}
|
||||
<div className="github-import-workspace">
|
||||
<div className="github-import-workspace" ref={workspaceRef}>
|
||||
{/* Left pane: Issue/PR list */}
|
||||
<section
|
||||
className={`github-import-list-pane ${isMobile ? 'mobile' : ''} ${mobileView === 'list' ? 'active' : ''}`}
|
||||
style={canResizePanes ? { flex: `0 0 ${listPaneWidth}px` } : undefined}
|
||||
/*
|
||||
FNXC:GitHubImport 2026-06-23-00:30:
|
||||
Drive the wide two-pane width from a CSS var so the embedded layout's container query can apply it with the
|
||||
precedence it needs (`flex-basis: var(--gh-import-list-width) !important`) WITHOUT the stacked/narrow rule's
|
||||
own `!important` reset stomping it. `flex` is also set inline for the non-embedded (dialog) presentation, which
|
||||
has no competing `!important`. Both surfaces (Issues + Pull Requests) share this single list pane.
|
||||
*/
|
||||
style={canResizePanes
|
||||
? ({ flex: `0 0 ${listPaneWidth}px`, ["--gh-import-list-width" as string]: `${listPaneWidth}px` } as CSSProperties)
|
||||
: undefined}
|
||||
data-testid="github-import-list-pane"
|
||||
aria-labelledby="github-import-results-heading"
|
||||
>
|
||||
@@ -806,7 +894,7 @@ export function GitHubImportModal({ isOpen, onClose, onImport, tasks, projectId,
|
||||
|
||||
{canResizePanes && (
|
||||
<div
|
||||
className="github-import-workspace__resize-handle"
|
||||
className="github-import-workspace__resize-handle github-import-resize-handle"
|
||||
role="separator"
|
||||
aria-orientation="vertical"
|
||||
aria-label={t("git.resizeIssuesList", "Resize issues list")}
|
||||
|
||||
@@ -1083,20 +1083,29 @@ describe("GitHubImportModal", () => {
|
||||
fireEvent.pointerUp(document, { pointerId: 1, clientX: endX });
|
||||
};
|
||||
|
||||
/*
|
||||
* FNXC:GitHubImport 2026-06-23-00:30:
|
||||
* The list pane defaults narrow (256px) and clamps to [160px, 480px] (the absolute cap; a 50%-of-container cap also
|
||||
* applies once the workspace is measured, which jsdom reports as 0 so the absolute cap governs here). Width persists
|
||||
* per-project via projectStorage under the unscoped key `kb-dashboard-github-import-list-width` (no projectId in tests).
|
||||
* Pointer drags map absolute pointer X to the list width relative to the workspace left edge (jsdom rect is all-zeros).
|
||||
*/
|
||||
const LIST_WIDTH_KEY = "kb-dashboard-github-import-list-width";
|
||||
|
||||
beforeEach(() => {
|
||||
window.localStorage.removeItem("fusion:github-import-list-pane-width");
|
||||
window.localStorage.removeItem(LIST_WIDTH_KEY);
|
||||
setViewportWidth(1200);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.localStorage.removeItem("fusion:github-import-list-pane-width");
|
||||
window.localStorage.removeItem(LIST_WIDTH_KEY);
|
||||
setViewportWidth(originalInnerWidth);
|
||||
});
|
||||
|
||||
it("renders handle only in the side-by-side two-pane band", async () => {
|
||||
await renderWithIssues();
|
||||
expect(screen.getByTestId("github-import-resize-handle")).toBeTruthy();
|
||||
expect(screen.getByTestId("github-import-list-pane").getAttribute("style")).toContain("flex: 0 0 360px");
|
||||
expect(screen.getByTestId("github-import-list-pane").getAttribute("style")).toContain("flex: 0 0 256px");
|
||||
|
||||
setViewportWidth(800);
|
||||
|
||||
@@ -1118,41 +1127,54 @@ describe("GitHubImportModal", () => {
|
||||
const handle = screen.getByTestId("github-import-resize-handle");
|
||||
const listPane = screen.getByTestId("github-import-list-pane");
|
||||
|
||||
dragHandle(handle, 100, 160);
|
||||
expect(handle.getAttribute("aria-valuenow")).toBe("420");
|
||||
expect(listPane.getAttribute("style")).toContain("flex: 0 0 420px");
|
||||
// jsdom workspace rect is all-zeros, so the pane width equals the clamped absolute pointer X.
|
||||
dragHandle(handle, 256, 300);
|
||||
expect(handle.getAttribute("aria-valuenow")).toBe("300");
|
||||
expect(listPane.getAttribute("style")).toContain("flex: 0 0 300px");
|
||||
|
||||
dragHandle(handle, 160, 120);
|
||||
expect(handle.getAttribute("aria-valuenow")).toBe("380");
|
||||
expect(listPane.getAttribute("style")).toContain("flex: 0 0 380px");
|
||||
dragHandle(handle, 300, 200);
|
||||
expect(handle.getAttribute("aria-valuenow")).toBe("200");
|
||||
expect(listPane.getAttribute("style")).toContain("flex: 0 0 200px");
|
||||
|
||||
dragHandle(handle, 120, -200);
|
||||
expect(handle.getAttribute("aria-valuenow")).toBe("240");
|
||||
expect(listPane.getAttribute("style")).toContain("flex: 0 0 240px");
|
||||
// Below the 160px minimum clamps up.
|
||||
dragHandle(handle, 200, 40);
|
||||
expect(handle.getAttribute("aria-valuenow")).toBe("160");
|
||||
expect(listPane.getAttribute("style")).toContain("flex: 0 0 160px");
|
||||
|
||||
dragHandle(handle, -200, 700);
|
||||
expect(handle.getAttribute("aria-valuenow")).toBe("640");
|
||||
expect(listPane.getAttribute("style")).toContain("flex: 0 0 640px");
|
||||
// Above the 480px maximum clamps down.
|
||||
dragHandle(handle, 40, 900);
|
||||
expect(handle.getAttribute("aria-valuenow")).toBe("480");
|
||||
expect(listPane.getAttribute("style")).toContain("flex: 0 0 480px");
|
||||
});
|
||||
|
||||
it("exposes the resize width as an inline CSS var for the embedded container query", async () => {
|
||||
await renderWithIssues();
|
||||
const listPane = screen.getByTestId("github-import-list-pane");
|
||||
expect(listPane.getAttribute("style")).toContain("--gh-import-list-width: 256px");
|
||||
|
||||
const handle = screen.getByTestId("github-import-resize-handle");
|
||||
dragHandle(handle, 256, 320);
|
||||
expect(listPane.getAttribute("style")).toContain("--gh-import-list-width: 320px");
|
||||
});
|
||||
|
||||
it("renders the desktop handle regardless of list content or active tab", async () => {
|
||||
const mounted = await renderWithEmptyIssues();
|
||||
expect(screen.getByTestId("github-import-resize-handle")).toBeTruthy();
|
||||
expect(screen.getByTestId("github-import-list-pane").getAttribute("style")).toContain("flex: 0 0 360px");
|
||||
expect(screen.getByTestId("github-import-list-pane").getAttribute("style")).toContain("flex: 0 0 256px");
|
||||
mounted.unmount();
|
||||
|
||||
vi.clearAllMocks();
|
||||
window.localStorage.removeItem("fusion:github-import-list-pane-width");
|
||||
window.localStorage.removeItem(LIST_WIDTH_KEY);
|
||||
setViewportWidth(1200);
|
||||
|
||||
await renderWithPulls();
|
||||
expect(screen.getByTestId("github-import-resize-handle")).toBeTruthy();
|
||||
expect(screen.getByTestId("github-import-list-pane").getAttribute("style")).toContain("flex: 0 0 360px");
|
||||
expect(screen.getByTestId("github-import-list-pane").getAttribute("style")).toContain("flex: 0 0 256px");
|
||||
});
|
||||
it.each([
|
||||
[{ key: "ArrowRight" }, 370],
|
||||
[{ key: "ArrowLeft" }, 350],
|
||||
[{ key: "ArrowRight", shiftKey: true }, 410],
|
||||
[{ key: "ArrowRight" }, 272],
|
||||
[{ key: "ArrowLeft" }, 240],
|
||||
[{ key: "ArrowRight", shiftKey: true }, 320],
|
||||
])("handles keyboard nudge %#", async (eventInit, expected) => {
|
||||
await renderWithIssues();
|
||||
const handle = screen.getByTestId("github-import-resize-handle");
|
||||
@@ -1167,10 +1189,10 @@ describe("GitHubImportModal", () => {
|
||||
const handle = screen.getByTestId("github-import-resize-handle");
|
||||
|
||||
fireEvent.keyDown(handle, { key: "Home" });
|
||||
expect(handle.getAttribute("aria-valuenow")).toBe("240");
|
||||
expect(handle.getAttribute("aria-valuenow")).toBe("160");
|
||||
|
||||
fireEvent.keyDown(handle, { key: "End" });
|
||||
expect(handle.getAttribute("aria-valuenow")).toBe("640");
|
||||
expect(handle.getAttribute("aria-valuenow")).toBe("480");
|
||||
});
|
||||
|
||||
it("clamps keyboard resizing to min and max bounds", async () => {
|
||||
@@ -1180,12 +1202,12 @@ describe("GitHubImportModal", () => {
|
||||
for (let i = 0; i < 30; i += 1) {
|
||||
fireEvent.keyDown(handle, { key: "ArrowLeft" });
|
||||
}
|
||||
expect(handle.getAttribute("aria-valuenow")).toBe("240");
|
||||
expect(handle.getAttribute("aria-valuenow")).toBe("160");
|
||||
|
||||
for (let i = 0; i < 60; i += 1) {
|
||||
fireEvent.keyDown(handle, { key: "ArrowRight" });
|
||||
}
|
||||
expect(handle.getAttribute("aria-valuenow")).toBe("640");
|
||||
expect(handle.getAttribute("aria-valuenow")).toBe("480");
|
||||
});
|
||||
|
||||
it("persists width across remounts", async () => {
|
||||
@@ -1193,20 +1215,29 @@ describe("GitHubImportModal", () => {
|
||||
let handle = screen.getByTestId("github-import-resize-handle");
|
||||
|
||||
fireEvent.keyDown(handle, { key: "ArrowRight", shiftKey: true });
|
||||
expect(handle.getAttribute("aria-valuenow")).toBe("410");
|
||||
expect(handle.getAttribute("aria-valuenow")).toBe("320");
|
||||
// Persisted under the projectStorage key (unscoped without a projectId).
|
||||
expect(window.localStorage.getItem(LIST_WIDTH_KEY)).toBe("320");
|
||||
|
||||
mounted.unmount();
|
||||
|
||||
await renderWithIssues();
|
||||
handle = await screen.findByTestId("github-import-resize-handle");
|
||||
expect(handle.getAttribute("aria-valuenow")).toBe("410");
|
||||
expect(handle.getAttribute("aria-valuenow")).toBe("320");
|
||||
});
|
||||
|
||||
it("clamps an out-of-range stored width back into bounds on mount", async () => {
|
||||
window.localStorage.setItem(LIST_WIDTH_KEY, "9000");
|
||||
await renderWithIssues();
|
||||
// Stored value above the 480px max is clamped down on read.
|
||||
expect(screen.getByTestId("github-import-resize-handle").getAttribute("aria-valuenow")).toBe("480");
|
||||
});
|
||||
|
||||
it("falls back to default width for invalid stored values", async () => {
|
||||
window.localStorage.setItem("fusion:github-import-list-pane-width", "not-a-number");
|
||||
window.localStorage.setItem(LIST_WIDTH_KEY, "not-a-number");
|
||||
await renderWithIssues();
|
||||
|
||||
expect(screen.getByTestId("github-import-resize-handle").getAttribute("aria-valuenow")).toBe("360");
|
||||
expect(screen.getByTestId("github-import-resize-handle").getAttribute("aria-valuenow")).toBe("256");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -85,6 +85,7 @@ describe("projectStorage", () => {
|
||||
"kb-dashboard-list-sidebar-width",
|
||||
"kb-dashboard-mailbox-sidebar-width",
|
||||
"kb-dashboard-agents-sidebar-width",
|
||||
"kb-dashboard-github-import-list-width",
|
||||
"kb-quick-entry-text",
|
||||
"kb-inline-create-text",
|
||||
"fn-agent-view",
|
||||
@@ -101,10 +102,11 @@ describe("projectStorage", () => {
|
||||
"kb-dashboard-base-branch-filter",
|
||||
"kb-capacity-risk-banner-dismissed",
|
||||
"kb-files-line-numbers",
|
||||
"kb-dashboard-dock-files-current",
|
||||
"fusion-plugin-dependency-graph:positions",
|
||||
]),
|
||||
);
|
||||
expect(PROJECT_STORAGE_KEYS).toHaveLength(26);
|
||||
expect(PROJECT_STORAGE_KEYS).toHaveLength(28);
|
||||
});
|
||||
|
||||
it("stores branch filter values as scoped strings per project", () => {
|
||||
|
||||
@@ -18,6 +18,7 @@ export const PROJECT_STORAGE_KEYS: string[] = [
|
||||
"kb-dashboard-list-sidebar-width",
|
||||
"kb-dashboard-mailbox-sidebar-width",
|
||||
"kb-dashboard-agents-sidebar-width",
|
||||
"kb-dashboard-github-import-list-width",
|
||||
"kb-quick-entry-text",
|
||||
"kb-inline-create-text",
|
||||
"fn-agent-view",
|
||||
|
||||
Reference in New Issue
Block a user