fix(windows): fenêtres de panneau détachées suivent le projet en focus côté UI (#47)
Partie frontend. Ajoute un adaptateur focusedProject et un port dédié : la ViewWindow détachée n'est plus liée à un project_id figé, elle s'abonne à l'event focused-project émis par la fenêtre principale et affiche le panneau du projet courant. ProjectsView propage le focus ; le détachement crée une fenêtre panel-only. Couvert par les tests window/ViewWindow/focusedProject/ ProjectsView.focus. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -91,6 +91,8 @@ import type {
|
||||
UiPreferencesGateway,
|
||||
ViewWindowClosed,
|
||||
WindowGateway,
|
||||
FocusedProject,
|
||||
FocusedProjectGateway,
|
||||
WorkStateGateway,
|
||||
} from "@/ports";
|
||||
import { normalizeProjectWorkState } from "../workStateNormalization";
|
||||
@ -1094,27 +1096,25 @@ class MockRemoteGateway implements RemoteGateway {
|
||||
}
|
||||
|
||||
/**
|
||||
* In-memory {@link WindowGateway} for tests/dev (#23). Tracks which views are
|
||||
* "detached" in a set keyed by `panel|projectId`, and lets tests simulate the
|
||||
* OS closing a window via {@link simulateOsClose}. `closeViewWindow` also emits
|
||||
* the close event, mirroring the backend (a programmatic close still notifies).
|
||||
* In-memory {@link WindowGateway} for tests/dev (#23, panel-only in #47). Tracks
|
||||
* which views are "detached" in a set keyed by `panel` alone (a panel-only
|
||||
* window follows the focused project, so no project id is part of the key), and
|
||||
* lets tests simulate the OS closing a window via {@link simulateOsClose}.
|
||||
* `closeViewWindow` also emits the close event, mirroring the backend (a
|
||||
* programmatic close still notifies).
|
||||
*/
|
||||
export class MockWindowGateway implements WindowGateway {
|
||||
/** Currently-open detached windows, keyed `panel|projectId`. */
|
||||
/** Currently-open detached windows, keyed by `panel`. */
|
||||
readonly open = new Set<string>();
|
||||
private readonly listeners = new Set<(e: ViewWindowClosed) => void>();
|
||||
|
||||
private key(panel: string, projectId: string): string {
|
||||
return `${panel}|${projectId}`;
|
||||
async openViewWindow(panel: string): Promise<void> {
|
||||
this.open.add(panel);
|
||||
}
|
||||
|
||||
async openViewWindow(panel: string, projectId: string): Promise<void> {
|
||||
this.open.add(this.key(panel, projectId));
|
||||
}
|
||||
|
||||
async closeViewWindow(panel: string, projectId: string): Promise<void> {
|
||||
if (this.open.delete(this.key(panel, projectId))) {
|
||||
this.emit({ panel, projectId });
|
||||
async closeViewWindow(panel: string): Promise<void> {
|
||||
if (this.open.delete(panel)) {
|
||||
this.emit({ panel });
|
||||
}
|
||||
}
|
||||
|
||||
@ -1128,9 +1128,9 @@ export class MockWindowGateway implements WindowGateway {
|
||||
}
|
||||
|
||||
/** Test hook: simulate the user closing the OS window (fires the event). */
|
||||
simulateOsClose(panel: string, projectId: string): void {
|
||||
this.open.delete(this.key(panel, projectId));
|
||||
this.emit({ panel, projectId });
|
||||
simulateOsClose(panel: string): void {
|
||||
this.open.delete(panel);
|
||||
this.emit({ panel });
|
||||
}
|
||||
|
||||
private emit(event: ViewWindowClosed): void {
|
||||
@ -1138,6 +1138,38 @@ export class MockWindowGateway implements WindowGateway {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* In-memory {@link FocusedProjectGateway} for tests/dev (#47). Holds the current
|
||||
* focus in memory and fans changes out to subscribers — the mock counterpart to
|
||||
* the backend's shared focused-project state + `focused-project://changed`
|
||||
* event. `setFocusedProject` always notifies (mirrors the backend emit), so a
|
||||
* detached-window test can drive a panel window's focus transitions.
|
||||
*/
|
||||
export class MockFocusedProjectGateway implements FocusedProjectGateway {
|
||||
private focused: FocusedProject | null = null;
|
||||
private readonly listeners = new Set<
|
||||
(project: FocusedProject | null) => void
|
||||
>();
|
||||
|
||||
async setFocusedProject(project: FocusedProject | null): Promise<void> {
|
||||
this.focused = project;
|
||||
for (const l of this.listeners) l(project);
|
||||
}
|
||||
|
||||
async getFocusedProject(): Promise<FocusedProject | null> {
|
||||
return this.focused;
|
||||
}
|
||||
|
||||
async onFocusedProjectChanged(
|
||||
handler: (project: FocusedProject | null) => void,
|
||||
): Promise<Unsubscribe> {
|
||||
this.listeners.add(handler);
|
||||
return () => {
|
||||
this.listeners.delete(handler);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The pre-filled reference catalogue the mock serves — mirror of the backend's
|
||||
* **selectable** catalogue (§17.3/D7): only profiles drivable in structured mode
|
||||
@ -2596,6 +2628,7 @@ export function createMockGateways(): Gateways {
|
||||
conversation: new MockConversationGateway(),
|
||||
ticket: new MockTicketGateway(systemGateway),
|
||||
window: new MockWindowGateway(),
|
||||
focusedProject: new MockFocusedProjectGateway(),
|
||||
uiPreferences: new MockUiPreferencesGateway(),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user