refactor: dedupe split-view render, strengthen persistence test
- Extract ConsoleArea helper to eliminate ~60 lines of duplicated layout toggle + grid rendering shared between currentLane and !currentLane branches - Fix persistence test to validate the component's actual write path instead of manually re-seeding localStorage (proves writeSplitViewState is called) - Remove orphaned grid/pane code left by incomplete merge
This commit is contained in:
+123
-118
@@ -54,6 +54,97 @@ import type { SplitViewState, SplitLayout } from "../lib/splitViewStorage";
|
|||||||
|
|
||||||
// ── Page ──────────────────────────────────────────────────────────────
|
// ── Page ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function ConsoleArea({
|
||||||
|
lanes,
|
||||||
|
selectedLaneId,
|
||||||
|
splitView,
|
||||||
|
setLayout,
|
||||||
|
setPaneLaneId,
|
||||||
|
binaryStatus,
|
||||||
|
cwdSuggestions,
|
||||||
|
activeRuns,
|
||||||
|
wsConnected,
|
||||||
|
defaultCwd,
|
||||||
|
onHasActiveRunChange,
|
||||||
|
onLaneCreated,
|
||||||
|
onLaneIdChange,
|
||||||
|
}: {
|
||||||
|
lanes: Lane[];
|
||||||
|
selectedLaneId: number | null;
|
||||||
|
splitView: SplitViewState;
|
||||||
|
setLayout: (layout: SplitLayout) => void;
|
||||||
|
setPaneLaneId: (index: number, id: number) => void;
|
||||||
|
binaryStatus: { found: boolean; path: string | null } | null;
|
||||||
|
cwdSuggestions: CwdSuggestion[];
|
||||||
|
activeRuns: RunListResponse | null;
|
||||||
|
wsConnected: boolean;
|
||||||
|
defaultCwd: string;
|
||||||
|
onHasActiveRunChange: (val: boolean) => void;
|
||||||
|
onLaneCreated: (lane: Lane) => void;
|
||||||
|
onLaneIdChange: (id: number | null) => void;
|
||||||
|
}) {
|
||||||
|
const { t: tLanes } = useTranslation("lanes");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
{([1, 2, 4] as const).map((n) => (
|
||||||
|
<button
|
||||||
|
key={n}
|
||||||
|
type="button"
|
||||||
|
aria-pressed={splitView.layout === n}
|
||||||
|
onClick={() => setLayout(n)}
|
||||||
|
className={`rounded border px-2 py-1 text-xs ${
|
||||||
|
splitView.layout === n
|
||||||
|
? "border-accent bg-accent/15 text-accent"
|
||||||
|
: "border-border text-fg-secondary hover:border-border-light"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{tLanes("splitView.paneCount", { count: n })}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{splitView.layout === 1 ? (
|
||||||
|
<LaneConsolePane
|
||||||
|
lanes={lanes}
|
||||||
|
laneId={selectedLaneId}
|
||||||
|
showLaneSelector={false}
|
||||||
|
onLaneIdChange={onLaneIdChange}
|
||||||
|
onLaneCreated={onLaneCreated}
|
||||||
|
binaryStatus={binaryStatus}
|
||||||
|
cwdSuggestions={cwdSuggestions}
|
||||||
|
activeRuns={activeRuns}
|
||||||
|
wsConnected={wsConnected}
|
||||||
|
defaultCwd={defaultCwd}
|
||||||
|
onHasActiveRunChange={onHasActiveRunChange}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className={`grid flex-1 min-h-0 gap-3 ${
|
||||||
|
splitView.layout === 2 ? "grid-cols-2" : "grid-cols-2 grid-rows-2"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{splitView.paneLaneIds.map((id, i) => (
|
||||||
|
<LaneConsolePane
|
||||||
|
key={i}
|
||||||
|
lanes={lanes}
|
||||||
|
laneId={id}
|
||||||
|
showLaneSelector
|
||||||
|
onLaneIdChange={(newId) => setPaneLaneId(i, newId)}
|
||||||
|
onLaneCreated={onLaneCreated}
|
||||||
|
binaryStatus={binaryStatus}
|
||||||
|
cwdSuggestions={cwdSuggestions}
|
||||||
|
activeRuns={activeRuns}
|
||||||
|
wsConnected={wsConnected}
|
||||||
|
defaultCwd={defaultCwd}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function Workspace() {
|
export function Workspace() {
|
||||||
const { t: tLanes } = useTranslation("lanes");
|
const { t: tLanes } = useTranslation("lanes");
|
||||||
const wsConnected = useSyncExternalStore(eventBus.onConnection, () => eventBus.connected);
|
const wsConnected = useSyncExternalStore(eventBus.onConnection, () => eventBus.connected);
|
||||||
@@ -552,132 +643,46 @@ export function Workspace() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex min-h-0 flex-col gap-2 border-t border-border pt-3">
|
<div className="flex min-h-0 flex-col gap-2 border-t border-border pt-3">
|
||||||
<div className="flex items-center gap-1.5">
|
<ConsoleArea
|
||||||
{([1, 2, 4] as const).map((n) => (
|
|
||||||
<button
|
|
||||||
key={n}
|
|
||||||
type="button"
|
|
||||||
aria-pressed={splitView.layout === n}
|
|
||||||
onClick={() => setLayout(n)}
|
|
||||||
className={`rounded border px-2 py-1 text-xs ${
|
|
||||||
splitView.layout === n
|
|
||||||
? "border-accent bg-accent/15 text-accent"
|
|
||||||
: "border-border text-fg-secondary hover:border-border-light"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{tLanes("splitView.paneCount", { count: n })}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
{splitView.layout === 1 ? (
|
|
||||||
<LaneConsolePane
|
|
||||||
lanes={lanes}
|
|
||||||
laneId={selectedLaneId}
|
|
||||||
showLaneSelector={false}
|
|
||||||
onLaneIdChange={(id) => setSelectedLaneId(id)}
|
|
||||||
onLaneCreated={(lane) =>
|
|
||||||
setLanes((prev) => (prev.some((l) => l.id === lane.id) ? prev : [...prev, lane]))
|
|
||||||
}
|
|
||||||
binaryStatus={binaryStatus}
|
|
||||||
cwdSuggestions={cwdSuggestions}
|
|
||||||
activeRuns={activeRuns}
|
|
||||||
wsConnected={wsConnected}
|
|
||||||
defaultCwd={defaultCwd}
|
|
||||||
onHasActiveRunChange={setPaneHasActiveRun}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div
|
|
||||||
className={`grid flex-1 min-h-0 gap-3 ${
|
|
||||||
splitView.layout === 2 ? "grid-cols-2" : "grid-cols-2 grid-rows-2"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{splitView.paneLaneIds.map((id, i) => (
|
|
||||||
<LaneConsolePane
|
|
||||||
key={i}
|
|
||||||
lanes={lanes}
|
|
||||||
laneId={id}
|
|
||||||
showLaneSelector
|
|
||||||
onLaneIdChange={(newId) => setPaneLaneId(i, newId)}
|
|
||||||
onLaneCreated={(lane) =>
|
|
||||||
setLanes((prev) =>
|
|
||||||
prev.some((l) => l.id === lane.id) ? prev : [...prev, lane]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
binaryStatus={binaryStatus}
|
|
||||||
cwdSuggestions={cwdSuggestions}
|
|
||||||
activeRuns={activeRuns}
|
|
||||||
wsConnected={wsConnected}
|
|
||||||
defaultCwd={defaultCwd}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!currentLane && (
|
|
||||||
<div className="flex min-h-0 flex-col gap-2">
|
|
||||||
<div className="flex items-center gap-1.5">
|
|
||||||
{([1, 2, 4] as const).map((n) => (
|
|
||||||
<button
|
|
||||||
key={n}
|
|
||||||
type="button"
|
|
||||||
aria-pressed={splitView.layout === n}
|
|
||||||
onClick={() => setLayout(n)}
|
|
||||||
className={`rounded border px-2 py-1 text-xs ${
|
|
||||||
splitView.layout === n
|
|
||||||
? "border-accent bg-accent/15 text-accent"
|
|
||||||
: "border-border text-fg-secondary hover:border-border-light"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{tLanes("splitView.paneCount", { count: n })}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
{splitView.layout === 1 ? (
|
|
||||||
<LaneConsolePane
|
|
||||||
lanes={lanes}
|
lanes={lanes}
|
||||||
laneId={selectedLaneId}
|
selectedLaneId={selectedLaneId}
|
||||||
showLaneSelector={false}
|
splitView={splitView}
|
||||||
onLaneIdChange={(id) => setSelectedLaneId(id)}
|
setLayout={setLayout}
|
||||||
onLaneCreated={(lane) =>
|
setPaneLaneId={setPaneLaneId}
|
||||||
setLanes((prev) => (prev.some((l) => l.id === lane.id) ? prev : [...prev, lane]))
|
|
||||||
}
|
|
||||||
binaryStatus={binaryStatus}
|
binaryStatus={binaryStatus}
|
||||||
cwdSuggestions={cwdSuggestions}
|
cwdSuggestions={cwdSuggestions}
|
||||||
activeRuns={activeRuns}
|
activeRuns={activeRuns}
|
||||||
wsConnected={wsConnected}
|
wsConnected={wsConnected}
|
||||||
defaultCwd={defaultCwd}
|
defaultCwd={defaultCwd}
|
||||||
onHasActiveRunChange={setPaneHasActiveRun}
|
onHasActiveRunChange={setPaneHasActiveRun}
|
||||||
|
onLaneCreated={(lane) =>
|
||||||
|
setLanes((prev) => (prev.some((l) => l.id === lane.id) ? prev : [...prev, lane]))
|
||||||
|
}
|
||||||
|
onLaneIdChange={setSelectedLaneId}
|
||||||
/>
|
/>
|
||||||
) : (
|
</div>
|
||||||
<div
|
</section>
|
||||||
className={`grid flex-1 min-h-0 gap-3 ${
|
)}
|
||||||
splitView.layout === 2 ? "grid-cols-2" : "grid-cols-2 grid-rows-2"
|
|
||||||
}`}
|
{!currentLane && (
|
||||||
>
|
<div className="flex min-h-0 flex-col gap-2">
|
||||||
{splitView.paneLaneIds.map((id, i) => (
|
<ConsoleArea
|
||||||
<LaneConsolePane
|
lanes={lanes}
|
||||||
key={i}
|
selectedLaneId={selectedLaneId}
|
||||||
lanes={lanes}
|
splitView={splitView}
|
||||||
laneId={id}
|
setLayout={setLayout}
|
||||||
showLaneSelector
|
setPaneLaneId={setPaneLaneId}
|
||||||
onLaneIdChange={(newId) => setPaneLaneId(i, newId)}
|
binaryStatus={binaryStatus}
|
||||||
onLaneCreated={(lane) =>
|
cwdSuggestions={cwdSuggestions}
|
||||||
setLanes((prev) =>
|
activeRuns={activeRuns}
|
||||||
prev.some((l) => l.id === lane.id) ? prev : [...prev, lane]
|
wsConnected={wsConnected}
|
||||||
)
|
defaultCwd={defaultCwd}
|
||||||
}
|
onHasActiveRunChange={setPaneHasActiveRun}
|
||||||
binaryStatus={binaryStatus}
|
onLaneCreated={(lane) =>
|
||||||
cwdSuggestions={cwdSuggestions}
|
setLanes((prev) => (prev.some((l) => l.id === lane.id) ? prev : [...prev, lane]))
|
||||||
activeRuns={activeRuns}
|
}
|
||||||
wsConnected={wsConnected}
|
onLaneIdChange={setSelectedLaneId}
|
||||||
defaultCwd={defaultCwd}
|
/>
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -671,12 +671,6 @@ describe("split terminal view", () => {
|
|||||||
await settle();
|
await settle();
|
||||||
unmount();
|
unmount();
|
||||||
|
|
||||||
localStorage.clear();
|
|
||||||
localStorage.setItem(
|
|
||||||
"ccam.workspace.splitView",
|
|
||||||
JSON.stringify({ layout: 2, paneLaneIds: [null, lanesToReturn[1]!.id] })
|
|
||||||
);
|
|
||||||
|
|
||||||
await renderWorkspace();
|
await renderWorkspace();
|
||||||
const persistedSelects = screen.getAllByTestId("pane-lane-select");
|
const persistedSelects = screen.getAllByTestId("pane-lane-select");
|
||||||
expect(persistedSelects).toHaveLength(2);
|
expect(persistedSelects).toHaveLength(2);
|
||||||
|
|||||||
Reference in New Issue
Block a user